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

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" ) 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.

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]) 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.

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) } } } } } } } } 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.

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) } } 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.

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) } } } 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.

fb { appSecret: "" pageAccessToken: "" verifyToken: "" responseUri = "https://graph.facebook.com/v2.6/me/messages" } 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.

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") } 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

Like this: Like Loading...