From ceb78b951e30340f44449602514e191f93c9ac4e Mon Sep 17 00:00:00 2001 From: Saurabh Newatiya Plivo <107537111+saurabhnewatiya-plivo@users.noreply.github.com> Date: Mon, 20 May 2024 13:08:44 +0530 Subject: [PATCH] Add support for templated & non-templated location WA messages (#205) * SMS-6815: Adding support for templated and non-templated location messages * minor fix * minor fix * updating release date * addressing review comment * Updating release date --- CHANGELOG.md | 5 + README.md | 440 ++++++++++++++++++++++++++++++++++++++++++++++++++ baseclient.go | 2 +- messages.go | 17 +- utils.go | 10 +- utils_test.go | 229 +++++++++++++++++++++++++- 6 files changed, 692 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b6d76e..3042c29 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Change Log +## [7.49.0](https://github.com/plivo/plivo-go/tree/v7.49.0) (2024-05-20) +**Feature - Adding support for location whatsapp messages** +- Added new param 'location' to [send message API](https://www.plivo.com/docs/sms/api/message#send-a-message) to support location 'whatsapp' messages +- Added new param 'location' in templates to support location based templated messages + ## [7.48.0](https://github.com/plivo/plivo-go/tree/v7.48.0) (2024-05-07) **Feature - Adding support for interactive whatsapp messages** - Added new param 'interactive' to [send message API](https://www.plivo.com/docs/sms/api/message#send-a-message) to support interactive 'whatsapp' messages diff --git a/README.md b/README.md index 47f2bcd..a63beb9 100644 --- a/README.md +++ b/README.md @@ -218,6 +218,446 @@ func testPhloRunWithParams() { } ``` + +## WhatsApp Messaging +Plivo's WhatsApp API allows you to send different types of messages over WhatsApp, including templated messages, free form messages and interactive messages. Below are some examples on how to use the Plivo Go SDK to send these types of messages. + +### Templated Messages +Templated messages are a crucial to your WhatsApp messaging experience, as businesses can only initiate WhatsApp conversation with their customers using templated messages. + +WhatsApp templates support 4 components: `header` , `body`, `footer` and `button`. At the point of sending messages, the template object you see in the code acts as a way to pass the dynamic values within these components. `header` can accomodate `text` or `media` (images, video, documents) content. `body` can accomodate text content. `button` can support dynamic values in a `url` button or to specify a developer-defined payload which will be returned when the WhatsApp user clicks on the `quick_reply` button. `footer` cannot have any dynamic variables. + +Example: +```go +package main + +import ( + "fmt" + "github.com/plivo/plivo-go/v7" +) + +func main() { + client, err := plivo.NewClient("", "", &plivo.ClientOptions{}) + if err != nil { + fmt.Println("Error:", err) + return + } + + // Create a WhatsApp template + template, err := plivo.CreateWhatsappTemplate(`{ + "name": "sample_purchase_feedback", + "language": "en_US", + "components": [ + { + "type": "header", + "parameters": [ + { + "type": "media", + "media": "https://xyz.com/img.jpg" + } + ] + }, + { + "type": "body", + "parameters": [ + { + "type": "text", + "text": "Water Purifier" + } + ] + } + ] + }`) + if err != nil { + fmt.Println("Error creating template:", err) + return + } + + // Send a templated message + response, err := client.Messages.Create(plivo.MessageCreateParams{ + Src: "source_number", + Dst: "destination_number", + Type: "whatsapp", + Template: &template, + }) + if err != nil { + fmt.Println("Error sending message:", err) + return + } + + fmt.Printf("Response: %#v\n", response) +} +``` + +### Free Form Messages +Non-templated or Free Form WhatsApp messages can be sent as a reply to a user-initiated conversation (Service conversation) or if there is an existing ongoing conversation created previously by sending a templated WhatsApp message. + +#### Free Form Text Message +Example: +```go +package main + +import ( + "fmt" + "github.com/plivo/plivo-go/v7" +) + +func main() { + client, err := plivo.NewClient("", "", &plivo.ClientOptions{}) + if err != nil { + fmt.Println("Error:", err) + return + } + + // Send a free form message + response, err := client.Messages.Create(plivo.MessageCreateParams{ + Src: "source_number", + Dst: "destination_number", + Text: "Hello! How can I help you today?", + Type: "whatsapp", + }) + if err != nil { + fmt.Println("Error sending message:", err) + return + } + + fmt.Printf("Response: %#v\n", response) +} +``` + +#### Free Form Media Message +Example: +```go +package main + +import ( + "fmt" + "github.com/plivo/plivo-go/v7" +) + +func main() { + client, err := plivo.NewClient("", "", &plivo.ClientOptions{}) + if err != nil { + fmt.Print("Error", err.Error()) + return + } + response, err := client.Messages.Create(plivo.MessageCreateParams{ + Src:"source_number", + Dst:"destination_number", + Type:"whatsapp", + Text:"Hello, this is sample text", + MediaUrls:[]string{"https://sample-videos.com/img/Sample-png-image-1mb.png"}, + URL: "https://foo.com/whatsapp_status/", + }) + if err != nil { + fmt.Print("Error sending message:", err.Error()) + return + } + fmt.Printf("Response: %#v\n", response) +} +``` + +### Interactive Messages +This guide shows how to send non-templated interactive messages to recipients using Plivo’s APIs. + +#### Quick Reply Buttons +Quick reply buttons allow customers to quickly respond to your message with predefined options. + +Example: +```go +package main + +import ( + "fmt" + "github.com/plivo/plivo-go/v7" +) + +func main() { + client, err := plivo.NewClient("", "", &plivo.ClientOptions{}) + if err != nil { + fmt.Println("Error:", err) + return + } + + // Create quick reply buttons + interactive, err := plivo.CreateWhatsappInteractive(`{ + "type": "button", + "body": { + "text": "Would you like to proceed?" + }, + "action": { + "buttons": [ + { + "title": "Yes", + "id": "yes" + }, + { + "title": "No", + "id": "no" + } + ] + } + }`) + if err != nil { + fmt.Println("Error creating interactive buttons:", err) + return + } + + // Send interactive message with quick reply buttons + response, err := client.Messages.Create(plivo.MessageCreateParams{ + Src: "source_number", + Dst: "destination_number", + Type: "whatsapp", + Interactive: &interactive, + }) + if err != nil { + fmt.Println("Error sending interactive message:", err) + return + } + + fmt.Printf("Response: %#v\n", response) +} +``` + +#### Interactive Lists +Interactive lists allow you to present customers with a list of options. + +Example: +```go +package main + +import ( + "fmt" + "github.com/plivo/plivo-go/v7" +) + +func main() { + client, err := plivo.NewClient("", "", &plivo.ClientOptions{}) + if err != nil { + fmt.Println("Error:", err) + return + } + + // Create an interactive list + interactive, err := plivo.CreateWhatsappInteractive(`{ + "type": "list", + "header": { + "type": "text", + "text": "Select an option" + }, + "body": { + "text": "Choose from the following options:" + }, + "action": { + "sections": [ + { + "title": "Options", + "rows": [ + { + "id": "option1", + "title": "Option 1", + "description": "Description of option 1" + }, + { + "id": "option2", + "title": "Option 2", + "description": "Description of option 2" + } + ] + } + ] + } + }`) + if err != nil { + fmt.Println("Error creating interactive list:", err) + return + } + + // Send interactive message with list + response, err := client.Messages.Create(plivo.MessageCreateParams{ + Src: "source_number", + Dst: "destination_number", + Type: "whatsapp", + Interactive: &interactive, + }) + if err != nil { + fmt.Println("Error sending interactive message:", err) + return + } + + fmt.Printf("Response: %#v\n", response) +} +``` + +#### Interactive CTA URLs +CTA URL messages allow you to send links and call-to-action buttons. + +Example: +```go +package main + +import ( + "fmt" + "github.com/plivo/plivo-go/v7" +) + +func main() { + client, err := plivo.NewClient("", "", &plivo.ClientOptions{}) + if err != nil { + fmt.Println("Error:", err) + return + } + + // Create a CTA URL message + interactive, err := plivo.CreateWhatsappInteractive(`{ + "type": "cta_url", + "header": { + "type": "media", + "media": "https://example.com/image.jpg" + }, + "body": { + "text": "Check out this link!" + }, + "footer": { + "text": "Footer text" + }, + "action": { + "buttons": [ + { + "title": "Visit Website", + "url": "https://example.com" + } + ] + } + }`) + if err != nil { + fmt.Println("Error creating CTA URL:", err) + return + } + + // Send interactive message with CTA URL + response, err := client.Messages.Create(plivo.MessageCreateParams{ + Src: "source_number", + Dst: "destination_number", + Type: "whatsapp", + Interactive: &interactive, + }) + if err != nil { + fmt.Println("Error sending interactive message:", err) + return + } + + fmt.Printf("Response: %#v\n", response) +} +``` + +### Location Messages +This guide shows how to send templated and non-templated location messages to recipients using Plivo’s APIs. + +#### Templated Location Messages +Example: +```go +package main + +import ( + "fmt" + "github.com/plivo/plivo-go/v7" +) + +func main() { + client, err := plivo.NewClient("", "", &plivo.ClientOptions{}) + if err != nil { + fmt.Println("Error:", err) + return + } + + // Create a WhatsApp template + template, err := plivo.CreateWhatsappTemplate(`{ + "name": "plivo_order_pickup", + "language": "en_US", + "components": [ + { + "type": "header", + "parameters": [ + { + "type": "location", + "location": { + "latitude": "37.483307", + "longitude": "122.148981", + "name": "Pablo Morales", + "address": "1 Hacker Way, Menlo Park, CA 94025" + } + } + ] + } + ] + }`) + if err != nil { + fmt.Println("Error creating template:", err) + return + } + + // Send a templated message + response, err := client.Messages.Create(plivo.MessageCreateParams{ + Src: "source_number", + Dst: "destination_number", + Type: "whatsapp", + Template: &template, + }) + if err != nil { + fmt.Println("Error sending message:", err) + return + } + + fmt.Printf("Response: %#v\n", response) +} +``` + +#### Non-Templated Location Messages +Example: +```go +package main + +import ( + "fmt" + "github.com/plivo/plivo-go/v7" +) + +func main() { + client, err := plivo.NewClient("", "", &plivo.ClientOptions{}) + if err != nil { + fmt.Println("Error:", err) + return + } + + // Create a WhatsApp location object + location, err := plivo.CreateWhatsappLocation(`{ + "latitude": "37.483307", + "longitude": "122.148981", + "name": "Pablo Morales", + "address": "1 Hacker Way, Menlo Park, CA 94025" + }`) + if err != nil { + fmt.Println("Error creating location:", err) + return + } + + // Send a templated message + response, err := client.Messages.Create(plivo.MessageCreateParams{ + Src: "source_number", + Dst: "destination_number", + Type: "whatsapp", + Location: &location, + }) + if err != nil { + fmt.Println("Error sending message:", err) + return + } + + fmt.Printf("Response: %#v\n", response) +} +``` + ### More examples Refer to the [Plivo API Reference](https://www.plivo.com/docs/sms/api/overview/) for more examples. diff --git a/baseclient.go b/baseclient.go index 8111ea3..c321ddf 100644 --- a/baseclient.go +++ b/baseclient.go @@ -13,7 +13,7 @@ import ( "github.com/google/go-querystring/query" ) -const sdkVersion = "7.48.0" +const sdkVersion = "7.49.0" const lookupBaseUrl = "lookup.plivo.com" diff --git a/messages.go b/messages.go index b473f27..94a85b9 100644 --- a/messages.go +++ b/messages.go @@ -27,6 +27,7 @@ type MessageCreateParams struct { MessageExpiry int `json:"message_expiry,omitempty" url:"message_expiry,omitempty"` Template *Template `json:"template,omitempty" url:"template,omitempty"` Interactive *Interactive `json:"interactive,omitempty" url:"interactive,omitempty"` + Location *Location `json:"location,omitempty" url:"location,omitempty"` DLTEntityID string `json:"dlt_entity_id,omitempty" url:"dlt_entity_id,omitempty"` DLTTemplateID string `json:"dlt_template_id,omitempty" url:"dlt_template_id,omitempty"` DLTTemplateCategory string `json:"dlt_template_category,omitempty" url:"dlt_template_category,omitempty"` @@ -140,16 +141,24 @@ type Parameter struct { Payload string `mapstructure:"payload" json:"payload,omitempty"` Currency *Currency `mapstructure:"currency" json:"currency,omitempty"` DateTime *DateTime `mapstructure:"date_time" json:"date_time,omitempty"` + Location *Location `mapstructure:"location" json:"location,omitempty"` +} + +type Location struct { + Longitude string `mapstructure:"longitude" json:"longitude,omitempty"` + Latitude string `mapstructure:"latitude" json:"latitude,omitempty"` + Name string `mapstructure:"name" json:"name,omitempty"` + Address string `mapstructure:"address" json:"address,omitempty"` } type Currency struct { - FallbackValue string `mapstructure:"fallback_value" json:"fallback_value" validate:"required"` - CurrencyCode string `mapstructure:"currency_code" json:"currency_code" validate:"required"` - Amount1000 int `mapstructure:"amount_1000" json:"amount_1000" validate:"required"` + FallbackValue string `mapstructure:"fallback_value" json:"fallback_value"` + CurrencyCode string `mapstructure:"currency_code" json:"currency_code"` + Amount1000 int `mapstructure:"amount_1000" json:"amount_1000"` } type DateTime struct { - FallbackValue string `mapstructure:"fallback_value" json:"fallback_value" validate:"required"` + FallbackValue string `mapstructure:"fallback_value" json:"fallback_value"` } type Interactive struct { diff --git a/utils.go b/utils.go index 441610d..e5c5279 100644 --- a/utils.go +++ b/utils.go @@ -180,9 +180,16 @@ func CreateWhatsappInteractive(interactiveData string) (interactive Interactive, } validate := validator.New() err = validate.Struct(interactive) + return +} + +func CreateWhatsappLocation(locationData string) (location Location, err error) { + err = json.Unmarshal([]byte(locationData), &location) if err != nil { return } + validate := validator.New() + err = validate.Struct(location) return } @@ -192,9 +199,6 @@ func CreateWhatsappTemplate(templateData string) (template Template, err error) return } err = validateWhatsappTemplate(template) - if err != nil { - return - } return } diff --git a/utils_test.go b/utils_test.go index ac7bc7c..0a2a843 100644 --- a/utils_test.go +++ b/utils_test.go @@ -1,7 +1,8 @@ package plivo -import "testing" import ( + "testing" + "github.com/stretchr/testify/assert" ) @@ -175,7 +176,7 @@ func TestValidateSignatureV3Pass4(t *testing.T) { ) } -func TestCreateWhatsappTemplatePass(t *testing.T){ +func TestCreateWhatsappTemplatePass(t *testing.T) { template_data := `{ "name": "plivo_verification", "language": "en_US", @@ -207,7 +208,7 @@ func TestCreateWhatsappTemplatePass(t *testing.T){ Language: "en_US", Components: []Component{ { - Type: "body", + Type: "body", Parameters: []Parameter{ { Type: "text", @@ -232,6 +233,228 @@ func TestCreateWhatsappTemplatePass(t *testing.T){ assert.Equal(t, template, templateCreated) } +func TestCreateWhatsappInteractiveList(t *testing.T) { + interactiveData := `{ + "type": "list", + "header": { + "type": "text", + "text": "Welcome to Plivo" + }, + "body": { + "text": "You can review the list of rewards we offer" + }, + "footer": { + "text": "Yours Truly" + }, + "action": { + "buttons": [{ + "title": "Click here", + "id": "bt1j1k2jkjk" + }], + "sections": [{ + "title": "SECTION_1_TITLE", + "rows": [{ + "id": "SECTION_1_ROW_1_ID", + "title": "SECTION_1_ROW_1_TITLE", + "description": "SECTION_1_ROW_1_DESCRIPTION" + }, { + "id": "SECTION_1_ROW_2_ID", + "title": "SECTION_1_ROW_2_TITLE", + "description": "SECTION_1_ROW_2_DESCRIPTION" + }] + }, { + "title": "SECTION_2_TITLE", + "rows": [{ + "id": "SECTION_2_ROW_1_ID", + "title": "SECTION_2_ROW_1_TITLE", + "description": "SECTION_2_ROW_1_DESCRIPTION" + }, { + "id": "SECTION_2_ROW_2_ID", + "title": "SECTION_2_ROW_2_TITLE", + "description": "SECTION_2_ROW_2_DESCRIPTION" + }] + }] + } + }` + txt := "Welcome to Plivo" + expectedInteractive := Interactive{ + Type: "list", + Header: &Header{ + Type: "text", + Text: &txt, + }, + Body: &Body{ + Text: "You can review the list of rewards we offer", + }, + Footer: &Footer{ + Text: "Yours Truly", + }, + Action: &Action{ + Button: []*Buttons{ + { + ID: "bt1j1k2jkjk", + Title: "Click here", + }, + }, + Section: []*Section{ + { + Title: "SECTION_1_TITLE", + Row: []*Row{ + { + ID: "SECTION_1_ROW_1_ID", + Title: "SECTION_1_ROW_1_TITLE", + Description: "SECTION_1_ROW_1_DESCRIPTION", + }, + { + ID: "SECTION_1_ROW_2_ID", + Title: "SECTION_1_ROW_2_TITLE", + Description: "SECTION_1_ROW_2_DESCRIPTION", + }, + }, + }, + { + Title: "SECTION_2_TITLE", + Row: []*Row{ + { + ID: "SECTION_2_ROW_1_ID", + Title: "SECTION_2_ROW_1_TITLE", + Description: "SECTION_2_ROW_1_DESCRIPTION", + }, + { + ID: "SECTION_2_ROW_2_ID", + Title: "SECTION_2_ROW_2_TITLE", + Description: "SECTION_2_ROW_2_DESCRIPTION", + }, + }, + }, + }, + }, + } + interactive, _ := CreateWhatsappInteractive(interactiveData) + assert.Equal(t, expectedInteractive, interactive) +} +func TestCreateWhatsappInteractiveReply(t *testing.T) { + interactiveData := `{ + "type": "reply", + "header": { + "type": "media", + "media": "https://media.geeksforgeeks.org/wp-content/uploads/20190712220639/ybearoutput-300x225.png" + }, + "body": { + "text": "Make your selection" + }, + "action": { + "buttons": [ + { + "title": "Click here", + "id": "bt1" + }, + { + "title": "Know More", + "id": "bt2" + }, + { + "title": "Request Callback", + "id": "bt3" + } + ] + } + }` + mediaLink := "https://media.geeksforgeeks.org/wp-content/uploads/20190712220639/ybearoutput-300x225.png" + expectedInteractive := Interactive{ + Type: "reply", + Header: &Header{ + Type: "media", + Media: &mediaLink, + }, + Body: &Body{ + Text: "Make your selection", + }, + Action: &Action{ + Button: []*Buttons{ + { + ID: "bt1", + Title: "Click here", + }, + { + ID: "bt2", + Title: "Know More", + }, + { + ID: "bt3", + Title: "Request Callback", + }, + }, + }, + } + + interactive, _ := CreateWhatsappInteractive(interactiveData) + assert.Equal(t, expectedInteractive, interactive) +} + +func TestCreateWhatsappInteractiveCTA(t *testing.T) { + interactiveData := `{ + "type": "cta_url", + "header": { + "type": "media", + "media": "https://media.geeksforgeeks.org/wp-content/uploads/20190712220639/ybearoutput-300x225.png" + }, + "body": { + "text": "Know More" + }, + "action": { + "buttons": [ + { + "title": "Click here", + "id": "bt1", + "cta_url": "https://plivo.com" + } + ] + } + }` + + mediaLink := "https://media.geeksforgeeks.org/wp-content/uploads/20190712220639/ybearoutput-300x225.png" + expectedInteractive := Interactive{ + Type: "cta_url", + Header: &Header{ + Type: "media", + Media: &mediaLink, + }, + Body: &Body{ + Text: "Know More", + }, + Action: &Action{ + Button: []*Buttons{ + { + ID: "bt1", + Title: "Click here", + CTAURL: "https://plivo.com", + }, + }, + }, + } + + interactive, err := CreateWhatsappInteractive(interactiveData) + assert.NoError(t, err) + assert.Equal(t, expectedInteractive, interactive) +} + +func TestCreateWhatsappLocationPass(t *testing.T) { + location_data := `{ + "latitude": "37.483307", + "longitude": "122.148981", + "name": "Pablo Morales", + "address": "1 Hacker Way, Menlo Park, CA 94025" + }` + location := Location{ + Latitude: "37.483307", + Longitude: "122.148981", + Name: "Pablo Morales", + Address: "1 Hacker Way, Menlo Park, CA 94025", + } + locaitonCreated, _ := CreateWhatsappLocation(location_data) + assert.Equal(t, location, locaitonCreated) +}