diff --git a/go/controller/conversations/conversations.go b/go/controller/conversations/conversations.go index 87a0fae..e72663d 100644 --- a/go/controller/conversations/conversations.go +++ b/go/controller/conversations/conversations.go @@ -13,7 +13,7 @@ import ( func Index(c *fiber.Ctx) error { conversation := []models.Conversation{} - err := DB.Model(&models.Conversation{}).Preload("Messages").Find(&conversation).Error + err := DB.Model(&models.Conversation{}).Preload("Messages.Buttons").Find(&conversation).Error if err != nil { return err } diff --git a/go/controller/messages/messages.go b/go/controller/messages/messages.go index a7b0394..a14574c 100644 --- a/go/controller/messages/messages.go +++ b/go/controller/messages/messages.go @@ -226,16 +226,18 @@ type TemplateOptions struct { Code string `json:"code"` // "en_US", "en" Policy string `json:"policy"` // "deterministic" } `json:"language"` // { "code": "en_US" } - Components []struct { - Type string `json:"type"` // "body", "button" - SubType string `json:"subType"` // "quick_reply" (in case of button) - Index string `json:"index"` // "0" (in case of button) - Parameters []struct { - Type string `json:"type"` // "header", "text", "payload" - Payload string `json:"payload"` // "hello_world" (in case of payload) - Text string `json:"text"` // "Hello World" (in case of text) - } `json:"parameters"` - } `json:"components"` + Components []TemplateComponent `json:"components"` +} + +type TemplateComponent struct { + Type string `json:"type"` // "body", "button" + SubType string `json:"subType"` // "quick_reply" (in case of button) + Index string `json:"index"` // "0" (in case of button) + Parameters []struct { + Type string `json:"type"` // "header", "text", "payload" + Payload string `json:"payload"` // "hello_world" (in case of payload) + Text string `json:"text"` // "Hello World" (in case of text) + } `json:"parameters"` } func handleSendTemplateMessage(c *fiber.Ctx, template TemplateOptions, to *phonenumber.ParsedPhoneNumber) error { @@ -251,7 +253,7 @@ func handleSendTemplateMessage(c *fiber.Ctx, template TemplateOptions, to *phone } msgTemplate := models.Template{} - err := DB.Model(&models.Template{}).Where("name = ?", template.Name).First(&msgTemplate).Error + err := DB.Model(&models.Template{}).Where("name = ?", template.Name).Preload("TemplateCustomButtons").First(&msgTemplate).Error if err != nil { msg := "(#132001) Template name does not exist in the translation" details := fmt.Sprintf("template name (%s) does not exist in %s", template.Name, template.Language.Code) @@ -260,10 +262,11 @@ func handleSendTemplateMessage(c *fiber.Ctx, template TemplateOptions, to *phone var requestBodyVariables []string var requestHeaderVariables []string + var buttons []TemplateComponent for idx, component := range template.Components { switch component.Type { case "button": - // TODO + buttons = append(buttons, component) case "body": if requestBodyVariables != nil { return customError(c, "There can be at max 1 body component") @@ -328,7 +331,77 @@ func handleSendTemplateMessage(c *fiber.Ctx, template TemplateOptions, to *phone } } - // FIXME validate request buttons + if len(msgTemplate.TemplateCustomButtons) != len(buttons) { + msg := "(#132000) Number of parameters does not match the expected number of params" + details := fmt.Sprintf( + "number of buttons (%d) does not match the expected number of params (%d)", + len(buttons), + len(msgTemplate.TemplateCustomButtons), + ) + return customError(c, msg, details) + } + + if buttons != nil { + type ButtonPayload struct { + Seen bool + Payload string + } + + buttonsPayload := make([]ButtonPayload, len(buttons)) + + for idx, button := range buttons { + prefix := fmt.Sprintf("template['components'][%d]", idx) + + if button.Index == "" { + return customError(c, fmt.Sprintf("Param %s['index'] is required", prefix)) + } + if button.SubType == "" { + return customError(c, fmt.Sprintf("Param %s['subType'] is required", prefix)) + } + if button.SubType != "quick_reply" { + return customError(c, fmt.Sprintf("Param %s['subType'] must be one of {QUICK_REPLY}", prefix)) + } + + switch len(button.Parameters) { + case 0: + return customError(c, fmt.Sprintf("Param %s['parameters'] is required", prefix)) + case 1: + // continue + default: + return customError(c, fmt.Sprintf("Param %s['parameters'] must have at max 1 element", prefix)) + } + firstParam := button.Parameters[0] + if firstParam.Type == "" { + return customError(c, fmt.Sprintf("Param %s['parameters'][0]['type'] is required", prefix)) + } + if firstParam.Type != "payload" { + return customError(c, fmt.Sprintf("Param %s['parameters'][0]['type'] must be one of {PAYLOAD}", prefix)) + } + if firstParam.Payload == "" { + return customError(c, fmt.Sprintf("Param %s['parameters'][0]['payload'] is required", prefix)) + } + + buttonIndex, err := strconv.Atoi(button.Index) + if err != nil { + return customError(c, fmt.Sprintf("Param %s['index'] must be a number", prefix)) + } + if buttonIndex < 0 || buttonIndex >= len(buttonsPayload) { + return customError(c, fmt.Sprintf("Param %s['index'] must be between 0 and %d", prefix, len(buttonsPayload)-1)) + } + + buttonsPayload[buttonIndex] = ButtonPayload{ + Seen: true, + Payload: firstParam.Payload, + } + } + + for idx, btn := range buttonsPayload { + if !btn.Seen { + return customError(c, fmt.Sprintf("Button with index %d missing", idx)) + } + } + + } message := &models.Message{ WhatsappID: to.WhatsappMessageID, diff --git a/go/lib/webhook/webhook.go b/go/lib/webhook/webhook.go index 675cbdd..4f12ce8 100644 --- a/go/lib/webhook/webhook.go +++ b/go/lib/webhook/webhook.go @@ -52,9 +52,11 @@ func Validate() error { randomSource := rand.New(rand.NewSource(time.Now().Unix())) challenge := random.Hex(randomSource, 16) - url.Query().Add("hub.mode", "subscribe") - url.Query().Add("hub.verivy_token", state.WebhookVerifyToken.Get()) - url.Query().Add("hub.challenge", challenge) + query := url.Query() + query.Add("hub.mode", "subscribe") + query.Add("hub.verify_token", state.WebhookVerifyToken.Get()) + query.Add("hub.challenge", challenge) + url.RawQuery = query.Encode() req, err := http.NewRequest("GET", url.String(), nil) if err != nil { diff --git a/go/models/conversation.go b/go/models/conversation.go index 548b7ff..98d02b3 100644 --- a/go/models/conversation.go +++ b/go/models/conversation.go @@ -16,13 +16,22 @@ type Conversation struct { type Message struct { gorm.Model - ConversationID uint `json:"conversationId"` - WhatsappID string `json:"whatsappID"` - Direction Direction `json:"direction"` - HeaderMessage *string `json:"headerMessage"` - Message string `json:"message"` - FooterMessage *string `json:"footerMessage"` - Timestamp int64 `json:"timestamp"` + ConversationID uint `json:"conversationId"` + WhatsappID string `json:"whatsappID"` + Direction Direction `json:"direction"` + HeaderMessage *string `json:"headerMessage"` + Message string `json:"message"` + FooterMessage *string `json:"footerMessage"` + Timestamp int64 `json:"timestamp"` + Buttons []MessageButton `json:"buttons"` +} + +type MessageButton struct { + gorm.Model + ConversationID uint `json:"conversationId"` + MessageID uint `json:"messageId"` + Text string `json:"text"` + Payload *string `json:"payload"` } type Direction string diff --git a/main.go b/main.go index d88f5e6..0fa32ce 100644 --- a/main.go +++ b/main.go @@ -128,6 +128,7 @@ func main() { &models.Message{}, &models.Template{}, &models.TemplateCustomButton{}, + &models.MessageButton{}, ) templatesCount := int64(0) @@ -149,7 +150,9 @@ func main() { go func() { err := webhook.Validate() - if err != nil { + if err == nil { + fmt.Println("Webhook validated successfully") + } else { fmt.Println("Failed to validate webhook:", err.Error()) } }() diff --git a/src/components/conversations/conversations.tsx b/src/components/conversations/conversations.tsx index cc492d4..53c2e41 100644 --- a/src/components/conversations/conversations.tsx +++ b/src/components/conversations/conversations.tsx @@ -3,7 +3,6 @@ import { AlertDialogAction, AlertDialogCancel, AlertDialogContent, - AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, @@ -182,7 +181,6 @@ function NewChatDialog({ newConversation, open, close }: NewChatDialogProps) { Create a new conversation - + setState((s) => { + let text = "Button text" + if (s.templateCustomButtons.length > 0) { + text += " " + (s.templateCustomButtons.length + 1) + } + + s.templateCustomButtons.push({ + ...emptyDBModel(), + templateID: s.ID, + text, + }) + + return { ...s } + }) + + const setButtonText = (idx: number, text: string) => + setState((s) => { + s.templateCustomButtons[idx].text = text + return { ...s } + }) + + const removeButton = (idx: number) => + setState((s) => { + s.templateCustomButtons.splice(idx, 1) + return { ...s } + }) + + const intermediateClose = () => { + close() + setState(emptyTemplate()) + } + return ( - close()}> + intermediateClose()}> Create a new conversation - setState((s) => ({ ...s, header: e.target.value }))} + onChange={(e) => setState((s) => ({ ...s, name: e.target.value }))} name="name" id="name" placeholder="hello_world" @@ -200,12 +233,13 @@ function NewTemplateDialog({ placeholder="Header" /> - setState((s) => ({ ...s, body: e.target.value }))} name="body" id="body" placeholder="Hello world!" + h-30 /> + + {state.templateCustomButtons.length ? ( + + ) : undefined} + {state.templateCustomButtons.map((btn, idx) => ( +
+
+ +
+
+ + setButtonText(idx, e.target.value)} + name={"button-" + idx} + id={"button-" + idx} + placeholder="Hello world!" + /> +
+
+ ))} +
+ +
Cancel - Start + Create
diff --git a/src/components/ui/textarea.tsx b/src/components/ui/textarea.tsx new file mode 100644 index 0000000..d1258e4 --- /dev/null +++ b/src/components/ui/textarea.tsx @@ -0,0 +1,24 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +export interface TextareaProps + extends React.TextareaHTMLAttributes {} + +const Textarea = React.forwardRef( + ({ className, ...props }, ref) => { + return ( +