Skip to content

Commit

Permalink
Merge pull request #45 from weni-ai/msg-catalog
Browse files Browse the repository at this point in the history
Msg catalog implementations in msg and handler
  • Loading branch information
Robi9 authored Nov 3, 2023
2 parents 93db237 + b2aff15 commit d632ce5
Show file tree
Hide file tree
Showing 5 changed files with 354 additions and 18 deletions.
70 changes: 70 additions & 0 deletions backends/rapidpro/msg.go
Original file line number Diff line number Diff line change
Expand Up @@ -581,6 +581,13 @@ type DBMsg struct {
alreadyWritten bool
quickReplies []string
textLanguage string

products []string
header string
body string
footer string
action string
sendAction bool
}

func (m *DBMsg) ID() courier.MsgID { return m.ID_ }
Expand Down Expand Up @@ -704,3 +711,66 @@ func GetMsgByUUID(b *backend, uuid string) (*DBMsg, error) {
}

func (m *DBMsg) Status() courier.MsgStatusValue { return m.Status_ }

func (m *DBMsg) Products() []string {
if m.products != nil {
return m.products
}

if m.Metadata_ == nil {
return nil
}

m.products = []string{}
jsonparser.ArrayEach(
m.Metadata_,
func(value []byte, dataType jsonparser.ValueType, offset int, err error) {
m.products = append(m.products, string(value))
},
"products")
return m.products
}

func (m *DBMsg) Header() string {
if m.Metadata_ == nil {
return ""
}
header, _, _, _ := jsonparser.Get(m.Metadata_, "header")
return string(header)
}

func (m *DBMsg) Body() string {
if m.Metadata_ == nil {
return ""
}
body, _, _, _ := jsonparser.Get(m.Metadata_, "body")
return string(body)
}

func (m *DBMsg) Footer() string {
if m.Metadata_ == nil {
return ""
}
footer, _, _, _ := jsonparser.Get(m.Metadata_, "footer")
return string(footer)
}

func (m *DBMsg) Action() string {
if m.Metadata_ == nil {
return ""
}
action, _, _, _ := jsonparser.Get(m.Metadata_, "action")
return string(action)
}

func (m *DBMsg) SendCatalog() bool {
if m.Metadata_ == nil {
return false
}
byteValue, _, _, _ := jsonparser.Get(m.Metadata_, "send_catalog")
sendCatalog, err := strconv.ParseBool(string(byteValue))
if err != nil {
return false
}
return sendCatalog
}
201 changes: 184 additions & 17 deletions handlers/facebookapp/facebookapp.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,13 @@ const (

var failedMediaCache *cache.Cache

const (
InteractiveProductSingleType = "product"
InteractiveProductListType = "product_list"
InteractiveProductCatalogType = "catalog_product"
InteractiveProductCatalogMessageType = "catalog_message"
)

func newHandler(channelType courier.ChannelType, name string, useUUIDRoutes bool) courier.ChannelHandler {
return &handler{handlers.NewBaseHandlerWithParams(channelType, name, useUUIDRoutes)}
}
Expand Down Expand Up @@ -1221,8 +1228,9 @@ type wacMTMedia struct {
}

type wacMTSection struct {
Title string `json:"title,omitempty"`
Rows []wacMTSectionRow `json:"rows" validate:"required"`
Title string `json:"title,omitempty"`
Rows []wacMTSectionRow `json:"rows,omitempty"`
ProductItems []wacMTProductItem `json:"product_items,omitempty"`
}

type wacMTSectionRow struct {
Expand Down Expand Up @@ -1286,9 +1294,12 @@ type wacInteractive struct {
Text string `json:"text"`
} `json:"footer,omitempty"`
Action *struct {
Button string `json:"button,omitempty"`
Sections []wacMTSection `json:"sections,omitempty"`
Buttons []wacMTButton `json:"buttons,omitempty"`
Button string `json:"button,omitempty"`
Sections []wacMTSection `json:"sections,omitempty"`
Buttons []wacMTButton `json:"buttons,omitempty"`
CatalogID string `json:"catalog_id,omitempty"`
ProductRetailerID string `json:"product_retailer_id,omitempty"`
Name string `json:"name,omitempty"`
} `json:"action,omitempty"`
}

Expand Down Expand Up @@ -1321,6 +1332,14 @@ type wacMTResponse struct {
} `json:"contacts,omitempty"`
}

type wacMTSectionProduct struct {
Title string `json:"title,omitempty"`
}

type wacMTProductItem struct {
ProductRetailerID string `json:"product_retailer_id" validate:"required"`
}

func (h *handler) sendCloudAPIWhatsappMsg(ctx context.Context, msg courier.Msg) (courier.MsgStatus, error) {
// can't do anything without an access token
accessToken := h.Server().Config().WhatsappAdminSystemUserToken
Expand Down Expand Up @@ -1446,9 +1465,12 @@ func (h *handler) sendCloudAPIWhatsappMsg(ctx context.Context, msg courier.Msg)
btns[i].Reply.Title = text
}
interactive.Action = &struct {
Button string "json:\"button,omitempty\""
Sections []wacMTSection "json:\"sections,omitempty\""
Buttons []wacMTButton "json:\"buttons,omitempty\""
Button string "json:\"button,omitempty\""
Sections []wacMTSection "json:\"sections,omitempty\""
Buttons []wacMTButton "json:\"buttons,omitempty\""
CatalogID string "json:\"catalog_id,omitempty\""
ProductRetailerID string "json:\"product_retailer_id,omitempty\""
Name string "json:\"name,omitempty\""
}{Buttons: btns}
payload.Interactive = &interactive
} else if len(qrs) <= 10 {
Expand All @@ -1475,9 +1497,12 @@ func (h *handler) sendCloudAPIWhatsappMsg(ctx context.Context, msg courier.Msg)
}

interactive.Action = &struct {
Button string "json:\"button,omitempty\""
Sections []wacMTSection "json:\"sections,omitempty\""
Buttons []wacMTButton "json:\"buttons,omitempty\""
Button string "json:\"button,omitempty\""
Sections []wacMTSection "json:\"sections,omitempty\""
Buttons []wacMTButton "json:\"buttons,omitempty\""
CatalogID string "json:\"catalog_id,omitempty\""
ProductRetailerID string "json:\"product_retailer_id,omitempty\""
Name string "json:\"name,omitempty\""
}{Button: "Menu", Sections: []wacMTSection{
section,
}}
Expand Down Expand Up @@ -1648,9 +1673,12 @@ func (h *handler) sendCloudAPIWhatsappMsg(ctx context.Context, msg courier.Msg)
btns[i].Reply.Title = text
}
interactive.Action = &struct {
Button string "json:\"button,omitempty\""
Sections []wacMTSection "json:\"sections,omitempty\""
Buttons []wacMTButton "json:\"buttons,omitempty\""
Button string "json:\"button,omitempty\""
Sections []wacMTSection "json:\"sections,omitempty\""
Buttons []wacMTButton "json:\"buttons,omitempty\""
CatalogID string "json:\"catalog_id,omitempty\""
ProductRetailerID string "json:\"product_retailer_id,omitempty\""
Name string "json:\"name,omitempty\""
}{Buttons: btns}
payload.Interactive = &interactive

Expand Down Expand Up @@ -1678,9 +1706,12 @@ func (h *handler) sendCloudAPIWhatsappMsg(ctx context.Context, msg courier.Msg)
}

interactive.Action = &struct {
Button string "json:\"button,omitempty\""
Sections []wacMTSection "json:\"sections,omitempty\""
Buttons []wacMTButton "json:\"buttons,omitempty\""
Button string "json:\"button,omitempty\""
Sections []wacMTSection "json:\"sections,omitempty\""
Buttons []wacMTButton "json:\"buttons,omitempty\""
CatalogID string "json:\"catalog_id,omitempty\""
ProductRetailerID string "json:\"product_retailer_id,omitempty\""
Name string "json:\"name,omitempty\""
}{Button: "Menu", Sections: []wacMTSection{
section,
}}
Expand Down Expand Up @@ -1730,6 +1761,142 @@ func (h *handler) sendCloudAPIWhatsappMsg(ctx context.Context, msg courier.Msg)
}

}

if len(msg.Products()) > 0 || msg.SendCatalog() {

catalogID := msg.Channel().StringConfigForKey("catalog_id", "")
if catalogID == "" {
return status, errors.New("Catalog ID not found in channel config")
}

payload := wacMTPayload{MessagingProduct: "whatsapp", RecipientType: "individual", To: msg.URN().Path()}

payload.Type = "interactive"

products := msg.Products()

var interactiveType string
if msg.SendCatalog() {
interactiveType = InteractiveProductCatalogMessageType
} else if len(products) > 1 {
interactiveType = InteractiveProductListType
} else {
interactiveType = InteractiveProductSingleType
}

interactive := wacInteractive{
Type: interactiveType,
}

interactive.Body = struct {
Text string `json:"text"`
}{
Text: msg.Body(),
}

if msg.Header() != "" && len(products) > 1 && !msg.SendCatalog() {
interactive.Header = &struct {
Type string `json:"type"`
Text string `json:"text,omitempty"`
Video wacMTMedia `json:"video,omitempty"`
Image wacMTMedia `json:"image,omitempty"`
Document wacMTMedia `json:"document,omitempty"`
}{
Type: "text",
Text: msg.Header(),
}
}

if msg.Footer() != "" {
interactive.Footer = &struct {
Text string "json:\"text\""
}{
Text: msg.Footer(),
}
}

if msg.SendCatalog() {
interactive.Action = &struct {
Button string `json:"button,omitempty"`
Sections []wacMTSection `json:"sections,omitempty"`
Buttons []wacMTButton `json:"buttons,omitempty"`
CatalogID string `json:"catalog_id,omitempty"`
ProductRetailerID string `json:"product_retailer_id,omitempty"`
Name string `json:"name,omitempty"`
}{
Name: "catalog_message",
}
payload.Interactive = &interactive
status, _, err := requestWAC(payload, accessToken, msg, status, wacPhoneURL, true)
if err != nil {
return status, err
}
} else if len(products) > 0 {
if len(products) > 1 {

blockSize := 30
iterations := (len(products) + blockSize - 1) / blockSize

for i := 0; i < iterations; i++ {
start := i * blockSize
end := (i + 1) * blockSize
if end > len(products) {
end = len(products)
}

sproducts := []wacMTProductItem{}
for _, p := range products[start:end] {
sproducts = append(sproducts, wacMTProductItem{
ProductRetailerID: p,
})
}

sections := []wacMTSection{{
Title: "items",
ProductItems: sproducts,
}}

interactive.Action = &struct {
Button string `json:"button,omitempty"`
Sections []wacMTSection `json:"sections,omitempty"`
Buttons []wacMTButton `json:"buttons,omitempty"`
CatalogID string `json:"catalog_id,omitempty"`
ProductRetailerID string `json:"product_retailer_id,omitempty"`
Name string `json:"name,omitempty"`
}{
CatalogID: catalogID,
Sections: sections,
Name: msg.Action(),
}

payload.Interactive = &interactive
status, _, err := requestWAC(payload, accessToken, msg, status, wacPhoneURL, true)
if err != nil {
return status, err
}
}
} else {
interactive.Action = &struct {
Button string `json:"button,omitempty"`
Sections []wacMTSection `json:"sections,omitempty"`
Buttons []wacMTButton `json:"buttons,omitempty"`
CatalogID string `json:"catalog_id,omitempty"`
ProductRetailerID string `json:"product_retailer_id,omitempty"`
Name string `json:"name,omitempty"`
}{
CatalogID: catalogID,
Name: msg.Action(),
ProductRetailerID: products[0],
}
payload.Interactive = &interactive
status, _, err := requestWAC(payload, accessToken, msg, status, wacPhoneURL, true)
if err != nil {
return status, err
}
}
}
}

return status, nil
}

Expand Down
23 changes: 22 additions & 1 deletion handlers/facebookapp/facebookapp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -817,6 +817,27 @@ var SendTestCasesWAC = []ChannelSendTestCase{
RequestBody: `{"messaging_product":"whatsapp","recipient_type":"individual","to":"5511987654321","type":"image","image":{"link":"https://foo.bar/image.jpg","caption":"Simple Message"}}`,
SendPrep: setSendURL,
NewURN: "whatsapp:551187654321"},
{Label: "Catalog Message Send 1 product",
Metadata: json.RawMessage(`{"body":"Catalog Body Msg", "products": ["p90duct-23t41l32-1D"], "action": "View Products"}`),
Text: "Catalog Msg", URN: "whatsapp:250788123123",
Status: "W", ExternalID: "157b5e14568e8",
ResponseBody: `{ "messages": [{"id": "157b5e14568e8"}] }`, ResponseStatus: 201,
RequestBody: `{"messaging_product":"whatsapp","recipient_type":"individual","to":"250788123123","type":"interactive","interactive":{"type":"product","body":{"text":"Catalog Body Msg"},"action":{"catalog_id":"c4t4l0g-1D","product_retailer_id":"p90duct-23t41l32-1D","name":"View Products"}}}`,
SendPrep: setSendURL},
{Label: "Catalog Message Send 2 products",
Metadata: json.RawMessage(`{"body":"Catalog Body Msg", "products": ["p1","p2"], "action": "View Products"}`),
Text: "Catalog Msg", URN: "whatsapp:250788123123",
Status: "W", ExternalID: "157b5e14568e8",
ResponseBody: `{ "messages": [{"id": "157b5e14568e8"}] }`, ResponseStatus: 201,
RequestBody: `{"messaging_product":"whatsapp","recipient_type":"individual","to":"250788123123","type":"interactive","interactive":{"type":"product_list","body":{"text":"Catalog Body Msg"},"action":{"sections":[{"title":"items","product_items":[{"product_retailer_id":"p1"},{"product_retailer_id":"p2"}]}],"catalog_id":"c4t4l0g-1D","name":"View Products"}}}`,
SendPrep: setSendURL},
{Label: "Catalog Message Send 30+ products",
Metadata: json.RawMessage(`{"body":"Catalog Body Msg", "products": ["p1", "p2", "p3", "p4", "p5", "p6", "p7", "p8", "p9", "p10", "p11", "p12", "p13", "p14", "p15", "p16", "p17", "p18", "p19", "p20", "p21", "p22", "p23", "p24", "p25", "p26", "p27", "p28", "p29", "p30", "p31"], "action": "View Products"}`),
Text: "Catalog Msg", URN: "whatsapp:250788123123",
Status: "W", ExternalID: "157b5e14568e8",
ResponseBody: `{ "messages": [{"id": "157b5e14568e8"}] }`, ResponseStatus: 201,
RequestBody: `{"messaging_product":"whatsapp","recipient_type":"individual","to":"250788123123","type":"interactive","interactive":{"type":"product_list","body":{"text":"Catalog Body Msg"},"action":{"sections":[{"title":"items","product_items":[{"product_retailer_id":"p31"}]}],"catalog_id":"c4t4l0g-1D","name":"View Products"}}}`,
SendPrep: setSendURL},
}

func TestSending(t *testing.T) {
Expand All @@ -826,7 +847,7 @@ func TestSending(t *testing.T) {
maxMsgLengthWAC = 100
var ChannelFBA = courier.NewMockChannel("8eb23e93-5ecb-45ba-b726-3b064e0c56ab", "FBA", "12345", "", map[string]interface{}{courier.ConfigAuthToken: "a123"})
var ChannelIG = courier.NewMockChannel("8eb23e93-5ecb-45ba-b726-3b064e0c56ab", "IG", "12345", "", map[string]interface{}{courier.ConfigAuthToken: "a123"})
var ChannelWAC = courier.NewMockChannel("8eb23e93-5ecb-45ba-b726-3b064e0c56ab", "WAC", "12345_ID", "", map[string]interface{}{courier.ConfigAuthToken: "a123"})
var ChannelWAC = courier.NewMockChannel("8eb23e93-5ecb-45ba-b726-3b064e0c56ab", "WAC", "12345_ID", "", map[string]interface{}{courier.ConfigAuthToken: "a123", "catalog_id": "c4t4l0g-1D"})
RunChannelSendTestCases(t, ChannelFBA, newHandler("FBA", "Facebook", false), SendTestCasesFBA, nil)
RunChannelSendTestCases(t, ChannelIG, newHandler("IG", "Instagram", false), SendTestCasesIG, nil)
RunChannelSendTestCases(t, ChannelWAC, newHandler("WAC", "Cloud API WhatsApp", false), SendTestCasesWAC, nil)
Expand Down
7 changes: 7 additions & 0 deletions msg.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,13 @@ type Msg interface {
TextLanguage() string

Status() MsgStatusValue

Products() []string
Header() string
Body() string
Footer() string
Action() string
SendCatalog() bool
}

type RunEvent struct {
Expand Down
Loading

0 comments on commit d632ce5

Please sign in to comment.