Skip to content

Commit

Permalink
Fixes and improvements
Browse files Browse the repository at this point in the history
  • Loading branch information
mjarkk committed Apr 26, 2024
1 parent 0f854a0 commit 732f0ff
Show file tree
Hide file tree
Showing 8 changed files with 203 additions and 32 deletions.
2 changes: 1 addition & 1 deletion go/controller/conversations/conversations.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
99 changes: 86 additions & 13 deletions go/controller/messages/messages.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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)
Expand All @@ -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")
Expand Down Expand Up @@ -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,
Expand Down
8 changes: 5 additions & 3 deletions go/lib/webhook/webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
23 changes: 16 additions & 7 deletions go/models/conversation.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 4 additions & 1 deletion main.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ func main() {
&models.Message{},
&models.Template{},
&models.TemplateCustomButton{},
&models.MessageButton{},
)

templatesCount := int64(0)
Expand All @@ -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())
}
}()
Expand Down
2 changes: 0 additions & 2 deletions src/components/conversations/conversations.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import {
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
Expand Down Expand Up @@ -182,7 +181,6 @@ function NewChatDialog({ newConversation, open, close }: NewChatDialogProps) {
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Create a new conversation</AlertDialogTitle>
<AlertDialogDescription></AlertDialogDescription>
</AlertDialogHeader>
<Label htmlFor="phoneNumber">Source phone number</Label>
<Input
Expand Down
72 changes: 67 additions & 5 deletions src/components/templates/templates.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { useEffect, useState, Fragment } from "react"
import { DBModel, emptyDBModel } from "@/lib/types"
import { TrashIcon } from "@radix-ui/react-icons"
import { OpenCloseButton } from "../openCloseButton"
import { Textarea } from "@/components/ui/textarea"

interface Template extends DBModel {
name: string
Expand Down Expand Up @@ -176,17 +177,49 @@ function NewTemplateDialog({
setState(emptyTemplate())
}

const addButton = () =>
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 (
<AlertDialog open={open} onOpenChange={() => close()}>
<AlertDialog open={open} onOpenChange={() => intermediateClose()}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Create a new conversation</AlertDialogTitle>
<AlertDialogDescription></AlertDialogDescription>
</AlertDialogHeader>
<Label htmlFor="header">Name</Label>
<Input
value={state.name}
onChange={(e) => setState((s) => ({ ...s, header: e.target.value }))}
onChange={(e) => setState((s) => ({ ...s, name: e.target.value }))}
name="name"
id="name"
placeholder="hello_world"
Expand All @@ -200,12 +233,13 @@ function NewTemplateDialog({
placeholder="Header"
/>
<Label htmlFor="body">Body</Label>
<Input
<Textarea
value={state.body}
onChange={(e) => setState((s) => ({ ...s, body: e.target.value }))}
name="body"
id="body"
placeholder="Hello world!"
h-30
/>
<Label htmlFor="footer">Footer</Label>
<Input
Expand All @@ -215,10 +249,38 @@ function NewTemplateDialog({
id="footer"
placeholder="Hello world!"
/>

{state.templateCustomButtons.length ? (
<Label htmlFor="footer">Buttons</Label>
) : undefined}
{state.templateCustomButtons.map((btn, idx) => (
<div key={idx} flex w-full items-center gap-4>
<div>
<Button onClick={() => removeButton(idx)} variant="ghost">
<TrashIcon />
</Button>
</div>
<div flex-1>
<Label htmlFor="footer">Button #{idx + 1}</Label>
<Input
value={btn.text}
onChange={(e) => setButtonText(idx, e.target.value)}
name={"button-" + idx}
id={"button-" + idx}
placeholder="Hello world!"
/>
</div>
</div>
))}
<div>
<Button variant="secondary" onClick={addButton}>
New button
</Button>
</div>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction onClick={createConversation}>
Start
Create
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
Expand Down
24 changes: 24 additions & 0 deletions src/components/ui/textarea.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import * as React from "react"

import { cn } from "@/lib/utils"

export interface TextareaProps
extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}

const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
({ className, ...props }, ref) => {
return (
<textarea
className={cn(
"flex min-h-[60px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
className
)}
ref={ref}
{...props}
/>
)
}
)
Textarea.displayName = "Textarea"

export { Textarea }

0 comments on commit 732f0ff

Please sign in to comment.