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.
我想试一试,所以我建立了一个WhatsApp机器人待办事项清单,因为谁不需要一个自动待办事项清单来帮助安排他们的一天?这听起来很复杂,但实际上很容易,我想告诉你关于它的一切。
Now, I work at MessageBird, so I could just dive in and start building. If you try this, you’ll need to 请求提前访问. But once you’re set up with a WhatsApp channel, you can log on 到 Dashboard on the MessageBird website and get started.
ǞǞǞ 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 -region eu -subdomain todobot
Next, I needed to do a call 到 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))
}
}
很好。现在Conversations API要做一个POST请求到。
https://todobot.eu.ngrok.io/create-hook whenever a new message gets created on the WhatsApp channel you set up earlier.
这就是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"
}
我们想回答这些信息。让我们从呼应他们开始,你怎么说?
// 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 处理r 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.Conversation.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")
}
现在,有趣的部分来了。做一个POST请求到。
“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
}
在那里。这就是你创建一个行为像5岁人类的机器人所需要的一切。
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 处理消息 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 在 “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)
}
这里,我们设置了:list := manager.fetch(whp.Conversation.ID)。基本上,"manager "是一个并发安全地图,它将对话ID映射到待办事项列表。
一个待办事项清单是一个并发安全的字符串片断。所有这些都在内存中!
另一件重要的事情!你可以对对话进行存档。在一些应用程序中,如CRM,跟踪某些互动是很重要的--例如,跟踪客户支持员工的效率。会话API让你归档一个对话,以 "结束 "这个话题。如果用户/客户发送了另一条信息,对话API将自动打开一个新的话题。
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":
档案对话(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.
但嘿,可编程对话是一个全渠道的解决方案。如果你想在不同的平台上重新使用机器人的代码,如微信?你会如何去做呢?
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!
这将发挥作用,因为处理程序代码总是使用来自webhook有效载荷的对话ID来回答消息,而不是硬编码的channelID。MessageBird的Conversations API将自动确定对话的通道,以发送你的消息。
Do you want to build your own bot? Take a look 在 full code on Github: https://github.com/marcelcorso/wabot, request early access to WhatsApp via this link and start building directly. Happy botting!