Feb 5, 2020
How to build a WhatsApp bot for to-do lists using Bird’s Programmable Conversations API
Bird recently launched Programmable Conversations. It lets companies blend communications platforms like WhatsApp, Messenger and SMS into their systems — using a single API.
Quería probarlo, así que he creado una lista de tareas de un bot de WhatsApp, porque ¿quién no necesita una lista de tareas automatizada que le ayude a organizar el día? Puede parecer complicado, pero en realidad fue fácil, y me gustaría contártelo todo.
Now, I work at MessageBird, so I could just dive in and start building. If you try this, you’ll need to solicitar acceso anticipado. But once you’re set up with a WhatsApp channel, you can log on a la Dashboard on the MessageBird website and get started.
En first thing I did was read the docs. I learned that, in order to get messages from the bot, I would have to use a webhook. This meant that my bot would need to be accessible from the internet. Since I was just starting to code it, I decided to use ngrok. It creates a tunnel from the public internet to your dear localhost port 5007. Engage!
ngrok http 5007 -región eu -subdominio todobot
Next, I needed to do a call a la Programmable Conversations API to create the webhook. It’s a POST to https://conversations.messagebird.com/v1/webhooks and it looks something like this:
func main() {// define the webhook json payload
wh := struct {
Events []string `json:"events"`
ChannelID string `json:"channelId"`
URL string `json:"url"`
} {// we would like to be notified on the URL
URL: "https://todobot.eu.ngrok.io/create-hook",
// whenever a message gets created
Events: []string{"message.created"},
// on the WhatsApp channel with ID
ChannelID: "23a780701b8849f7b974d8620a89a279",
}// encode the payload to json
var b bytes.Buffer
err := json.NewEncoder(&b).Encode(&wh)
if err != nil {
panic(err)
}// create the http request and set authorization header
req, err := http.NewRequest("POST", "https://conversations.messagebird.com/v1/webhooks", &b)
req.Header.Set("Authorization", "AccessKey todo-your-access-key")
req.Header.Set("Content-Type", "application/json")// fire the http request
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()// is everything ok?
body, _ := ioutil.ReadAll(resp.Body)
if resp.StatusCode >= http.StatusBadRequest {
panic(fmt.Errorf("Bad response code from api when trying to create webhook: %s. Body: %s", resp.Status, string(body)))
} else {
log.Println("All good. response body: ", string(body))
}
}
Genial. Ahora la API de Conversaciones va a hacer una petición POST a:
https://todobot.eu.ngrok.io/create-hook whenever a new message gets created on the WhatsApp channel you set up earlier.
Este es el aspecto de la carga útil de un webhook:
{
"conversation":{
"id":"55c66895c22a40e39a8e6bd321ec192e",
"contactId":"db4dd5087fb343738e968a323f640576",
"status":"active",
"createdDatetime":"2018-08-17T10:14:14Z",
"updatedDatetime":"2018-08-17T14:30:31.915292912Z",
"lastReceivedDatetime":"2018-08-17T14:30:31.898389294Z"
},
"message":{
"id":"ddb150149e2c4036a48f581544e22cfe",
"conversationId":"55c66895c22a40e39a8e6bd321ec192e",
"channelId":"23a780701b8849f7b974d8620a89a279",
"status":"received",
"type":"text",
"direction":"received",
"content":{
"text":"add buy milk"
},
"createdDatetime":"2018-08-17T14:30:31.898389294Z",
"updatedDatetime":"2018-08-17T14:30:31.915292912Z"
},
"type":"message.created"
}
Queremos responder a esos mensajes. Empecemos por hacernos eco de ellos, ¿qué me dices?
// define the structs where we'll parse the webhook payload intype whPayload struct {
Conversation conversation `json:"conversation"`
Message message `json:"message"`
Type string `json:"type"`
}type message struct {
ID string `json:"id"`
Direction string `json:"direction"`
Type string `json:"type"`
Content content `json:"content"`
}type content struct {
Text string `json:"text"`
}type conversation struct {
ID string `json:"id"`
}func main() {
http.HandleFunc("/create-hook", createHookHandler)
log.Fatal(http.ListenAndServe(*httpListenAddress, nil))
}// createHookHandler is an http asar that will handle webhook requests
func createHookHandler(w http.ResponseWriter, r *http.Request) {
// parse the incoming json payload
whp := &whPayload{}
err := json.NewDecoder(r.Body).Decode(whp)
if err != nil {
log.Println("Err: got weird body on the webhook")
w.WriteHeader(http.StatusInternalServerError)
fmt.Fprintf(w, "Internal Server Error")
return
}if whp.Message.Direction != "received" {
// you will get *all* messages on the webhook. Even the ones this bot sends to the channel. We don't want to answer those.
fmt.Fprintf(w, "ok")
return
}// echo: respond what we get
err = respond(whp.Conversación.ID, whp.Message.Content.Text)
if err != nil {
log.Println("Err: ", err)
w.WriteHeader(http.StatusInternalServerError)
fmt.Fprintf(w, "Internal Server Error")return
}w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, "ok")
}
Ahora, la parte interesante. Haz una petición POST a:
“https://conversations.messagebird.com/v1/conversations/<conversationID>/messages” to answer the request.
func respond(conversationID, responseBody string) error {
u := fmt.Sprintf("https://conversations.messagebird.com/v1/conversations/%s/messages", conversationID)msg := message{
Content: content{
Text: responseBody,
},
Type: "text",
}var b bytes.Buffer
err := json.NewEncoder(&b).Encode(&msg)
if err != nil {
return fmt.Errorf("Error encoding buffer: %v", err)
}req, err := http.NewRequest("POST", u.String(), &b)
req.Header.Set("Authorization", "AccessKey todo-your-access-key")
req.Header.Set("Content-Type", "application/json")client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()body, _ := ioutil.ReadAll(resp.Body)
if resp.StatusCode != http.StatusCreated {
return fmt.Errorf("Bad response code from api when trying to create message: %s. Body: %s", resp.Status, string(body))
}log.Println("All good. Response body: ", string(body))
return nil
}
Listo. Esto es todo lo que necesitas para crear un bot que actúe como un humano de 5 años.
Now, let’s make a push towards building the whole to-do list. First, modify the createHookHandler function a bit so it calls the new handleMessage function instead of respond.
func createHookHandler(w http.ResponseWriter, r *http.Request) {
...
err = handleMessage(whp)
...
}
handle will simplistically parse the messages, do some work, and pick the response. Let’s look en el “add” command:
func handleMessage(whp *whPayload) error {
// every conversation has a todo list
list := manager.fetch(whp.Conversation.ID)
// parse the command from the message body: it's the first word
text := whp.Message.Content.Text
text = regexp.MustCompile(" +").ReplaceAllString(text, " ")
parts := strings.Split(text, " ")
command := strings.ToLower(parts[0])
// default message
responseBody := "I don't understand. Type 'help' to get help."
switch command {
...
case "add":
if len(parts) < 2 {
return respond(whp.Conversation.ID, "err... the 'add' command needs a second param: the todo item you want to save. Something like 'add buy milk'.")
}
// get the item from the message body
item := strings.Join(parts[1:], " ")list.add(item)
responseBody = "added."
...
return respond(whp.Conversation.ID, responseBody)
}
Aquí, establecemos:list := manager.fetch(whp.Conversation.ID). Básicamente, "manager" es un mapa seguro de concurrencia que asigna IDs de conversación a listas de tareas.
Una lista de tareas pendientes es un string slice seguro para la concurrencia. ¡Todo en memoria!
Otra cosa importante Puedes archivar conversaciones. En algunas aplicaciones, como los CRM, es importante hacer un seguimiento de ciertas interacciones, por ejemplo, para controlar la eficacia de los empleados de atención al cliente. La API de Conversaciones te permite archivar una conversación para "cerrar" el tema. Si el usuario/cliente envía otro mensaje, la API de Conversaciones abrirá un nuevo tema automáticamente.
Also. Doing PATCH request to https://conversations.messagebird.com/v1/conversations/{id} with the right status on the body allows you to archive the conversation with that id. We do this with the “bye” command:
case "bye":
archivoConversación(whp.Conversation.ID)
manager.close(whp.Conversation.ID)
responseBody = "bye!"
archiveConversation will do the PATCH request and manager.close(whp.Conversation.ID) will remove the to-do list conversation.
Pero bueno, Programmable Conversations es una solución omnicanal. ¿Y si quisieras reutilizar el código del bot para una plataforma diferente, como WeChat? ¿Cómo lo harías?
Just create a new webhook to target that channel! A webhook that sends requests to the same https://todobot.eu.ngrok.io/create-hook url we used for WhatsApp!
Esto funcionará porque el código del manejador siempre utiliza el conversationID de la carga útil del webhook para responder a los mensajes en lugar de un channelID codificado. La API de Conversaciones de MessageBird determinará automáticamente el canal de la conversación para enviar tu mensaje.
Do you want to build your own bot? Take a look en el full code on Github: https://github.com/marcelcorso/wabot, request early access to WhatsApp via this link and start building directly. Happy botting!