In this tutorial, we will go through all the steps to make your own Facebook Messenger Bot that echoes back messages it receives. We will use Scala and Akka HTTP , library to create REST API.
Let’s first define dependencies
1 2 3 4 5 6 7 8 |
libraryDependencies ++= Seq( "commons-codec" % "commons-codec" % "1.10", "com.typesafe.akka" %% "akka-actor" % "2.4.14", "com.typesafe.scala-logging" %% "scala-logging" % "3.1.0", "com.typesafe.akka" % "akka-slf4j_2.11" % "2.4.8", "com.typesafe.akka" %% "akka-http" % "10.0.0", "com.typesafe.akka" %% "akka-http-spray-json" % "10.0.0" ) |
Model
In this simple scenario, the model is just set of case classes to receive and send messages to Facebook.
Link to official documentation, where you can find a detailed description of each field.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
case class Payload(url: String) case class Attachment(`type`: String,payload: Payload) case class FBMessage(mid: Option[String] = None, seq: Option[Long] = None, text: Option[String] = None, metadata: Option[String] = None, attachment: Option[Attachment] = None) case class FBSender(id: String) case class FBRecipient(id: String) case class FBMessageEventIn(sender: FBSender, recipient: FBRecipient, timestamp: Long, message: FBMessage) case class FBMessageEventOut(recipient: FBRecipient, message: FBMessage) case class FBEntry(id: String, time: Long, messaging: List[FBMessageEventIn]) case class FBPObject(`object`: String, entry: List[FBEntry]) |
Routes
GET /webhook
is used by Facebook for verification during subscription.
POST /webhook
is used for handling messages.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
trait FBRoute extends Directives with LazyLogging with RouteSupport { protected implicit def actorSystem: ActorSystem protected implicit def ec: ExecutionContext protected implicit val materializer: ActorMaterializer private val fbService = FBService val fbRoute = { extractRequest { request: HttpRequest => get { path("webhook") { parameters("hub.verify_token", "hub.mode", "hub.challenge") { (token, mode, challenge) => complete { fbService.verifyToken(token, mode, challenge) } } } } ~ post { verifyPayload(request)(materializer, ec) { path("webhook") { entity(as[FBPObject]) { fbObject => complete { fbService.handleMessage(fbObject) } } } } } } } } |
Service layer
FBService consist of two methods:
verifyToken
: is used during subscription to verify token, send by Facebook, and sends back challenge token, otherwise Forbidden Http Code must be returned.
handleMessage
: after we receive a message, we create response payload messages with response text which is an echo of the message which we received. In order to send our newly created messages back, we need to make POST call to Facebook’s Graph API, we doing it in an asynchronous way using helper method HttpClient. We need to send 200 status code to confirm that we receive the message.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
object FBService extends LazyLogging { def verifyToken(token: String, mode: String, challenge: String) (implicit ec: ExecutionContext): (StatusCode, List[HttpHeader], Option[Either[String, String]]) = { if(mode == "subscribe" && token == BotConfig.fb.verifyToken){ logger.info(s"Verify webhook token: ${token}, mode ${mode}") (StatusCodes.OK, List.empty[HttpHeader], Some(Left(challenge))) } else { logger.error(s"Invalid webhook token: ${token}, mode ${mode}") (StatusCodes.Forbidden, List.empty[HttpHeader], None) } } def handleMessage(fbObject: FBPObject) (implicit ec: ExecutionContext, system: ActorSystem, materializer :ActorMaterializer): (StatusCode, List[HttpHeader], Option[Either[String, String]]) = { logger.info(s"Receive fbObject: $fbObject") fbObject.entry.foreach{ entry => entry.messaging.foreach{ me => val senderId = me.sender.id val message = me.message message.text match { case Some(text) => val fbMessage = FBMessageEventOut(recipient = FBRecipient(senderId), message = FBMessage(text = Some(s"Scala messenger bot: $text"), metadata = Some("DEVELOPER_DEFINED_METADATA"))).toJson.toString().getBytes HttpClient .post(s"${BotConfig.fb.responseUri}?access_token=${BotConfig.fb.pageAccessToken}", fbMessage) .map(_ => ()) case None => logger.info("Receive image") Future.successful(()) } } } (StatusCodes.OK, List.empty[HttpHeader], None) } } |
Security
To protect from unauthorized calls, HTTP requests coming from Facebook include header X-Hub-Signature
which is the SHA1 hash of the payload in method verifyPayload
we verify it using the App Secret
key.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
trait RouteSupport extends LazyLogging with Directives { def verifyPayload(req: HttpRequest) (implicit materializer: Materializer, ec: ExecutionContext): Directive0 = { def isValid(payload: Array[Byte], secret: String, expected: String): Boolean = { val secretKeySpec = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA1") val mac = Mac.getInstance("HmacSHA1") mac.init(secretKeySpec) val result = mac.doFinal(payload) val computedHash = Hex.encodeHex(result).mkString logger.info(s"Computed hash: $computedHash") computedHash == expected } req.headers.find(_.name == "X-Hub-Signature").map(_.value()) match { case Some(token) => val payload = Await.result(req.entity.toStrict(5 seconds).map(_.data.decodeString("UTF-8")), 5 second) logger.info(s"Receive token ${token} and payload ${payload}") val elements = token.split("=") val method = elements(0) val signaturedHash = elements(1) if(isValid(payload.getBytes, BotConfig.fb.appSecret, signaturedHash)) pass else { logger.error(s"Tokens are different, expected ${signaturedHash}") complete(StatusCodes.Forbidden) } case None => logger.error(s"X-Hub-Signature is not defined") complete(StatusCodes.Forbidden) } } } |
Configuration
In application.conf
we need to define verifyToken
, which is some random string you will create during subscription, to setup your webhooks, pageAccessToken
and appSecret
will be provided by Facebook during integration phase.
1 2 3 4 5 6 |
fb { appSecret: "" pageAccessToken: "" verifyToken: "" responseUri = "https://graph.facebook.com/v2.6/me/messages" } |
Starting the server
BotApp starts an Http server which listens on port 8080.
DebuggingDirectives.logRequestResult
enables to log all incoming requests and outgoing responses, which is very helpful during the integration phase.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
object BotApp extends App with FBRoute with LazyLogging { val decider: Supervision.Decider = { e => logger.error(s"Exception in stream $e") Supervision.Stop } implicit val actorSystem = ActorSystem("bot", ConfigFactory.load) val materializerSettings = ActorMaterializerSettings(actorSystem).withSupervisionStrategy(decider) implicit val materializer = ActorMaterializer(materializerSettings)(actorSystem) implicit val ec = actorSystem.dispatcher val routes = { logRequestResult("bot") { fbRoute } } implicit val timeout = Timeout(30.seconds) val routeLogging = DebuggingDirectives.logRequestResult("RouteLogging", Logging.InfoLevel)(routes) Http().bindAndHandle(routeLogging, "localhost", 8080) logger.info("Starting") } |
HTTPS
Facebook requires HTTPS to setup webooks. You can get free HTTPS certificates from Let’s Encrypt.
Integration with Facebook
You will need to create Facebook App and Facebook Page, setup webhooks, get Page Access Token
and App Secret
. All these steps are describe in official Facebook guide.
Deployment
Here I will give you few hints how I go about deployment. I added sbt-assembly
to the project, running sbt assembly
command will lets you package the whole project into single jar. I use Nginx as the reverse proxy, here is a link tutorial about setting up SSL with Nginx and Let’s Encrypt.
Summary
Assuming that this bot only echoes messages back to the sender, quite substantial amount of code was written, definitely, a lot has to do with the security but we got here clean, easy to extend solution to help you build cool stuff using Facebook messenger platform.
You can download complete project from https://github.com/cpuheater/scala-messenger-bot
Thx for guid. Maybe you will add something about how to deploy it on Heroku? I’m confused with Procfile creating
Thanks for feedback, as regards Heroku I’m not a big fun of this kind of services, but I added section Deployment with few hopefully useful tips.