How to Develop a Facebook Messenger Bot in Golang

Written by vgukasov | Published 2021/12/16
Tech Story Tags: facebook | facebook-messenger | facebook-messenger-chatbot | chatbots | chatbot | golang | go | messenger-bot-in-golang

TLDRRecently, we developed a Facebook Messenger bot as a different way for 2FA (Two Factor Authentication), and it’s significantly reduced the SMS costs. That’s why I want to tell you how to develop a Facebook Messenger bot.via the TL;DR App

Facebook is the biggest social network worldwide today. It has more than 2.9 billion monthly active users. That’s why many companies have started using Facebook Messenger for business purposes.

Recently, we developed a Facebook Messenger bot as a different way for 2FA (Two Factor Authentication), and it significantly reduced SMS costs. That’s why I want to tell you how to develop a Facebook Messenger bot.

Architecture

Let’s see how the whole architecture looks like.

So a user sends a message to the Facebook Messenger bot. On message, Facebook sends webhook to our server (Golang App). The server handles the message and responds to the user by Facebook Messenger API.

Golang Webhook Handler

I’ve made a sample repository to show how you can quickly develop the Facebook Messenger bot.

Before setting up webhooks from the Facebook side, we need to implement our server app that will handle the requests:

package main

import (
	"go-facebook-bot/pkg/fb"
	"log"
	"net/http"
)

func main() {
	http.HandleFunc("/webhook", fb.HandleMessenger)

	port := ":8099" // port to listen to
	log.Fatal(http.ListenAndServe(port, nil))
}

// HandleMessenger handles all incoming webhooks from Facebook Messenger.
func HandleMessenger(w http.ResponseWriter, r *http.Request) {
	if r.Method == http.MethodGet {
		HandleVerification(w, r)
		return
	}

	HandleWebHook(w, r)
}

Facebook messenger can send two types of webhooks: GET or POST. It depends on the purpose of the webhook:

  • GET requests means verification. Facebook sends them when you add a new webhook URL in your bot’s settings. It’s a kind of way to verify that your server works correctly. In the method, you have to compare that the hub.verify_token matches your verification token (we’ll get it in the Facebook setup part):

// HandleVerification handles the verification request from Facebook.
func HandleVerification(w http.ResponseWriter, r *http.Request) {
	if verifyToken != r.URL.Query().Get("hub.verify_token") {
		w.WriteHeader(http.StatusUnauthorized)
		w.Write(nil)
		return
	}

	w.WriteHeader(http.StatusOK)
	w.Write([]byte(r.URL.Query().Get("hub.challenge")))
}

  • POST requests are triggered when a user sends a message to your bot:

// HandleWebHook handles a webhook incoming from Facebook.
func HandleWebHook(w http.ResponseWriter, r *http.Request) {
	err := Authorize(r)
	if err != nil {
		w.WriteHeader(http.StatusUnauthorized)
		w.Write([]byte("unauthorized"))
		log.Println("authorize", err)
		return
	}

	body, err := io.ReadAll(r.Body)
	if err != nil {
		w.WriteHeader(http.StatusBadRequest)
		w.Write([]byte("bad request"))
		log.Println("read webhook body", err)
		return
	}

	wr := WebHookRequest{}
	err = json.Unmarshal(body, &wr)
	if err != nil {
		w.WriteHeader(http.StatusBadRequest)
		w.Write([]byte("bad request"))
		log.Println("unmarshal request", err)
		return
	}

	err = handleWebHookRequest(wr)
	if err != nil {
		w.WriteHeader(http.StatusInternalServerError)
		w.Write([]byte("internal"))
		log.Println("handle webhook request", err)
		return
	}

	// Facebook waits for the constant message to get that everything is OK
	w.WriteHeader(http.StatusOK)
	w.Write([]byte("EVENT_RECEIVED"))
}

Firstly, we need to authorize the webhook because a potential attacker can use our server:

package fb

import (
	"bytes"
	"crypto/hmac"
	"crypto/sha1"
	"encoding/hex"
	"errors"
	"fmt"
	"io/ioutil"
	"net/http"
	"strings"
)

const (
	headerNameXSign = "X-Hub-Signature"
	signaturePrefix = "sha1="
)

// errors
var (
	errNoXSignHeader      = errors.New("there is no x-sign header")
	errInvalidXSignHeader = errors.New("invalid x-sign header")
)

// Authorize authorizes web hooks from FB
// https://developers.facebook.com/docs/graph-api/webhooks/getting-started/#validate-payloads
func Authorize(r *http.Request) error {
	signature := r.Header.Get(headerNameXSign)
	if !strings.HasPrefix(signature, signaturePrefix) {
		return errNoXSignHeader
	}

	body, err := ioutil.ReadAll(r.Body)
	if err != nil {
		return fmt.Errorf("read all: %w", err)
	}

	// We read the request body and now it's empty. We have to rewrite it for further reads.
	r.Body.Close() //nolint:errcheck
	r.Body = ioutil.NopCloser(bytes.NewBuffer(body))

	validSignature, err := isValidSignature(signature, body)
	if err != nil {
		return fmt.Errorf("is valid signature: %w", err)
	}
	if !validSignature {
		return errInvalidXSignHeader
	}

	return nil
}

func signBody(body []byte) []byte {
	h := hmac.New(sha1.New, []byte(appSecret))
	h.Reset()
	h.Write(body)

	return h.Sum(nil)
}

func isValidSignature(signature string, body []byte) (bool, error) {
	actualSign, err := hex.DecodeString(signature[len(signaturePrefix):])
	if err != nil {
		return false, fmt.Errorf("decode string: %w", err)
	}

	return hmac.Equal(signBody(body), actualSign), nil
}

When we’re satisfied that the webhook came from Facebook, we can handle it. In my sample, a bot takes webhooks with the following algorithm: if a user sends a message with the text hello, it responds with word. Otherwise, the bot asks the user: What can I do for you? .

func handleWebHookRequest(r WebHookRequest) error {
	if r.Object != "page" {
		return errUnknownWebHookObject
	}

	for _, we := range r.Entry {
		err := handleWebHookRequestEntry(we)
		if err != nil {
			return fmt.Errorf("handle webhook request entry: %w", err)
		}
	}

	return nil
}

func handleWebHookRequestEntry(we WebHookRequestEntry) error {
    // Facebook claims that the arr always contains a single item but we don't trust them :)
	if len(we.Messaging) == 0 { 
		return errNoMessageEntry
	}

	em := we.Messaging[0]

	// message action
	if em.Message != nil {
		err := handleMessage(em.Sender.ID, em.Message.Text)
		if err != nil {
			return fmt.Errorf("handle message: %w", err)
		}
	}

	return nil
}

func handleMessage(recipientID, msgText string) error {
	msgText = strings.TrimSpace(msgText)

	var responseText string
	switch msgText {
	case "hello":
		responseText = "world"
	// @TODO your custom cases
	default:
		responseText = "What can I do for you?"
	}

	return Respond(context.TODO(), recipientID, responseText)
}

Finally, there is a method Respond to send a message to a user via Facebook API:

package fb

import (
	"context"
	"encoding/json"
	"fmt"
	"github.com/valyala/fasthttp"
	"time"
)

const (
	uriSendMessage = "https://graph.facebook.com/v12.0/me/messages"

	defaultRequestTimeout = 10 * time.Second
)

// https://developers.facebook.com/docs/messenger-platform/send-messages/#messaging_types
const (
	messageTypeResponse = "RESPONSE"
)

var (
	client = fasthttp.Client{}
)

// Respond responds to a user in FB messenger. This includes promotional and non-promotional messages sent inside the 24-hour standard messaging window.
// For example, use this tag to respond if a person asks for a reservation confirmation or an status update.
func Respond(ctx context.Context, recipientID, msgText string) error {
	return callAPI(ctx, uriSendMessage, SendMessageRequest{
		MessagingType: messageTypeResponse,
		RecipientID: MessageRecipient{
			ID: recipientID,
		},
		Message: Message{
			Text: msgText,
		},
	})
}

func callAPI(ctx context.Context, reqURI string, reqBody interface{}) error {
	req := fasthttp.AcquireRequest()
	defer fasthttp.ReleaseRequest(req)

	req.SetRequestURI(fmt.Sprintf("%s?access_token=%s", reqURI, accessToken))
	req.Header.SetMethod(fasthttp.MethodPost)
	req.Header.Add("Content-Type", "application/json")

	body, err := json.Marshal(&reqBody)
	if err != nil {
		return fmt.Errorf("marshal: %w", err)
	}
	req.SetBody(body)

	res := fasthttp.AcquireResponse()
	defer fasthttp.ReleaseResponse(res)

	dl, ok := ctx.Deadline()
	if !ok {
		dl = time.Now().Add(defaultRequestTimeout)
	}

	err = client.DoDeadline(req, res, dl)
	if err != nil {
		return fmt.Errorf("do deadline: %w", err)
	}

	resp := APIResponse{}
	err = json.Unmarshal(res.Body(), &resp)
	if err != nil {
		return fmt.Errorf("unmarshal response: %w", err)
	}
	if resp.Error != nil {
		return fmt.Errorf("response error: %s", resp.Error.Error())
	}
	if res.StatusCode() != fasthttp.StatusOK {
		return fmt.Errorf("unexpected rsponse status %d", res.StatusCode())
	}

	return nil
}

Facebook Configuration

We developed a bot that can handle webhooks, but we still need to run it live. The point is Facebook will check that our server can handle requests.

Fortunately, a tool can help us expose the local port from our machine to the external internet. It’s Ngrok. Start right now if you are not using it yet because it’s indispensable in bot development.

Start ngrok:

ngrok http 8099

Run the Go server:

go run main.go

Now you’re ready to set up Facebook stuff. Luckily, they have good documentation on how to prepare everything for your bot. In short, you need to:

  • Create a new Facebook Page that will be used as the identity of your Messenger bot
  • Register a Facebook Developer Account
  • Create a new Facebook App in the Developer Account from the previous item
  • Set up WebHook URL in your Facebook App. It’s going to be something like https://5e5a-188-168-215-46.ngrok.io/v1/webhook (replace the ngrok part with your own)

Checking That Everything Works

Now let’s check that our bot correctly handles the Facebook Messenger webhooks:

It looks like it works 🙂

Conclusion

At first, integration with Facebook Messenger may look scary: complex documentation and architecture. But when your start to develop, you see that it is pretty simple. And with the support of a great programming language as Go, you can develop a reliable and extremely fast bot.


Written by vgukasov | Software Engineer @ Amazon
Published by HackerNoon on 2021/12/16