diff --git a/.env b/.env index 91c18ed..770653f 100644 --- a/.env +++ b/.env @@ -1,2 +1,4 @@ -OPENROUTER_API_KEY=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 \ No newline at end of file +OPENROUTER_API_KEY=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 +WHATSAPP_PHONE_ID=986583417873961 +WHATSAPP_TOKEN=EAATqIU03y9YBQ5EBDxAFANmJIokKqjceliErZA1rYERpTzZBpRZAIKDVqlE2UWD0YUztSRwvsqjEdX2Uzt92Lsst5CEwcBiLZBbjiK8aAqpDclh2r2KyW5YvZCo7jXZAQf6dNYZABksjuxi5jUdLgfNQbhOhvSfv1z1qoWdZCUh9dUyzEcH3xmtCZBk9VG1qdtuLZBIlT335DSrSKgDpaLTBaWoe54aZCLwTxB89YZA78DkiIFmLq6dZBz3wSuUyMEfKokrRvtz7lXE6VkienXNucslgihZCkAMgZDZD \ No newline at end of file diff --git a/bot.db b/bot.db index 7bf7bc1..514ac89 100644 Binary files a/bot.db and b/bot.db differ diff --git a/db/db.go b/db/db.go index 4bc00a5..42ffff1 100644 --- a/db/db.go +++ b/db/db.go @@ -53,3 +53,21 @@ func SaveAppointment(phone string, date string) error { ) return err } + +// GetOrCreateChatByPhone finds a chat for this phone number or creates one +func GetOrCreateChatByPhone(phone string) int { + // 1. Check if we already have a chat for this phone + // (Note: You might want to add a 'phone' column to 'chats' table if you haven't yet. + // For now, I'll cheat and put the phone in the title if it's new) + var id int + err := Conn.QueryRow("SELECT id FROM chats WHERE title = ?", phone).Scan(&id) + + if err == nil { + return id + } + + // 2. If not found, create one + res, _ := Conn.Exec("INSERT INTO chats (title) VALUES (?)", phone) + newId, _ := res.LastInsertId() + return int(newId) +} diff --git a/handlers/webhook.go b/handlers/webhook.go index dd0a32c..02b4037 100644 --- a/handlers/webhook.go +++ b/handlers/webhook.go @@ -1,17 +1,38 @@ package handlers import ( + "fmt" "net/http" + "whatsapp-bot/db" + "whatsapp-bot/services" "github.com/gin-gonic/gin" ) -// Meta verification -func VerifyWebhook(c *gin.Context) { - challenge := c.Query("hub.challenge") - token := c.Query("hub.verify_token") +// Structs to parse incoming WhatsApp Webhook JSON +type WebhookPayload struct { + Entry []struct { + Changes []struct { + Value struct { + Messages []struct { + From string `json:"from"` + Text struct { + Body string `json:"body"` + } `json:"text"` + Type string `json:"type"` + } `json:"messages"` + } `json:"value"` + } `json:"changes"` + } `json:"entry"` +} - if token == "YOUR_SECRET_TOKEN" { +// VerifyWebhook (Keep this as is) +func VerifyWebhook(c *gin.Context) { + mode := c.Query("hub.mode") + token := c.Query("hub.verify_token") + challenge := c.Query("hub.challenge") + + if mode == "subscribe" && token == "YOUR_SECRET_TOKEN" { // CHANGE THIS to match your Meta setup c.String(http.StatusOK, challenge) } else { c.Status(http.StatusForbidden) @@ -19,7 +40,61 @@ func VerifyWebhook(c *gin.Context) { } func HandleMessage(c *gin.Context) { - // You should bind your WhatsApp types here - // go services.Process(...) - c.Status(http.StatusOK) + var payload WebhookPayload + if err := c.BindJSON(&payload); err != nil { + // WhatsApp sends other events (statuses) that might not match. Ignore errors. + c.Status(200) + return + } + + // 1. Loop through messages (usually just one) + for _, entry := range payload.Entry { + for _, change := range entry.Changes { + for _, msg := range change.Value.Messages { + + // We only handle text for now + if msg.Type != "text" { + continue + } + + userPhone := msg.From + userText := msg.Text.Body + + fmt.Printf("📩 Received from %s: %s\n", userPhone, userText) + + // 2. Identify the Chat Logic + chatID := db.GetOrCreateChatByPhone(userPhone) + + // 3. Save User Message + db.Conn.Exec("INSERT INTO messages (chat_id, role, content) VALUES (?, 'user', ?)", chatID, userText) + + // 4. Get AI Response + // Fetch history + rows, _ := db.Conn.Query("SELECT role, content FROM messages WHERE chat_id = ? ORDER BY created_at ASC", chatID) + var history []services.Message + for rows.Next() { + var m services.Message + rows.Scan(&m.Role, &m.Content) + history = append(history, m) + } + rows.Close() + + // Call AI (We don't need the stream callback here, just the final string) + aiResponse, _ := services.StreamAIResponse(history, func(chunk string) { + // We can't stream to WhatsApp, so we do nothing here. + }) + + // 5. Save & Send Response + if aiResponse != "" { + db.Conn.Exec("INSERT INTO messages (chat_id, role, content) VALUES (?, 'assistant', ?)", chatID, aiResponse) + err := services.SendWhatsAppMessage(userPhone, aiResponse) + if err != nil { + fmt.Println("❌ Error sending to WhatsApp:", err) + } + } + } + } + } + + c.Status(200) } diff --git a/services/whatsapp.go b/services/whatsapp.go index 5e568ea..39b0be2 100644 --- a/services/whatsapp.go +++ b/services/whatsapp.go @@ -1 +1,43 @@ package services + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" + "os" +) + +// SendWhatsAppMessage sends a text reply to a user +func SendWhatsAppMessage(toPhone string, messageBody string) error { + token := os.Getenv("WHATSAPP_TOKEN") // "EAA..." + phoneID := os.Getenv("WHATSAPP_PHONE_ID") // "100..." + version := "v17.0" + url := fmt.Sprintf("https://graph.facebook.com/%s/%s/messages", version, phoneID) + + payload := map[string]interface{}{ + "messaging_product": "whatsapp", + "to": toPhone, + "type": "text", + "text": map[string]string{ + "body": messageBody, + }, + } + + jsonData, _ := json.Marshal(payload) + req, _ := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) + req.Header.Set("Authorization", "Bearer "+token) + req.Header.Set("Content-Type", "application/json") + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode >= 400 { + return fmt.Errorf("whatsapp api error: status %d", resp.StatusCode) + } + + return nil +}