Tutorial: Building a Facebook bot with Vapor 3 and Swift, part I

June 25, 2019

Bots are everywhere! They are not taking over the world yet, but can definitely do a lot. Facebook has built a great ecosystem around Messenger. You can play with and use a lot of different Facebook bots, some of them are really useful, some of them are there just for fun 😉. Today I would like to show you how to build your own Messenger bot using Swift and Vapor framework. In part 1, I will start with a small simple bot that can just echo commands. In part 2, I will dive deep into the Facebook API and create a more interesting chat bot.

Let’s get started 🚀

Getting started with chatbots 🤖

Facebook Messenger bots

You probably have seen bots on the Messenger platform. If not, go to the Messenger app on your smartphone and explore the Discover tab.

There are basically three different types of chatbots:

  • Simple notification chatbots - these can, for example, provide information about your package shipping.
  • Process flow chatbots - these can guide you through the process of, for example, ordering a pizza.
  • Conversational chatbots - these can help you resolve issues using NLP (Natural Language Processing).

In this tutorial I will focus on a simple notification chatbot, which will just echo all received messages.

How does the Facebook platform looks like?

To create a Messenger Bot first of all you need to create a Facebook app. This app has to be connected to a Facebook page.

Create a Page

Let’s start with the Facebook Page. It will be used as the identity of your Bot. You have to keep in mind that a Facebook bot is just an app which is handling messages for you. When people chat with a Bot they will see the Page name and the Page profile picture.

Create a Page

To create a new Facebook page, go to this address. Choose Business or brand and click Get started. Enter Page name and Category. For now you can skip the Add a profile picture and Add a cover photo steps.

Business or brand

Your Facebook page is ready. Now it’s time to create the Facebook app.

To create a new Facebook app you will need a Facebook Developer Account. It is very easy to get one and if you have ever done anything with the Facebook api chances are you already have one. You can create a new developer account on the Facebook for Developers website.

Now open the apps page and click the + Add a New App button.

Create a New App ID

Enter a Display Name and your Contact Email and click Create App ID. After creating an app you will be redirected to the app settings.

App settings

On the left sidebar, click the PRODUCTS + button, it will redirect you to the Add a Product page.

Add a product

On this page click the Set Up button on the Messenger card. Messenger will be added to your app and you will be redirected to the settings page.

Inside the settings page look for the Access Tokens:

Access Tokens

Select your Facebook page and then click the Edit Permissions button. Follow the creator, at the end you will see that your app has been linked: You've now linked Test to Facebook. Now you can access your Page Access Token. Copy it and save it; you will need it later.

Webhooks

Next look for the Webhooks section and click Setup Webhooks:

New page subscription

On this page you need to provide a Callback URL for your the messenger app. This url will be used to send information back to your Vapor app. Before you create it let me explain what exactly it is.

What is a Webhook

Create a Page

Webhook provides a mechanism for the Facebook platform to notify your application when a new event has occurred on the server. It is an easy way of communicating with the Facebook platform. Instead of constantly checking if there is a new message from the user, Facebook will call your app whenever a new message arrives or other event occurs.

Inside Vapor, a webhook is simply just a route which you can name as you want. For a Messenger bot you will have to create an endpoint with two types of HTTP methods: GET and POST.

  • Webhook with GET method: will only be used once for a request validation when you create a new page subscription during the webhook setup on the Facebook platform.
  • Webhook with POST method: this is the heart of your app. The Facebook platform will send all of the notifications here.

Before creating a webhook inside Vapor there is one more thing to consider. In order to test the Messenger bot on your Macbook you need to expose your machine to the outside connections. The simplest solution is to use tools like Ngrok or other similar apps that will create public URLs to your local web server. For more details how to setup and use Ngrok you can go here.

Just remember that default port for Vapor is 8080:

➜ ./ngrok http 8080
Ngrok

Setting Up Your Webhook

Now it’s time for the fun part, let’s create the Vapor app 🎉

Create a new Vapor project:

➜ vapor new Facebook-bot-with-Vapor-3

Set Swift 5.0 and Vapor 3.3.0 inside Package.swift, you can also clean up and remove FluentSQLite from your dependencies, as it won’t be needed. You can check what Package.swift should look like here.

Generate the Xcode project file and open it:

➜ vapor xcode -y

To verify the webhook, create a controller file with the verify function. To know what kind of data Facebook will send during the verification, check FB documentation.

Create the controller file: App/Controllers/BotController.swift

import Vapor

final class BotController {
  
  let FacebookVerifyToken = Environment.get("FBVERIFYTOKEN")

  func verify(_ req: Request) throws -> Future<String> {
    let mode = try req.query.get(String.self, at: "hub.mode")
    let token = try req.query.get(String.self, at: "hub.verify_token")
    let challenge = try req.query.get(String.self, at: "hub.challenge")
    
    if mode == "subscribe" && FacebookVerifyToken == token {
      return req.eventLoop.newSucceededFuture(result: challenge)
    }
    else {
      throw Abort(.forbidden)
    }
  }
}

From the request query you need to get three parameters: hub.mode, hub.verify_token and hub.challenge.

  • hub.mode is not described inside the documentation so I’m not sure if other options instead of subscribe are available. But to be sure just check if hub.mode is equal to subscribe.

  • hub.challenge is a random text sent by the Facebook platform which you have to return during webhook verification.

  • hub.verify_token is the token that you set during webhook setup. It’s a good idea to pass it as an environment variable instead of hardcoding it.

Remember to set FBVERIFYTOKEN as an environment variable inside the Xcode scheme:

Ngrok

or if you are running your app from the Terminal:

➜ FBVERIFYTOKEN=bot123 ./Run serve --hostname 127.0.0.1 --port 8080 --env development

The verify function will be used for request validation. Remember to add missing routes inside App/routes.swift:

import Vapor

public func routes(_ router: Router) throws {
  let botController = BotController()
  router.get("webhook", use: botController.verify)
}

Run the Vapor project and inside the Terminal execute this curl command:

➜ curl -X GET "localhost:8080/webhook?hub.verify_token=bot123&hub.challenge=CHALLENGE_ACCEPTED&hub.mode=subscribe"

CHALLENGE_ACCEPTED%

As a result of the curl inside the Terminal, you should see the following printed text: CHALLENGE_ACCEPTED.

Let’s also write a simple test for this function.

To simplify all of the tests, copy the Vapor ➡️ helper and paste it inside: Tests/AppTests/TestsHelper.swift file. Don’t forget to make the extension public so other tests can see it. Then create the test file Tests/AppTests/WebhookTests.swift and put the testWebhook function inside:

func testWebhook() throws {
  
  let expectation = self.expectation(description: "Webhook")
  var responseData: Response?
  var challenge: String?
  
  try app?.test(.GET, "/webhook?hub.mode=subscribe&hub.verify_token=test&hub.challenge=CHALLENGE_ACCEPTED") { response in
    responseData = response
    challenge = String(decoding: response.http.body.data!, as: UTF8.self)
    expectation.fulfill()
  }
  
  waitForExpectations(timeout: 5, handler: nil)
  
  XCTAssertEqual(responseData?.http.status, .ok)
  XCTAssertEqual(responseData?.http.contentType, MediaType.plainText)
  XCTAssertEqual(challenge, "CHALLENGE_ACCEPTED")
}

The Application.test function will create a request and call the webhook endpoint. The body will be assigned to the challenge variable. Don’t forget to create an expectation and fulfill it inside the closure. You can check the whole test file here: WebhookTests.swift. Set FBVERIFYTOKEN as an environment variable also for test scheme.

It’s time to connect your webhook with the Facebook platform. To do this go back to the Facebook page and look for Setup Webhooks inside the settings page.

New page subscription

  • Callback URL - put your Ngrok public url (or any other that you are using) here, don’t forget to add the /webhook route to the address.
  • Verify Token - here you need to enter verification token that will be sent back to the webhook. You can name it as you want, test, bot123 etc.
  • Subscription Fields - here you can select different types of webhook events that will be delivered to the webhook. For now just select messages and messaging_postbacks.

Confirm it by clicking Verify and Save. Don’t forget to run the Vapor app! After your webhook has been verified you can now move to message handling.

Receiving the messages

It’s time to create a function that will take care of the incoming messages via webhook POST.

To better understand what kind of structure will be there, check out the Facebook documentation: Webhook Events Reference. I have already created the file with a Facebook webhook response struct. Download it from Github and put it in Models/BotStructure.swift.

Inside BotController.swift add a function that will be responsible for receiving the messages from the Messenger platform:

func message(_ req: Request) throws -> Future<Response> {
  // #1
  let data = try req.content.decode(FBResponseWrapper.self)
  
  // #2
  let botService = try req.make(BotServiceProtocol.self)
  let messagesService = try req.make(MessagesServiceProtocol.self)
  
  return data.flatMap { result in
    // #3
    guard let messageInput = result.entry.last?.messaging.last else {
      // Beware that in some cases Facebook response may not contain any messages. 
      throw Abort(.badRequest)
    }

    // #4
    let fbMessage = try messagesService.createMessage(for: messageInput)
    return try botService.sendFBMessage(req, message: fbMessage, recipient: messageInput.sender)
  }
}

Let’s go through this code step by step:

  • #1: Content of the request will be decoded to the FBResponseWrapper struct. You can find this struct in the Models/BotStructure.swift file.
  • #2: Then create the bot service, which will be responsible for sending messages, and the messages service which will be responsible for handling the messages logic. Remember to use protocols, this way it will be easier to switch those dependencies inside tests. You can read more about it in my previous blog post: Using the dependency injection framework for testing in Vapor 3 and Swift.
  • #3: Get the incoming message. There can be more messages inside the request body, but for now just get the last one.
  • #4: Bot will echo all of the incoming messages back to the user. Using the messages service, create a response message and send it back to the user using the bot service.

Don’t forget to add all missing routes inside the App/routes.swift:

import Vapor

public func routes(_ router: Router) throws {
    let botController = BotController()
    router.get("webhook", use: botController.verify)
    router.post("webhook", use: botController.message)
}

Caveat:

As you may have noticed, the message function is not checking the source of the input, so anyone who knows the webhook address can send malicious requests and pretend to be a Messenger bot. There is a simple solution to this issue. Messenger is providing a signed signature of the body that you can access from header: X-Hub-Signature. An attacker would need your personal app secret to spoof the request. Read more in the documentation: Validating Webhook Events

I will write how to secure your webhook in part two of this tutorial.

Create a new message

The Facebook messenger platform offers additional settings for the initial conversation. This means that you can set a greeting message and a Get started button. Using the Get started button will help your bot initialize conversation and set a different first response. To set the Get started button you need to setup an additional payload for the get started action. To do this, simply send it to the Facebook API in a Terminal:

curl -X POST -H "Content-Type: application/json" -d '{
  "get_started": { "payload": "START" },
  "greeting":[{ "locale":"default", "text":"Hello 👋" }]
}' "https://graph.facebook.com/v2.6/me/messenger_profile?access_token=[PUT YOUR FB TOKEN HERE]"

You can read more about Get started button and Greeting Text in the Messenger documentation.

Initial conversation

Now let’s create a messages service: Models/MessagesService.swift. Start with a protocol: MessagesServiceProtocol:

protocol MessagesServiceProtocol {
  func createMessage(for input: FBMessagingDetails) throws -> FBMessage
}

Then create a class that conforms to the MessagesServiceProtocol and the Service protocol:

class MessagesService: MessagesServiceProtocol, Service {
  func createMessage(for input: FBMessagingDetails) throws -> FBMessage {
    if input.postback?.payload == "START" {
      // #1
      return FBMessage(text: "Hi 👋 I'm Echo Bot, I will send back everything you write to me 😄")
    }
    else if let message = input.message {
      // #2
      return FBMessage(text: "Echo: \(message.text ?? "This format is not supported yet, sorry 😇")")
    }
    else {
      throw Abort(.badRequest)
    }
  }
}

createMessage function has a very simple logic:

  • #1: If there is a payload value and it matches with the predefined payload value it means that this is the initial conversation. The user has just clicked the Get Started button. In this case say hi to the user.
  • #2: If there is a message inside, create an echo message. Also remember to check if there is text inside the FBMessage struct. Text is optional, the user can send a sticker or any other type of message. In that case text will be nil.

To use the MessagesService don’t forget to register it as a MessagesServiceProtocol inside the app configuration file App/configure.swift:

services.register(MessagesServiceProtocol.self) { container in
  return MessagesService()
}

Sending the messages

You are almost there! The messages service is ready. Now let’s create the FBService which will handle the communication with the Messenger platform.

Create Models/BotService.swift and start with the protocol BotServiceProtocol:

protocol BotServiceProtocol {
  func sendFBMessage(_ req: Request, message: FBMessage, recipient: FBUserID) throws -> EventLoopFuture<Response>
}

The sendFBMessage function requires Request to get access to the Client object, which will allow you to send HTTP requests. Then create the class FBService that conforms to the BotServiceProtocol and the Service protocol:

class FBService: BotServiceProtocol, Service {
  let FacebookAPIToken = Environment.get("FBAPITOKEN")
  
  func sendFBMessage(_ req: Request, message: FBMessage, recipient: FBUserID) throws -> EventLoopFuture<Response> {
    // #1
    var fbMessagesAPI = URLComponents(string: "https://graph.facebook.com/v2.6/me/messages")!
    fbMessagesAPI.queryItems = [URLQueryItem(name: "access_token", value: FacebookAPIToken)]
    
    // #2
    return try req.client().post(fbMessagesAPI.url!, headers: HTTPHeaders.init([("Content-Type", "application/json")])) { body in
      try body.content.encode(json: FBSendMessage(message: message, recipient: recipient))
    }
  }
}

Sending the message to the Facebook platform is pretty straightforward:

  • #1: Prepare the API url, add access_token.
  • #2: Send POST request using the Client object. Inside the closure you can set the body of the request. All you have to do is to send a FBSendMessage struct with a message and a recipient.

To use the FBService don’t forget to register it as a BotServiceProtocol inside the app configuration file App/configure.swift:

services.register(BotServiceProtocol.self) { container in
  return FBService()
}

Now let’s create two additional tests to fully test the second webhook endpoint.

First of all, you need to create the FBService mock. You don’t want to send actual data to Facebook during testing. Create file Tests/AppTests/BotManagerMock.swift. Details of this file you can find here. Then modify setUp function to register BotServiceProtocol and MessagesServiceProtocol, inside the test file Tests/AppTests/WebhookTests.swift:

override func setUp() {
  super.setUp()
  self.fbServiceMock = FBServiceMock()
  
  app = try! Application.makeTest(configure: { (config, services) in
    services.register(BotServiceProtocol.self) { container in
      return self.fbServiceMock!
    }
    
    services.register(MessagesServiceProtocol.self) { container in
      return MessagesService()
    }
  }, routes: routes)
}

let testsURL: URL = {
  let directory = DirectoryConfig.detect()
  let workingDirectory = URL(fileURLWithPath: directory.workDir)
  return workingDirectory.appendingPathComponent("Tests", isDirectory: true)
}()

override func tearDown() {
  super.tearDown()
  
  app = nil
  fbServiceMock = nil
}

Now create a test for the initial message:

func testInitialMessage() throws {
  
  let expectation = self.expectation(description: "Webhook")
  var responseData: Response?
  // #1
  let jsonData = try Data(contentsOf: testsURL.appendingPathComponent("FB-request-1.json"))
  self.fbServiceMock!.response = HTTPResponse(status: .ok, body: "{}")
  // #2
  try app?.test(HTTPRequest(method: .POST, url: URL(string: "/webhook")!), beforeSend: { request in
    request.http.headers = HTTPHeaders.init([("Content-Type", "application/json")])
    // #3
    request.http.body = HTTPBody(data: jsonData)
  }, afterSend: { response in
    responseData = response
    expectation.fulfill()
  })
  
  waitForExpectations(timeout: 5, handler: nil)
  
  XCTAssertEqual(responseData?.http.status, .ok)
  // #4
  XCTAssertEqual(self.fbServiceMock?.responseMessage, "Hi 👋 I'm Echo Bot, I will send back everything you write to me 😄")
}
  • #1: Load the content of the FB-request-1.json file, you can get this file from here. This is the request which Facebook will send to your webhook after the user clicks the Get Started button.
  • #2: Application.test function will create a POST request to the webhook.
  • #3: Inside the beforeSend closure you can set the body of the request, simply assign the json data loaded earlier.
  • #4: Check if FBService mock has a proper message value.

You can check the whole test file here. It also has the second test for the echo message.

It’s time to play with your bot 😄. To find your Messenger Link, go to your fanpage: Settings > Messenger Platform and look for Your Messenger Link:

Your Messenger Link

Don’t forget to run the Vapor project and set a proper webhook address. Copy your messenger link into Safari:

Summary ✅

As you can see, this Facebook bot is pretty simple. The goal of this post is to help you to understand the basics of the Messenger API and how to use it. You can check the repository with the entire project on Github.

On the next blog post I will show you more interesting functions that are available on the platform. I will also talk about how to secure the webhook and prevent it from being accessed by anyone who knows its address.

What do you think of the Messenger platform? Have you tried to create your own bot? Did you have any difficulties with the API? Let me know - along with your questions, comments or feedback - on Twitter @mikemikina.

Happy coding 😊