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 🚀
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:
In this tutorial I will focus on a simple notification chatbot, which will just echo all received messages.
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
.
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.
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.
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.
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.
On the left sidebar, click the PRODUCTS +
button, it will redirect you to the Add a Product
page.
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
:
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.
Next look for the Webhooks
section and click Setup Webhooks
:
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.
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
.
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.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
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:
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.
/webhook
route to the address.test
, bot123
etc.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.
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.
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.
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()
}
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
:
Don’t forget to run the Vapor
project and set a proper webhook address. Copy your messenger link into Safari
:
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 😊