AI/ML

Facebook Messenger bot: A tutorial in Go



Facebook Messenger bot: A tutorial in Go

Today is about simplicity. Users don’t want to download an app or sign up to your website to use a simple feature. With the social media and messaging landscape of today, there is no better integration than a chatbot. Take a look at china: they literally live and breathe WeChat. The western landscape is moving in this direction as well, with well over 60% of adults being on Facebook, which also means Messenger. Want to create a great customer experience? Create a good chatbot.

With so many options out there however, why would you ever write it yourself? To me, that’s simple; customization. As a developer, I crave control in technical environments, and don’t want to learn a new platform every time I want to prototype an idea. Why use Golang? Because I love writing code in it, and I love GCP (Google Cloud Platform).

The chatbot used in this example can be found here:

Messenger

This bot was written to make Niceable more approachable:

Niceable – nice people, nice things

All code present in this post can be found here:

frikky/medium-examples

Prerequisites

Webhooks

Webhooks are the backbone of how this works. The chat platform receives a message from a user, packs up the message into a nice HTTP POST request and sends it off to a webserver that you control. On the webserver, you handle it however you would like, before sending information back to the platform, usually in the form of a return message.

Let’s start by creating a webserver in Go. The following code handles GET and POST requests to http://localhost:8000/, which then will verify whether its a valid Messenger request. Take note that we’ve skipped all error checks here, and the variable “secretKey”.

package main
import (
"github.com/gorilla/mux"
"log"
"net/http"
"net/url"
)
func HandleMessenger(resp http.ResponseWriter, request *http.Request) {
secretKey := "secret_token"
if request.Method == "GET" {
u, _ := url.Parse(request.RequestURI)
values, _ := url.ParseQuery(u.RawQuery)
token := values.Get("hub.verify_token")
if token == secretKey {
resp.WriteHeader(200)
resp.Write([]byte(values.Get("hub.challenge")))
return
}
resp.WriteHeader(400)
resp.Write([]byte(`Bad token`))
return
}
}
// Initialize request
func main() {
router := mux.NewRouter()
router.HandleFunc("/", HandleMessenger).Methods("POST", "GET")
port := ":8000"
log.Printf("Server started on %s", port)
log.Fatal(http.ListenAndServe(port, router))
}
When you run it (go run function.go) and open it in your browser, it should look a little something like this.

When you run it ($ go run function.go) and open it in a browser (localhost:8000), it should look something like this. The reason for the “Bad token” will be explained in the following section, where we verify our endpoint.

Bad token request, as we’re verifying for Messenger

We will modify this code further down to match Messenger’s webhook needs.

Localhost to Public — developer environment

As we’re still developing on localhost, there is no point in setting up webhook forwarding from messenger yet. You remember how I said to download Ngrok in the Prerequisites section? Otherwise, download it now.

Start the webserver and point it to http://localhost:8000

./ngrok http 8000

It will start a new session, and tell you the forwarding rule.

In my case, the URL is https://e1343ffe.ngrok.io. We will take this URL and use it in the next section.

Configure Facebook Messenger webhooks

How do we get facebook to point to this webserver, you ask?

  1. Create a Facebook app: https://developers.facebook.com/apps. This requires a Facebook Developer account and a Facebook page.
Click the “Set up” button next to Messenger under the Products menu.

2. Inside the app, find “Products” on the left menu and click the + icon. Find Messenger and click “Set up”. Find “Webhooks” on the page and click “Add callback URL”.

Messenger webhook setup

3. Set the Callback URL to the Ngrok address we set earlier. Input “secret_token” in the second field, as this was hardcoded in our basic server setup. If you changed it, set it to what you changed it for.

Connect it to a facebook page

4. Connect it to your page by clicking the “Add Pages” button. If you don’t have a page, create one. When it’s set up, click the “Generate Token” button next to your page, and save the token it gives you for later. To directly work with our code example, export it like so:

export FACEBOOK_ACCESS_TOKEN="<INPUT YOUR TOKEN HERE>"
Configure messaging

5. Configure page webhook by scrolling back down to “webhooks”. Select the “messages” for now.

Enabling Natural language processing

6. Enable Natural language processing from Facebook. We will use this for a really simple matter later.

A POST request from the messenger webhook

7. Test your integration by messaging YOUR page on https://messenger.com (write literally anything). Check your webserver, and you should have a return as seen above.

The fun stuff

Now that we got the initial integration in order, its time to have fun and build what we actually want. This requires some overhead code so bear with me.

  • Text message (basic chatbot)
  • Postback (item with response question, e.g. a thing to buy)
  • Natural language processing

Text message — “Hello” response or NLP

First we start with imports and structs. As you can see, we need quite a bit more to make this work now. The first struct, InputMessage, is the message sent from a user on Messenger. The others are to be used to SEND a response back to the user, either as a text message (responseMessage) or a template (responseAttachment).

package main
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"github.com/gorilla/mux"
"io/ioutil"
"log"
"net/http"
"net/url"
"os"
)
// The message we get from Messenger
type InputMessage struct {
Object string `json:"object"`
Entry []struct {
ID string `json:"id"`
Time int64 `json:"time"
Payload struct {
Title string `json:"title"`
payload string `json:"payload"`
} `json:"payload"``
Messaging []struct {
Sender struct {
ID string `json:"id"`
} `json:"sender"`
Recipient struct {
ID string `json:"id"`
} `json:"recipient"`
Timestamp int64 `json:"timestamp"`
Message struct {
Mid string `json:"mid"`
Text string `json:"text"`
Nlp struct {
Entities struct {
Sentiment []struct {
Confidence float64 `json:"confidence"`
Value string `json:"value"`
} `json:"sentiment"`
Greetings []struct {
Confidence float64 `json:"confidence"`
Value string `json:"value"`
} `json:"greetings"`
} `json:"entities"`
DetectedLocales []struct {
Locale string `json:"locale"`
Confidence float64 `json:"confidence"`
} `json:"detected_locales"`
} `json:"nlp"`
} `json:"message"`
} `json:"messaging"`
} `json:"entry"`
}
// The recipient of our message
type Recipient struct {
ID string `json:"id"`
}
// The message to send it its basic
type Message struct {
Text string `json:"text,omitempty"`
}
type Button struct {
Type string `json:"type,omitempty"`
Title string `json:"title,omitempty"`
Payload string `json:"payload,omitempty"`
URL string `json:"url,omitempty"`
}
type Element struct {
Title string `json:"title,omitempty"`
Subtitle string `json:"subtitle,omitempty"`
ImageURL string `json:"image_url,omitempty"`
DefaultAction DefaultAction `json:"default_action,omitempty"`
Buttons []Button `json:"buttons,omitempty"`
}
type DefaultAction struct {
Type string `json:"type,omitempty"`
URL string `json:"url,omitempty"`
WebViewHeightRation string `json:"webview_height_ratio,omitempty"`
}
// The attachment to send (custom)
type Attachment struct {
Attachment struct {
Type string `json:"type,omitempty"`
Payload struct {
TemplateType string `json:"template_type,omitempty"`
Elements []Element `json:"elements,omitempty"`
} `json:"payload,omitempty"`
} `json:"attachment,omitempty"`
}
// Full response
type ResponseAttachment struct {
Recipient Recipient `json:"recipient"`
Message Attachment `json:"message,omitempty"`
}
// Full response
type ResponseMessage struct {
Recipient Recipient `json:"recipient"`
Message Message `json:"message,omitempty"`
}

Update to HandleMessenger

Our new HandleMessenger takes into account POST requests from Messenger, parsing the body into the InputMessage struct, before sending the request along to the handleMessage function.

func HandleMessenger(resp http.ResponseWriter, request *http.Request) {
secretKey := "secret_token"
if request.Method == "GET" {
u, _ := url.Parse(request.RequestURI)
values, _ := url.ParseQuery(u.RawQuery)
token := values.Get("hub.verify_token")
if token == secretKey {
resp.WriteHeader(200)
resp.Write([]byte(values.Get("hub.challenge")))
return
}
resp.WriteHeader(400)
resp.Write([]byte(`Bad token`))
return
}
// Anything that reaches here is POST.
body, err := ioutil.ReadAll(request.Body)
if err != nil {
log.Printf("Failed parsing body: %s", err)
resp.WriteHeader(400)
resp.Write([]byte("An error occurred"))
return
}
// Parse message into the Message struct
var message InputMessage
err = json.Unmarshal(body, &message)
if err != nil {
log.Printf("Failed unmarshalling message: %s", err)
resp.WriteHeader(400)
resp.Write([]byte("An error occurred"))
return
}
// Find messages
//log.Printf("Message: %#v", message)
for _, entry := range message.Entry {
if len(entry.Messaging) == 0 {
log.Printf("No messages")
resp.WriteHeader(400)
resp.Write([]byte("An error occurred"))
return
}
event := entry.Messaging[0]
err = handleMessage(event.Sender.ID, event.Message.Text)
if err != nil {
log.Printf("Failed sending message: %s", err)
resp.WriteHeader(400)
resp.Write([]byte("An error occurred"))
return
}
}
}

handleMessage function

With a parsed message and senderId, we now have everything required to make a proper answer. This function takes the message from the user, parses it and simply answers “Hello”. If you want to echo the request instead, change the response.Message.Text to equal the “message” string parameter. This is where you would use a NLP platform to help you out in your answer as well.

Trending Chatbot Tutorials

1. Adding Chatbots to Your Stream Chat App Using Google’s Dialogflow

2. DialogFlow fulfillment — dynamic responses from Google Firestore

3. Building a Simple Finance Bot with TensorFlow 1.12

4. Chatbot Conference 2020

We take into account the access_token we previously generated as well. This example uses the OS environemtn variable “FACEBOOK_ACCESS_TOKEN” as previously defined.

// Handles messages
func handleMessage(senderId, message string) error {
if len(message) == 0 {
return errors.New("No message found.")
}
response := Response{
Recipient: Recipient{
ID: senderId,
},
Message: Message{
Text: "Hello",
},
}
data, err := json.Marshal(response)
if err != nil {
log.Printf("Marshal error: %s", err)
return err
}
uri := "https://graph.facebook.com/v2.6/me/messages"
uri = fmt.Sprintf("%s?access_token=%s", uri, os.Getenv("FACEBOOK_ACCESS_TOKEN"))
log.Printf("URI: %s", uri)
req, err := http.NewRequest(
"POST",
uri,
bytes.NewBuffer(data),
)
if err != nil {
log.Printf("Failed making request: %s", err)
return err
}
req.Header.Add("Content-Type", "application/json")
client := http.Client{}
res, err := client.Do(req)
if err != nil {
log.Printf("Failed doing request: %s", err)
return err
}
log.Printf("MESSAGE SENT?n%#v", res)
return nil
}

Chat result

Automated result from our chatbot

Postback with multiple choice

With that out of the way, lets move onto something more interesting. There are multiple templates to be used (e.g. receipt or airline), but we’ll create a simple one like the image below. Click here to learn more about templates.

A giveaway template postback

There isn’t that much more to these actually. They require us to choose some features like Title, Subtitle, Buttons and a default action, but not much else.

Be sure to choose the right template however. The picture on the left is the “default” one. Here’s the function used to generate it:

// Handles messages
func handleAttachment(senderId, message string) error {
if len(message) == 0 {
return errors.New("No message found.")
}
response := ResponseAttachment{
Recipient: Recipient{
ID: senderId,
},
Message: Attachment{},
}
elements := []Element{
Element{
Title: "Check us out",
ImageURL: "https://niceable.co/images/heart.jpg",
Subtitle: "Fresh, organic and ethical giveaways",
DefaultAction: DefaultAction{
Type: "web_url",
URL: "https://niceable.co",
WebViewHeightRation: "tall",
},
Buttons: []Button{
Button{
Type: "web_url",
URL: "https://niceable.co",
Title: "Join giveaways",
},
},
},
}
response.Message.Attachment.Type = "template"
response.Message.Attachment.Payload.TemplateType = "generic"
response.Message.Attachment.Payload.Elements = elements
data, err := json.Marshal(response)
if err != nil {
log.Printf("Marshal error: %s", err)
return err
}
log.Printf("DATA: %s", string(data))
return sendRequest(data)
}

Not that different from the handleMessage function is it? One difference can be seen in the fact that we have buttons. To make button conversations (move the chat forwards), you can also use whats called “postbacks”. The postback button code looks like this:

{
"type":"postback",
"title":"Start Chatting",
"payload":"DEVELOPER_DEFINED_PAYLOAD"
}

Postbacks are essential if you want to have a conversation that’s simpler than just answering the same thing over and over. Other button options can be found here

Buttons – Messenger Platform – Documentation – Facebook for Developers

Moving to production

With our base code intact, now what? What can we actually do with this? The next and last step will be the following:

  • Have a welcome screen
  • Have a user sign up for a newsletter with just clicking
  • Deploy the webserver to a google cloud function

The welcome screen

The welcome screen is not a part of our Go code, but is configured through the API. This is how we configure it for now. It says hello to the user, and let’s them click the Get started button, which is a payload we will handle in our Go code.

#!/bin/sh
curl -X POST -H "Content-Type: application/json" -d '{
"get_started": {"payload": "GET_STARTED"},
"greeting": [
{
"locale":"default",
"text":"Hello {{user_first_name}}!"
}, {
"locale":"en_US",
"text":"Giveaways with a twist"
}
],
"persistent_menu": [
{
"locale": "default",
"composer_input_disabled": false,
"call_to_actions": [{
"type": "postback",
"title": "Join newsletter",
"payload": "JOIN_NEWSLETTER"
},
{
"type": "postback",
"title": "Enter a giveaway",
"payload": "GET_RAFFLES"
},
{
"type": "web_url",
"title": "Visit us",
"url": "https://niceable.co",
"webview_height_ratio": "full"
}]
}
]
}' "https://graph.facebook.com/v2.6/me/messenger_profile?access_token=$FACEBOOK_ACCESS_TOKEN"

If it works, you should see this. There is no way to test it before you go live with it, so just trust me on this one for now.

{“result”:”success”}

Basic conversation

With that out of the way, we’ll move onto a basic conversation.

  • Handle “GET_STARTED” payload or “Get started” and “Help” text, then give two routes:
  1. Show a carousel with information
  2. Sign up to newsletter with email or messenger through JUST clicks
How our chatbot works when finished

To get started, we have to understand the data we’re working with. Since we’re gonna work with EMAIL, we need to have a way to identify it. It’s simple to use regex, but I decided to use some built-in NLP for it instead.

Below are the types we will be working with based on Facebook specifications. These will help us route the user actions in the right direction. The two first ones are different because of Postback vs Message, but are really the same as they handle payload. The third one handles plain text (remember we’re looking for “help” and “get started”, and the last one is to use NLP to parse emails (this is always 1, so I set it above 0.99 :))

event.Postback.Payload
event.Message.QuickReply.Payload
event.Message.Text
event.Message.Nlp.Entities.Email[0].Confidence > 0.99

We have a couple of new global variables. This won’t work for a distributed systems environment, but that’s not what this post is for. These exist to save IDs between sessions to know what’s being replied to in quick replies.

var newsLetterIds = []string{}
var raffleIds = map[string][]string{}

The variables above are used in the following code, replaced in our function HandleMessenger.

// Find messages
for _, entry := range message.Entry {
if len(entry.Messaging) == 0 {
log.Printf("No messages")
resp.WriteHeader(400)
resp.Write([]byte("An error occurred"))
return
}
event := entry.Messaging[0]
// Handle the email with the MID we had earlier
log.Printf("EMAIL: %#v", event.Message.Nlp.Entities.Email)
if len(event.Message.Nlp.Entities.Email) > 0 {
if event.Message.Nlp.Entities.Email[0].Confidence > 0.99 {
err = handleEmail(event.Message.Nlp.Entities.Email[0].Value, event.Sender.ID)
if err != nil {
log.Printf("Failed handling email: %s", err)
resp.WriteHeader(400)
resp.Write([]byte("An error occurred"))
return
}
continue
}
}
if len(event.Postback.Payload) > 0 {
err = handlePostback(event.Sender.ID, event.Postback.Title, event.Postback.Payload)
if err != nil {
log.Printf("Failed payload(1): %s", err)
resp.WriteHeader(400)
resp.Write([]byte("An error occurred"))
return
}
} else if len(event.Message.QuickReply.Payload) > 0 {
err = handlePostback(event.Sender.ID, event.Message.Text, event.Message.QuickReply.Payload)
if err != nil {
log.Printf("Failed payload(2): %s", err)
resp.WriteHeader(400)
resp.Write([]byte("An error occurred"))
return
}
} else if len(event.Message.Text) > 0 {
err = handleText(event.Sender.ID, event.Message.Text)
if err != nil {
log.Printf("Failed payload(3 - text): %s", err)
resp.WriteHeader(400)
resp.Write([]byte("An error occurred"))
return
}
}
}

The code that actually handles the text routing is handlePostback (and other similar functions, all in the source code). As described previously, we’re using postback, which is data sent by the user when they click a button.

// Handles messages
func handlePostback(senderId, title, postback string) error {
if len(postback) == 0 {
return nil
}
log.Printf("INSIDE POSTBACK. Title: %s, postback: %s", title, postback)
//case "ENTER_TO_WIN":
// return handleEnterToWin(title, senderId)
switch message := postback; message {
case "GET_STARTED":
return handleGetStarted(senderId)
case "GET_RAFFLES":
return handleGetRaffles(senderId)
case "JOIN_NEWSLETTER":
return handleJoinNewsletter(senderId)
case "ENTER_NEWSLETTER_MESSENGER":
return handleJoinNewsletterMessenger(senderId)
default:
log.Printf("Can't handle payload %s", postback)
return nil
}
return nil
}

If you still don’t get the jist of it (no point in pasting all the code), routing goes like this:

  1. User writes or clicks a button -> Webhook to webserver
  2. Webserver parses what they write / clicked -> Answers, adds to a list, does API things…
  3. See step 1

Deploy the webserver to a google cloud function (optional)

Why would you want to push to a cloud function you ask? Because it’s relatively easy, it can run Go naively and it handles potential scaling for you.

Before you start, make sure you have gcloud installed (not part of prerequisites), and have a project ready and configured.

With that out of the way, take the following code (GIST) and save it to a file (or take the function.go file from the github).

https://medium.com/media/dd7319e3a46acaea8912a255527cc2ed/href

With all that in order, run the following code to deploy your webhook.

go mod init bot-tutorial
go get
# Deploy function to gcloud project. CONFIGURE!
gcloud functions deploy HandleMessenger --set-env-vars FACEBOOK_SECRET_KEY=<MAKE_UP_A_SECRET_KEY>,FACEBOOK_ACCESS_TOKEN="<YOUR_TOKEN_FROM_FACEBOOK_DEV(LONG)>" --runtime go111 --trigger-http

If you see an error like “note: module requires Go 1.13”, edit go.mod line 3 to be “go 1.11”.

When it’s done deploying, look for the field “httpsTrigger” and find the URL. It should be something like. We need to reconfigure our webhook in the Facebook app.

https://location-projectname.cloudfunctions.net/HandleMessenger

Voila, it’s now deployed and hopefully working! If we got our verification, you should be able to interact with ours here:

Messenger

Facebook Verification

Facebook’s verification is required to make your bot available to the public. Scroll down to the bottom of the Messenger developer view and click “Add to submission”. A modal will pop up, where you should input what they ask for. For the bot in this post, it took about 8 hours from application until it was in production.

Bot Conclusion

Creating a chatbot from scratch takes a bit of perseverance, but isn’t all that magical. We’ve talked about everything from why we want to make it (in Go), discussed webhooks and made a full implementation that can do basic chat functions. All in less than 500 lines of code.

The good thing? This code is VERY easy to change. Want to integrate with WhatsApp, WeChat, Line or whichever other? It’s same in essence, but another struct. Want to add Language processing through e.g. Dialogflow on top? Here’s some code for you to use in the “handleText” function (change the variable “message” to “inputMessage”, and you’re there.

sessionPath := fmt.Sprintf("projects/%s/agent/sessions/%s", projectID, sessionID)
textInput := dialogflowpb.TextInput{Text: inputMessage, LanguageCode: languageCode}
queryTextInput := dialogflowpb.QueryInput_Text{Text: &textInput}
queryInput := dialogflowpb.QueryInput{Input: &queryTextInput}
request := dialogflowpb.DetectIntentRequest{Session: sessionPath, QueryInput: &queryInput}
response, err := dialogflowClient.DetectIntent(ctx, &request)
if err != nil {
log.Printf("Response error: %s", err)
return "", err
}

Check out my other posts 🙂

Frikky – Medium

My twitter:

Frikky

And one of my pashion projects:

Niceable – nice people, nice things

Hope you enjoyed this rather long walk through.

Enjoy! 🙂

Don’t forget to give us your 👏 !

https://medium.com/media/7078d8ad19192c4c53d3bf199468e4ab/href


Facebook Messenger bot: A tutorial in Go was originally published in Chatbots Life on Medium, where people are continuing the conversation by highlighting and responding to this story.



Source link





Related posts

An iPhone application using a novel stool color detection algorithm for biliary atresia screening.

Newsemia

Teaching artificial intelligence to connect senses like vision and touch

Newsemia

#6: The Book of Why: The New Science of Cause and Effect

Newsemia

This website uses cookies to improve your experience. We'll assume you're ok with this, but you can opt-out if you wish. Accept Read More

Privacy & Cookies Policy