├── .gitignore ├── README.md ├── example └── index.js ├── index.js ├── package.json └── test ├── test.basic.js ├── test.send.api.js └── test.thread.settings.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Node template 3 | # Logs 4 | logs 5 | *.log 6 | npm-debug.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | 13 | # Directory for instrumented libs generated by jscoverage/JSCover 14 | lib-cov 15 | 16 | # Coverage directory used by tools like istanbul 17 | coverage 18 | 19 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 20 | .grunt 21 | 22 | # node-waf configuration 23 | .lock-wscript 24 | 25 | # Compiled binary addons (http://nodejs.org/api/addons.html) 26 | build/Release 27 | 28 | # Dependency directory 29 | # https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git 30 | node_modules 31 | ### JetBrains template 32 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio 33 | 34 | *.iml 35 | 36 | ## Directory-based project format: 37 | .idea/ 38 | # if you remove the above rule, at least ignore the following: 39 | 40 | # User-specific stuff: 41 | # .idea/workspace.xml 42 | # .idea/tasks.xml 43 | # .idea/dictionaries 44 | 45 | # Sensitive or high-churn files: 46 | # .idea/dataSources.ids 47 | # .idea/dataSources.xml 48 | # .idea/sqlDataSources.xml 49 | # .idea/dynamic.xml 50 | # .idea/uiDesigner.xml 51 | 52 | # Gradle: 53 | # .idea/gradle.xml 54 | # .idea/libraries 55 | 56 | # Mongo Explorer plugin: 57 | # .idea/mongoSettings.xml 58 | 59 | ## File-based project format: 60 | *.ipr 61 | *.iws 62 | 63 | ## Plugin-specific files: 64 | 65 | # IntelliJ 66 | /out/ 67 | 68 | # mpeltonen/sbt-idea plugin 69 | .idea_modules/ 70 | 71 | # JIRA plugin 72 | atlassian-ide-plugin.xml 73 | 74 | # Crashlytics plugin (for Android Studio and IntelliJ) 75 | com_crashlytics_export_strings.xml 76 | crashlytics.properties 77 | crashlytics-build.properties 78 | ### OSX template 79 | .DS_Store 80 | .AppleDouble 81 | .LSOverride 82 | 83 | # Icon must end with two \r 84 | Icon 85 | 86 | # Thumbnails 87 | ._* 88 | 89 | # Files that might appear in the root of a volume 90 | .DocumentRevisions-V100 91 | .fseventsd 92 | .Spotlight-V100 93 | .TemporaryItems 94 | .Trashes 95 | .VolumeIcon.icns 96 | 97 | # Directories potentially created on remote AFP share 98 | .AppleDB 99 | .AppleDesktop 100 | Network Trash Folder 101 | Temporary Items 102 | .apdisk 103 | 104 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FB Bot Framework 2 | `fb-bot-framework` is a node framework for [Facebook Messenger Platform](https://developers.facebook.com/docs/messenger-platform)(FB Messenger Bot). 3 | 4 | ## Installation 5 | ```bash 6 | npm install fb-bot-framework --save 7 | ``` 8 | 9 | ## Features 10 | * work as Express Middleware 11 | * implement of [Send API](https://developers.facebook.com/docs/messenger-platform/send-api-reference). 12 | * customize notification type(REGULAR, SILENT_PUSH, NO_PUSH) for each individual message 13 | * implement of Thread Settings API (Greeting Text / Get Started Button / Persistent Menu) 14 | 15 | 16 | ## Example Usage 17 | You can use ```bot.middleware()``` to hook any request to the path ```/webhook``` to the bot. 18 | 19 | ```js 20 | var express = require('express'); 21 | var app = express(); 22 | 23 | var FBBotFramework = require('fb-bot-framework'); 24 | 25 | // Initialize 26 | var bot = new FBBotFramework({ 27 | page_token: "THIS_IS_PAGE_TOKEN", 28 | verify_token: "THIS_IS_VERIFY_TOKEN" 29 | }); 30 | 31 | // Setup Express middleware for /webhook 32 | app.use('/webhook', bot.middleware()); 33 | 34 | // Setup listener for incoming messages 35 | bot.on('message', function(userId, message){ 36 | // bot.sendTextMessage(userId, "Echo Message:" + message); 37 | 38 | // Send quick replies 39 | var replies = [ 40 | { 41 | "content_type": "text", 42 | "title": "Good", 43 | "payload": "thumbs_up" 44 | }, 45 | { 46 | "content_type": "text", 47 | "title": "Bad", 48 | "payload": "thumbs_down" 49 | } 50 | ]; 51 | bot.sendQuickReplies(userId, message, replies); 52 | }); 53 | 54 | // Setup listener for quick reply messages 55 | bot.on('quickreply', function(userId, payload){ 56 | bot.sendTextMessage(userId, "payload:" + payload); 57 | }); 58 | 59 | // Config the Get Started Button and register a callback 60 | bot.setGetStartedButton("GET_STARTED"); 61 | bot.on('postback', function(userId, payload){ 62 | 63 | if (payload == "GET_STARTED") { 64 | getStarted(userId); 65 | } 66 | 67 | // Other postback callbacks here 68 | // ... 69 | 70 | }); 71 | 72 | function getStarted(userId){ 73 | 74 | // Get started process 75 | } 76 | 77 | // Setup listener for attachment 78 | bot.on('attachment', function(userId, attachment){ 79 | 80 | // Echo the audio attachment 81 | if (attachment[0].type == "audio") { 82 | bot.sendAudioAttachment(userId, attachment[0].payload.url); 83 | } 84 | 85 | }); 86 | 87 | // Make Express listening 88 | app.listen(3000); 89 | ``` 90 | 91 | ## API 92 | ### Events 93 | #### ```bot.on("message", function(userId, message))``` 94 | 95 | Triggered when an user sends a typed message to the bot. 96 | * ```userId``` - String: The Facebook Id of the sender. 97 | * ```message``` - String: The actual message sent by the user. 98 | 99 | #### ```bot.on("postback", function(userId, payload))``` 100 | 101 | Triggered when an user clicks a button which will send a postback message. 102 | * ```userId``` - String: The Facebook Id of the sender. 103 | * ```payload``` - String: The payload associated with the button clicked by the user. 104 | 105 | #### ```bot.on("quickreply", function(userId, payload))``` 106 | 107 | Triggered when an user clicks a quick reply button.. 108 | * ```userId``` - String: The Facebook Id of the sender. 109 | * ```payload``` - String: The payload associated with the button clicked by the user. 110 | 111 | 112 | ### Functions 113 | #### ```bot.setGreetingText(text, cb)``` 114 | Set the greeting message for new conversations. 115 | * ```text``` - The greeting text 116 | * ```cb``` - Optional, callback with arguments of ```err``` and ```result```. 117 | 118 | #### ```bot.setGetStartedButton(payload, cb)``` 119 | Set the Get Started button postback callback in the Welcome Screen. 120 | * ```payload``` - The postback callback to be called 121 | * ```cb``` - Optional, callback with arguments of ```err``` and ```result```. 122 | 123 | #### ```bot.setPersistentMenu(menuButtons, cb)``` 124 | Set the Persistent Menu which is always available to the user. 125 | * ```menuButtons``` - The menu button for the Persistent Menu 126 | * ```cb``` - Optional, callback with arguments of ```err``` and ```result```. 127 | ```js 128 | var menuButtons = [ 129 | { 130 | "type": "postback", 131 | "title": "Help", 132 | "payload": "DEVELOPER_DEFINED_PAYLOAD_FOR_HELP" 133 | }, 134 | { 135 | "type": "postback", 136 | "title": "Start a New Order", 137 | "payload": "DEVELOPER_DEFINED_PAYLOAD_FOR_START_ORDER" 138 | }, 139 | { 140 | "type": "web_url", 141 | "title": "View Website", 142 | "url": "http://petersapparel.parseapp.com/" 143 | } 144 | ]; 145 | bot.setPersistentMenu(menuButtons); 146 | ``` 147 | 148 | #### ```bot.sendTextMessage(userId, text, notificationType, cb)``` 149 | Send a text message to a specific user. 150 | * ```userId``` - The recipient's Facebook Id 151 | * ```text``` - The actual text 152 | * ```notificationType``` - Optional, push notification type: REGULAR (default), SILENT_PUSH, NO_PUSH 153 | * ```cb``` - Optional, callback with arguments of ```err``` and ```result```. 154 | 155 | #### ```bot.sendImageMessage(userId, imageUrl, notificationType, cb)``` 156 | Send an image message to a specific user. 157 | * ```userId``` - The recipient's Facebook Id 158 | * ```imageUrl``` - The url of the image 159 | * ```notificationType``` - Optional, push notification type: REGULAR (default), SILENT_PUSH, NO_PUSH 160 | * ```cb``` - Optional, callback with arguments of ```err``` and ```result```. 161 | 162 | #### ```bot.sendAudioAttachment(userId, audioUrl, notificationType, cb)``` 163 | Send an image message to a specific user. 164 | * ```userId``` - The recipient's Facebook Id 165 | * ```audioUrl``` - The url of the audio file 166 | * ```notificationType``` - Optional, push notification type: REGULAR (default), SILENT_PUSH, NO_PUSH 167 | * ```cb``` - Optional, callback with arguments of ```err``` and ```result```. 168 | 169 | #### ```bot.sendVideoAttachment(userId, videoUrl, notificationType, cb)``` 170 | Send an image message to a specific user. 171 | * ```userId``` - The recipient's Facebook Id 172 | * ```videoUrl``` - The url of the video file 173 | * ```notificationType``` - Optional, push notification type: REGULAR (default), SILENT_PUSH, NO_PUSH 174 | * ```cb``` - Optional, callback with arguments of ```err``` and ```result```. 175 | 176 | #### ```bot.sendFileAttachment(userId, fileUrl, notificationType, cb)``` 177 | Send an image message to a specific user. 178 | * ```userId``` - The recipient's Facebook Id 179 | * ```fileUrl``` - The url of the file 180 | * ```notificationType``` - Optional, push notification type: REGULAR (default), SILENT_PUSH, NO_PUSH 181 | * ```cb``` - Optional, callback with arguments of ```err``` and ```result```. 182 | 183 | #### ```bot.sendButtonMessage(userId, text, buttons, notificationType, cb)``` 184 | Send a message with buttons to a specific user. 185 | * ```userId``` - The recipient's Facebook Id 186 | * ```text``` - The title of the message 187 | * ```buttons``` - The buttons of the message 188 | * ```notificationType``` - Optional, push notification type: REGULAR (default), SILENT_PUSH, NO_PUSH 189 | * ```cb``` - Optional, callback with arguments of ```err``` and ```result```. 190 | 191 | ```js 192 | var text = "What do you want to do next?"; 193 | var buttons = [ 194 | { 195 | "type": "web_url", 196 | "url": "https://petersapparel.parseapp.com", 197 | "title": "Show Website" 198 | }, 199 | { 200 | "type": "postback", 201 | "title": "Start Chatting", 202 | "payload": "USER_DEFINED_PAYLOAD" 203 | } 204 | ]; 205 | bot.sendButtonMessage(recipient, text, buttons); 206 | ``` 207 | 208 | #### ```bot.sendGenericMessage(userId, elements, notificationType, cb)``` 209 | Send a message with bubbles(horizontal scroll cards) to a specific user. 210 | * ```userId``` - The recipient's Facebook Id 211 | * ```elements``` - The elements of the message 212 | * ```notificationType``` - Optional, push notification type: REGULAR (default), SILENT_PUSH, NO_PUSH 213 | * ```cb``` - Optional, callback with arguments of ```err``` and ```result```. 214 | 215 | ```js 216 | 217 | var elements = [ 218 | { 219 | "title": "Classic White T-Shirt", 220 | "image_url": "http://petersapparel.parseapp.com/img/item100-thumb.png", 221 | "subtitle": "Soft white cotton t-shirt is back in style", 222 | "buttons": [ 223 | { 224 | "type": "web_url", 225 | "url": "https://petersapparel.parseapp.com/view_item?item_id=100", 226 | "title": "View Item" 227 | }, 228 | { 229 | "type": "web_url", 230 | "url": "https://petersapparel.parseapp.com/buy_item?item_id=100", 231 | "title": "Buy Item" 232 | }, 233 | { 234 | "type": "postback", 235 | "title": "Bookmark Item", 236 | "payload": "USER_DEFINED_PAYLOAD_FOR_ITEM100" 237 | } 238 | ] 239 | }, 240 | { 241 | "title": "Classic Grey T-Shirt", 242 | "image_url": "http://petersapparel.parseapp.com/img/item101-thumb.png", 243 | "subtitle": "Soft gray cotton t-shirt is back in style", 244 | "buttons": [ 245 | { 246 | "type": "web_url", 247 | "url": "https://petersapparel.parseapp.com/view_item?item_id=101", 248 | "title": "View Item" 249 | }, 250 | { 251 | "type": "web_url", 252 | "url": "https://petersapparel.parseapp.com/buy_item?item_id=101", 253 | "title": "Buy Item" 254 | }, 255 | { 256 | "type": "postback", 257 | "title": "Bookmark Item", 258 | "payload": "USER_DEFINED_PAYLOAD_FOR_ITEM101" 259 | } 260 | ] 261 | } 262 | ]; 263 | 264 | bot.sendGenericMessage(recipient, elements, buttons); 265 | ``` 266 | 267 | #### ```bot.sendBubbleMessage(userId, elements, notificationType, cb)``` 268 | It is the same as ```sendGenericMessage()``` 269 | 270 | #### ```bot.sendReceiptMessage(userId, receipt, notificationType, cb)``` 271 | Send a message with receipt to a specific user. 272 | * ```userId``` - The recipient's Facebook Id 273 | * ```elements``` - The elements of the message 274 | * ```notificationType``` - Optional, push notification type: REGULAR (default), SILENT_PUSH, NO_PUSH 275 | * ```cb``` - Optional, callback with arguments of ```err``` and ```result```. 276 | 277 | ```js 278 | 279 | var receipt = { 280 | "recipient_name": "Stephane Crozatier", 281 | "order_number": "12345678902", 282 | "currency": "USD", 283 | "payment_method": "Visa 2345", 284 | "order_url": "http://petersapparel.parseapp.com/order?order_id=123456", 285 | "timestamp": "1428444852", 286 | "elements": [ 287 | { 288 | "title": "Classic White T-Shirt", 289 | "subtitle": "100% Soft and Luxurious Cotton", 290 | "quantity": 2, 291 | "price": 50, 292 | "currency": "USD", 293 | "image_url": "http://petersapparel.parseapp.com/img/whiteshirt.png" 294 | }, 295 | { 296 | "title": "Classic Gray T-Shirt", 297 | "subtitle": "100% Soft and Luxurious Cotton", 298 | "quantity": 1, 299 | "price": 25, 300 | "currency": "USD", 301 | "image_url": "http://petersapparel.parseapp.com/img/grayshirt.png" 302 | } 303 | ], 304 | "address": { 305 | "street_1": "1 Hacker Way", 306 | "street_2": "", 307 | "city": "Menlo Park", 308 | "postal_code": "94025", 309 | "state": "CA", 310 | "country": "US" 311 | }, 312 | "summary": { 313 | "subtotal": 75.00, 314 | "shipping_cost": 4.95, 315 | "total_tax": 6.19, 316 | "total_cost": 56.14 317 | }, 318 | "adjustments": [ 319 | { 320 | "name": "New Customer Discount", 321 | "amount": 20 322 | }, 323 | { 324 | "name": "$10 Off Coupon", 325 | "amount": 10 326 | } 327 | ] 328 | }; 329 | 330 | bot.sendReceiptMessage(recipient, receipt); 331 | ``` 332 | 333 | #### ```bot.sendLocationRequest(userId, text, notificationType, cb)``` 334 | Send a location request message to a specific user. 335 | * ```userId``` - The recipient's Facebook Id 336 | * ```text``` - The prompt text ask for location 337 | * ```notificationType``` - Optional, push notification type: REGULAR (default), SILENT_PUSH, NO_PUSH 338 | * ```cb``` - Optional, callback with arguments of ```err``` and ```result```. 339 | 340 | #### ```bot.sendQuickReplies(userId, text, replies, notificationType, cb)``` 341 | Send quick reply message to a specific user. 342 | * ```userId``` - The recipient's Facebook Id 343 | * ```text``` - The title of the message 344 | * ```replies``` - The quick reply buttons 345 | * ```notificationType``` - Optional, push notification type: REGULAR (default), SILENT_PUSH, NO_PUSH 346 | * ```cb``` - Optional, callback with arguments of ```err``` and ```result```. 347 | 348 | ```js 349 | var replies = [ 350 | { 351 | "content_type": "text", 352 | "title": "Good", 353 | "payload": "thumbs_up" 354 | }, 355 | { 356 | "content_type": "text", 357 | "title": "Bad", 358 | "payload": "thumbs_down" 359 | } 360 | ]; 361 | bot.sendQuickReplies(userId, message, replies); 362 | 363 | bot.on('quickreply', function (userId, payload) { 364 | console.log("payload =" + payload); 365 | }); 366 | 367 | ``` 368 | 369 | #### ```bot.sendListMessage(userId, elements, notificationType, cb)``` 370 | Send quick reply message to a specific user. 371 | * ```userId``` - The recipient's Facebook Id 372 | * ```elements``` - The list elements of the message 373 | * ```notificationType``` - Optional, push notification type: REGULAR (default), SILENT_PUSH, NO_PUSH 374 | * ```cb``` - Op 375 | 376 | ```js 377 | var elements = [ 378 | { 379 | "title": "Classic T-Shirt Collection", 380 | "image_url": "https://peterssendreceiveapp.ngrok.io/img/collection.png", 381 | "subtitle": "See all our colors", 382 | "default_action": { 383 | "type": "web_url", 384 | "url": "https://peterssendreceiveapp.ngrok.io/shop_collection", 385 | "messenger_extensions": true, 386 | "webview_height_ratio": "tall", 387 | "fallback_url": "https://peterssendreceiveapp.ngrok.io/" 388 | }, 389 | "buttons": [ 390 | { 391 | "title": "View", 392 | "type": "web_url", 393 | "url": "https://peterssendreceiveapp.ngrok.io/collection", 394 | "messenger_extensions": true, 395 | "webview_height_ratio": "tall", 396 | "fallback_url": "https://peterssendreceiveapp.ngrok.io/" 397 | } 398 | ] 399 | }, 400 | { 401 | "title": "Classic White T-Shirt", 402 | "image_url": "https://peterssendreceiveapp.ngrok.io/img/white-t-shirt.png", 403 | "subtitle": "100% Cotton, 200% Comfortable", 404 | "default_action": { 405 | "type": "web_url", 406 | "url": "https://peterssendreceiveapp.ngrok.io/view?item=100", 407 | "messenger_extensions": true, 408 | "webview_height_ratio": "tall", 409 | "fallback_url": "https://peterssendreceiveapp.ngrok.io/" 410 | }, 411 | "buttons": [ 412 | { 413 | "title": "Shop Now", 414 | "type": "web_url", 415 | "url": "https://peterssendreceiveapp.ngrok.io/shop?item=100", 416 | "messenger_extensions": true, 417 | "webview_height_ratio": "tall", 418 | "fallback_url": "https://peterssendreceiveapp.ngrok.io/" 419 | } 420 | ] 421 | }, 422 | { 423 | "title": "Classic Blue T-Shirt", 424 | "image_url": "https://peterssendreceiveapp.ngrok.io/img/blue-t-shirt.png", 425 | "subtitle": "100% Cotton, 200% Comfortable", 426 | "default_action": { 427 | "type": "web_url", 428 | "url": "https://peterssendreceiveapp.ngrok.io/view?item=101", 429 | "messenger_extensions": true, 430 | "webview_height_ratio": "tall", 431 | "fallback_url": "https://peterssendreceiveapp.ngrok.io/" 432 | }, 433 | "buttons": [ 434 | { 435 | "title": "Shop Now", 436 | "type": "web_url", 437 | "url": "https://peterssendreceiveapp.ngrok.io/shop?item=101", 438 | "messenger_extensions": true, 439 | "webview_height_ratio": "tall", 440 | "fallback_url": "https://peterssendreceiveapp.ngrok.io/" 441 | } 442 | ] 443 | }, 444 | { 445 | "title": "Classic Black T-Shirt", 446 | "image_url": "https://peterssendreceiveapp.ngrok.io/img/black-t-shirt.png", 447 | "subtitle": "100% Cotton, 200% Comfortable", 448 | "default_action": { 449 | "type": "web_url", 450 | "url": "https://peterssendreceiveapp.ngrok.io/view?item=102", 451 | "messenger_extensions": true, 452 | "webview_height_ratio": "tall", 453 | "fallback_url": "https://peterssendreceiveapp.ngrok.io/" 454 | }, 455 | "buttons": [ 456 | { 457 | "title": "Shop Now", 458 | "type": "web_url", 459 | "url": "https://peterssendreceiveapp.ngrok.io/shop?item=102", 460 | "messenger_extensions": true, 461 | "webview_height_ratio": "tall", 462 | "fallback_url": "https://peterssendreceiveapp.ngrok.io/" 463 | } 464 | ] 465 | } 466 | ]; 467 | bot.sendListMessage(userId, elements); 468 | 469 | ``` 470 | 471 | #### ```bot.getUserProfile(userId, cb)``` 472 | Get the Facebook profile from a specific user. 473 | * ```userId``` - The target's Facebook Id 474 | * ```cb``` - callback with arguments of ```err``` and ```profile```. 475 | 476 | ```js 477 | bot.getUserProfile(userId, function (err, profile) { 478 | console.log(profile); 479 | }); 480 | ``` 481 | 482 | #### ```bot.getBotProfile(fields, cb)``` 483 | Get the Messenger Profile of your bot. 484 | See detailed fields available in the [Facebook Profile API.](https://developers.facebook.com/docs/messenger-platform/reference/messenger-profile-api#profile_properties) 485 | * ```fields``` - Array of fields in string of properties requested 486 | * ```cb``` - callback with arguments of ```err``` and ```profile```. 487 | 488 | ```js 489 | bot.getUserProfile(['greeting', 'get_started'], function (err, profile) { 490 | console.log(profile); 491 | }); 492 | ``` -------------------------------------------------------------------------------- /example/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by mewkinni on 19/2/2017. 3 | */ 4 | var express = require('express'); 5 | var app = express(); 6 | 7 | var FBBotFramework = require('../'); 8 | 9 | // Initialize 10 | var bot = new FBBotFramework({ 11 | page_token: "THIS_IS_PAGE_TOKEN", 12 | verify_token: "THIS_IS_VERIFY_TOKEN" 13 | }); 14 | 15 | // Setup Express middleware for /webhook 16 | app.use('/webhook', bot.middleware()); 17 | 18 | // Setup listener for incoming messages 19 | bot.on('message', function (userId, message) { 20 | 21 | // Send text message 22 | // bot.sendTextMessage(userId, "Echo Message:" + message); 23 | 24 | // Send quick replies 25 | var replies = [ 26 | { 27 | "content_type": "text", 28 | "title": "👍", 29 | "payload": "thumbs_up" 30 | }, 31 | { 32 | "content_type": "text", 33 | "title": "👎", 34 | "payload": "thumbs_down" 35 | } 36 | ]; 37 | bot.sendQuickReplies(userId, message, replies); 38 | }); 39 | 40 | bot.on('quickreply', function (userId, payload) { 41 | bot.sendTextMessage(userId, "payload:" + payload); 42 | }); 43 | 44 | // Config the Get Started Button and register a callback 45 | bot.setGetStartedButton("GET_STARTED"); 46 | bot.on('postback', function (userId, payload) { 47 | 48 | if (payload == "GET_STARTED") { 49 | getStarted(userId); 50 | } 51 | 52 | // Other postback callbacks here 53 | // ... 54 | 55 | }); 56 | 57 | function getStarted(userId) { 58 | 59 | // Get started process 60 | } 61 | 62 | // Setup listener for attachment 63 | bot.on('attachment', function (userId, attachment) { 64 | 65 | // Echo the audio attachment 66 | if (attachment[0].type == "audio") { 67 | bot.sendAudioAttachment(userId, attachment[0].payload.url); 68 | } 69 | 70 | }); 71 | 72 | // Make Express listening 73 | app.listen(3000); -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by kinnimew on 26/4/16. 3 | */ 4 | var util = require('util'); 5 | var EventEmitter = require('events').EventEmitter; 6 | 7 | var request = require("request"); 8 | 9 | const FB_MESSENGER_ENDPOINT = "https://graph.facebook.com/v2.6/me/messages"; 10 | const FB_PROFILE_ENDPOINT = "https://graph.facebook.com/v2.6/"; 11 | const FB_SETTINGS_ENDPOINT = "https://graph.facebook.com/v2.6/me/thread_settings"; 12 | 13 | const NOTIFICATION_TYPE = { 14 | REGULAR: "REGULAR", 15 | SILENT_PUSH: "SILENT_PUSH", 16 | NO_PUSH: "NO_PUSH" 17 | }; 18 | 19 | function FBBotFramework(options, cb) { 20 | 21 | if (!options || !options.page_token) { 22 | 23 | var error = new Error("Page Access Token missing. See FB documentation for details: https://developers.facebook.com/docs/messenger-platform/quickstart"); 24 | 25 | if (typeof cb === "function") { 26 | return cb(error) 27 | } 28 | 29 | throw error; 30 | } 31 | 32 | this.page_token = options.page_token; 33 | this.verify_token = options.verify_token; 34 | this.commands = []; 35 | 36 | if (cb) cb(null); 37 | 38 | } 39 | 40 | // Setup 41 | util.inherits(FBBotFramework, EventEmitter); 42 | 43 | FBBotFramework.NOTIFICATION_TYPE = NOTIFICATION_TYPE; 44 | 45 | FBBotFramework.prototype.verify = function (req, res) { 46 | if (req.query['hub.verify_token'] === this.verify_token) { 47 | res.send(req.query['hub.challenge']); 48 | } else { 49 | res.status(500).send('Error, wrong validation token'); 50 | } 51 | }; 52 | 53 | // Send API, Details please visit https://developers.facebook.com/docs/messenger-platform/send-api-reference#request 54 | 55 | FBBotFramework.prototype.send = function (recipient, messageData, notificationType, cb) { 56 | notificationType = notificationType || NOTIFICATION_TYPE.REGULAR; 57 | 58 | if (typeof notificationType === 'function') { 59 | cb = notificationType; 60 | notificationType = NOTIFICATION_TYPE.REGULAR 61 | } 62 | 63 | var req = { 64 | url: FB_MESSENGER_ENDPOINT, 65 | qs: {access_token: this.page_token}, 66 | method: "POST", 67 | json: { 68 | recipient: {id: recipient}, 69 | message: messageData, 70 | notification_type: notificationType 71 | } 72 | }; 73 | 74 | request(req, function (err, res, body) { 75 | if (cb) { 76 | if (err) return cb(err); 77 | if (body.error) return cb(body.error); 78 | cb(null, body); 79 | } 80 | }); 81 | 82 | }; 83 | 84 | 85 | FBBotFramework.prototype.sendTextMessage = function (recipient, text, notificationType, cb) { 86 | var messageData = {text: text}; 87 | this.send(recipient, messageData, notificationType, cb); 88 | }; 89 | 90 | FBBotFramework.prototype.sendAudioAttachment = function (recipient, audioUrl, notificationType, cb) { 91 | var messageData = { 92 | attachment: { 93 | type: "audio", 94 | payload: {url: audioUrl} 95 | } 96 | }; 97 | 98 | this.send(recipient, messageData, notificationType, cb); 99 | }; 100 | 101 | FBBotFramework.prototype.sendVideoAttachment = function (recipient, videoUrl, notificationType, cb) { 102 | var messageData = { 103 | attachment: { 104 | type: "file", 105 | payload: {url: videoUrl} 106 | } 107 | }; 108 | 109 | this.send(recipient, messageData, notificationType, cb); 110 | }; 111 | 112 | FBBotFramework.prototype.sendFileAttachment = function (recipient, fileUrl, notificationType, cb) { 113 | var messageData = { 114 | attachment: { 115 | type: "video", 116 | payload: {url: fileUrl} 117 | } 118 | }; 119 | 120 | this.send(recipient, messageData, notificationType, cb); 121 | }; 122 | 123 | // TODO: Audio, Video and File Upload 124 | 125 | FBBotFramework.prototype.sendImageMessage = function (recipient, imageUrl, notificationType, cb) { 126 | var messageData = { 127 | attachment: { 128 | type: "image", 129 | payload: {url: imageUrl} 130 | } 131 | }; 132 | 133 | this.send(recipient, messageData, notificationType, cb); 134 | }; 135 | 136 | FBBotFramework.prototype.sendButtonMessage = function (recipient, text, buttons, notificationType, cb) { 137 | 138 | var messageData = { 139 | attachment: { 140 | type: "template", 141 | payload: { 142 | template_type: "button", 143 | text: text, 144 | buttons: buttons 145 | } 146 | } 147 | }; 148 | 149 | this.send(recipient, messageData, notificationType, cb); 150 | }; 151 | 152 | 153 | // Limitation 154 | // Title: 45 characters 155 | // Subtitle: 80 characters 156 | // Call-to-action title: 20 characters 157 | // Call-to-action items: 3 buttons 158 | // Bubbles per message (horizontal scroll): 10 elements 159 | 160 | FBBotFramework.prototype.sendBubbleMessage = FBBotFramework.prototype.sendGenericMessage = function (recipient, elements, notificationType, cb) { 161 | var messageData = { 162 | attachment: { 163 | type: "template", 164 | payload: { 165 | template_type: "generic", 166 | elements: elements 167 | } 168 | } 169 | }; 170 | 171 | this.send(recipient, messageData, notificationType, cb); 172 | 173 | }; 174 | 175 | FBBotFramework.prototype.sendReceiptMessage = function (recipient, receipt, notificationType, cb) { 176 | 177 | if (!receipt.template_type) { 178 | receipt.template_type = "receipt"; 179 | } 180 | 181 | var messageData = { 182 | "attachment": { 183 | "type": "template", 184 | "payload": receipt 185 | } 186 | }; 187 | 188 | this.send(recipient, messageData, notificationType, cb); 189 | }; 190 | 191 | FBBotFramework.prototype.getUserProfile = function (userId, cb) { 192 | 193 | var req = { 194 | method: "GET", 195 | uri: FB_PROFILE_ENDPOINT + userId, 196 | qs: { 197 | fields: 'first_name,last_name,profile_pic,locale,timezone,gender', 198 | access_token: this.page_token 199 | }, 200 | json: true 201 | }; 202 | 203 | request(req, function (err, res, body) { 204 | if (err) return cb(err); 205 | if (body.error) return cb(body.error); 206 | cb(null, body); 207 | }); 208 | }; 209 | 210 | // Bot Profile 211 | FBBotFramework.prototype.getBotProfile = function (fields, cb) { 212 | 213 | var req = { 214 | method: 'GET', 215 | uri: FB_PROFILE_ENDPOINT + 'messenger_profile', 216 | qs: { 217 | fields: fields.join(), 218 | access_token: this.page_token 219 | }, 220 | json: true 221 | }; 222 | 223 | request(req, function (err, res, body) { 224 | if (err) return cb(err); 225 | if (body.error) return cb(body.err); 226 | cb(null, body); 227 | }) 228 | }; 229 | 230 | // Middleware 231 | FBBotFramework.prototype.middleware = function () { 232 | 233 | var bot = this; 234 | 235 | return function (req, res) { 236 | if (req.method === 'GET') { 237 | return bot.verify(req, res); 238 | } 239 | 240 | if (req.method === 'POST') { 241 | 242 | // Read data from the request 243 | var data = ''; 244 | req.setEncoding('utf8'); 245 | req.on('data', function (chunk) { 246 | data += chunk; 247 | }); 248 | 249 | req.on('end', function () { 250 | 251 | // Always return HTTP200 to Facebook's POST Request 252 | res.send({}); 253 | 254 | var messageData = JSON.parse(data); 255 | var messagingEvent = messageData.entry[0].messaging; 256 | messagingEvent.forEach(function (event) { 257 | 258 | // Extract senderID, i.e. recipient 259 | var sender = event.sender.id; 260 | 261 | // Trigger onEcho Listener 262 | if (event.message && event.message.is_echo) { 263 | return bot.emit('echo', event.recipient.id, event.message.text); 264 | } 265 | 266 | // Trigger quickyReply Listener 267 | if (event.message && event.message.quick_reply) { 268 | return bot.emit('quickreply', sender, event.message.quick_reply.payload); 269 | } 270 | 271 | // Trigger onMessage Listener 272 | if (event.message && event.message.text) { 273 | bot.emit('message', sender, event.message.text); 274 | } 275 | 276 | // Trigger onPostback Listener 277 | if (event.postback && event.postback.payload) { 278 | bot.emit('postback', sender, event.postback.payload); 279 | } 280 | 281 | // Trigger onAttachment Listener 282 | if (event.message && event.message.attachments) { 283 | bot.emit('attachment', sender, event.message.attachments); 284 | } 285 | 286 | }); 287 | }); 288 | 289 | } 290 | }; 291 | }; 292 | 293 | 294 | FBBotFramework.prototype.setGreetingText = function (text, cb) { 295 | 296 | 297 | var req = { 298 | url: FB_SETTINGS_ENDPOINT, 299 | qs: {access_token: this.page_token}, 300 | method: "POST", 301 | json: { 302 | "setting_type": "greeting", 303 | "greeting": { 304 | "text": text 305 | } 306 | } 307 | }; 308 | 309 | request(req, function (err, res, body) { 310 | if (cb) { 311 | if (err) return cb(err); 312 | if (body.error) return cb(body.error); 313 | cb(null, body); 314 | } 315 | }); 316 | }; 317 | 318 | FBBotFramework.prototype.setGetStartedButton = function (payload, cb) { 319 | var req = { 320 | url: FB_SETTINGS_ENDPOINT, 321 | qs: {access_token: this.page_token}, 322 | method: "POST", 323 | json: { 324 | "setting_type": "call_to_actions", 325 | "thread_state": "new_thread", 326 | "call_to_actions": [ 327 | { 328 | "payload": payload 329 | } 330 | ] 331 | } 332 | }; 333 | 334 | request(req, function (err, res, body) { 335 | if (cb) { 336 | if (err) return cb(err); 337 | if (body.error) return cb(body.error); 338 | cb(null, body); 339 | } 340 | }); 341 | }; 342 | 343 | FBBotFramework.prototype.setPersistentMenu = function (menuButtons, cb) { 344 | var req = { 345 | url: FB_SETTINGS_ENDPOINT, 346 | qs: {access_token: this.page_token}, 347 | method: "POST", 348 | json: { 349 | "setting_type": "call_to_actions", 350 | "thread_state": "existing_thread", 351 | "call_to_actions": menuButtons 352 | } 353 | }; 354 | 355 | request(req, function (err, res, body) { 356 | if (cb) { 357 | if (err) return cb(err); 358 | if (body.error) return cb(body.error); 359 | cb(null, body); 360 | } 361 | }); 362 | }; 363 | 364 | 365 | FBBotFramework.prototype.sendQuickReplies = function (recipient, text, replies, notificationType, cb) { 366 | var messageData = { 367 | text: text, 368 | quick_replies: replies 369 | }; 370 | 371 | this.send(recipient, messageData, notificationType, cb); 372 | 373 | }; 374 | 375 | FBBotFramework.prototype.sendLocationRequest = function (recipient, text, notificationType, cb) { 376 | var messageData = { 377 | text: text, 378 | quick_replies: [{content_type: "location"}] 379 | }; 380 | 381 | this.send(recipient, messageData, notificationType, cb); 382 | 383 | }; 384 | 385 | FBBotFramework.prototype.sendListMessage = function (recipient, elements, notificationType, cb) { 386 | 387 | var messageData = { 388 | "attachment": { 389 | "type": "template", 390 | "payload": { 391 | "template_type": "list", 392 | "top_element_style": "compact", 393 | "elements": elements 394 | } 395 | } 396 | }; 397 | 398 | this.send(recipient, messageData, notificationType, cb); 399 | 400 | 401 | }; 402 | 403 | 404 | module.exports = FBBotFramework; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fb-bot-framework", 3 | "version": "0.3.0", 4 | "description": "A node framework for Facebook Messenger Platform", 5 | "main": "index.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "git+https://github.com/kinni/fb-bot-framework.git" 9 | }, 10 | "keywords": [ 11 | "fb", 12 | "facebook", 13 | "messenger", 14 | "messanger", 15 | "bot", 16 | "bots", 17 | "framework" 18 | ], 19 | "author": "Kinni Mew", 20 | "license": "MIT", 21 | "dependencies": { 22 | "request": "^2.72.0" 23 | }, 24 | "devDependencies": { 25 | "chai": "^3.5.0", 26 | "express": "^4.14.1", 27 | "nock": "^8.0.0", 28 | "sinon": "^1.17.4" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /test/test.basic.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by kinnimew on 30/4/16. 3 | */ 4 | var FBBotFramework = require('../'); 5 | 6 | var express = require('express'); 7 | var request = require('request'); 8 | var chai = require('chai'); 9 | var expect = chai.expect; 10 | var nock = require('nock'); 11 | 12 | var sinon = require('sinon'); 13 | 14 | describe('Initialize', function () { 15 | 16 | it('should initialize correctly if token is supplied', function () { 17 | var bot = new FBBotFramework({page_token: "DUMMY_PAGE_TOKEN", verify_token: "DUMMY_VERIFY_TOKEN"}, function (err) { 18 | expect(err).to.be.a('null'); 19 | }); 20 | }); 21 | 22 | it('should throw error if token is missing', function () { 23 | 24 | expect(function () { 25 | var bot = new FBBotFramework(); 26 | }).to.throw(Error); 27 | 28 | }); 29 | 30 | }); 31 | 32 | describe('Middleware', function () { 33 | 34 | var server; 35 | var bot; 36 | var app = express(); 37 | 38 | before(function (done) { 39 | bot = new FBBotFramework({page_token: "DUMMY_PAGE_TOKEN", verify_token: "DUMMY_VERIFY_TOKEN"}); 40 | app.use('/webhook', bot.middleware()); 41 | 42 | server = app.listen(3000, function () { 43 | done(); 44 | }) 45 | }); 46 | 47 | after(function (done) { 48 | server.close(function () { 49 | done(); 50 | }); 51 | }); 52 | 53 | 54 | it('GET /webhook should return hub.challenge value', function (done) { 55 | 56 | var hub = { 57 | verify_token: bot.verify_token, 58 | challenge: 'bar' 59 | }; 60 | 61 | var req = { 62 | url: 'http://localhost:3000/webhook', 63 | method: 'GET', 64 | qs: { 65 | 'hub.verify_token': hub.verify_token, 66 | 'hub.challenge': hub.challenge 67 | } 68 | }; 69 | 70 | request(req, function (err, res, body) { 71 | expect(err).to.be.null; 72 | expect(res.statusCode).equal(200); 73 | expect(body).equal(hub.challenge); 74 | done(); 75 | }) 76 | 77 | }); 78 | 79 | it('POST /webhook should return HTTP200', function (done) { 80 | 81 | var body = { 82 | "object": "page", 83 | "entry": [{ 84 | "id": 874297639348115, 85 | "time": 1462071915462, 86 | "messaging": [{ 87 | "sender": {"id": 1030608157033272}, 88 | "recipient": {"id": 874297639348115}, 89 | "timestamp": 1462071915446, 90 | "message": {"mid": "mid.1462071915439:2db22192d03e67b801", "seq": 317, "text": "Hello World!"} 91 | }] 92 | }] 93 | }; 94 | 95 | var req = { 96 | url: 'http://localhost:3000/webhook', 97 | method: 'POST', 98 | body: body, 99 | json: true 100 | }; 101 | 102 | request(req, function (err, res, body) { 103 | expect(err).to.be.null; 104 | expect(res.statusCode).equal(200); 105 | done(); 106 | }) 107 | }) 108 | }); 109 | 110 | describe('Event Handlers', function () { 111 | 112 | var server; 113 | var bot; 114 | var app = express(); 115 | 116 | var onMessageListener = sinon.spy(); 117 | var onPostbackListener = sinon.spy(); 118 | 119 | before(function (done) { 120 | bot = new FBBotFramework({page_token: "DUMMY_PAGE_TOKEN", verify_token: "DUMMY_VERIFY_TOKEN"}); 121 | 122 | bot.on('message', onMessageListener); 123 | bot.on('postback', onPostbackListener); 124 | 125 | app.use('/webhook', bot.middleware()); 126 | server = app.listen(3000, function () { 127 | done(); 128 | }); 129 | }); 130 | 131 | it('should trigger message event', function (done) { 132 | 133 | var senderId = 1030608157033272; 134 | var text = "Hello World!"; 135 | 136 | var body = { 137 | "object": "page", 138 | "entry": [{ 139 | "id": 874297639348115, 140 | "time": 1462071915462, 141 | "messaging": [{ 142 | "sender": {"id": senderId}, 143 | "recipient": {"id": 874297639348115}, 144 | "timestamp": 1462071915446, 145 | "message": {"mid": "mid.1462071915439:2db22192d03e67b801", "seq": 318, "text": text} 146 | }] 147 | }] 148 | }; 149 | 150 | var req = { 151 | url: 'http://localhost:3000/webhook', 152 | method: 'POST', 153 | body: body, 154 | json: true 155 | }; 156 | 157 | request(req, function (err, res, body) { 158 | expect(onMessageListener.called).to.be.true; 159 | expect(onMessageListener.args[0][0]).equal(senderId); 160 | expect(onMessageListener.args[0][1]).equal(text); 161 | done(); 162 | }); 163 | 164 | }); 165 | 166 | it('should trigger Postback event', function (done) { 167 | 168 | var senderId = "1030608157033272"; 169 | var payload = "DUMMY_PAYLOAD"; 170 | 171 | var body = { 172 | "object": "page", 173 | "entry": [{ 174 | "id": 874297639348115, 175 | "time": 1462071915462, 176 | "messaging": [{ 177 | "sender": {"id": senderId}, 178 | "recipient": {"id": 874297639348115}, 179 | "timestamp": 1462071915446, 180 | "postback": {"payload": payload} 181 | }] 182 | }] 183 | }; 184 | 185 | var req = { 186 | url: 'http://localhost:3000/webhook', 187 | method: 'POST', 188 | body: body, 189 | json: true 190 | }; 191 | 192 | request(req, function (err, res, body) { 193 | expect(onPostbackListener.called).to.be.true; 194 | expect(onPostbackListener.args[0][0]).equal(senderId); 195 | expect(onPostbackListener.args[0][1]).equal(payload); 196 | done(); 197 | }); 198 | 199 | 200 | }); 201 | }); -------------------------------------------------------------------------------- /test/test.send.api.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by kinnimew on 30/4/16. 3 | */ 4 | var FBBotFramework = require('../'); 5 | 6 | var nock = require('nock'); 7 | var expect = require('chai').expect; 8 | 9 | describe('Send API', function () { 10 | 11 | 12 | var bot; 13 | var recipient = 1; 14 | var dummyResponse = { 15 | recipient_id: '1030608157033270', 16 | message_id: 'mid.1462087493117:d0dcdd2121dce00540' 17 | }; 18 | 19 | before(function (done) { 20 | bot = new FBBotFramework({page_token: "DUMMY_PAGE_TOKEN", verify_token: "DUMMY_VERIFY_TOKEN"}, function (err) { 21 | done(); 22 | }) 23 | }); 24 | 25 | it('should send text message', function (done) { 26 | 27 | var text = "hello world!"; 28 | 29 | var payload = { 30 | recipient: {id: recipient}, 31 | message: {text: text} 32 | }; 33 | 34 | nock('https://graph.facebook.com') 35 | .post('/v2.6/me/messages', payload) 36 | .query({access_token: bot.page_token}) 37 | .reply(200, dummyResponse); 38 | 39 | bot.sendTextMessage(recipient, text, FBBotFramework.NOTIFICATION_TYPE.SILENT_PUSH, function (err, result) { 40 | expect(err).to.be.null; 41 | expect(result).to.deep.equal(dummyResponse); 42 | done(); 43 | }); 44 | 45 | }); 46 | 47 | it('should send image message', function (done) { 48 | 49 | var imageUrl = "https://petersapparel.com/img/shirt.png"; 50 | 51 | var payload = { 52 | "recipient": { 53 | "id": recipient 54 | }, 55 | "message": { 56 | "attachment": { 57 | "type": "image", 58 | "payload": { 59 | "url": "https://petersapparel.com/img/shirt.png" 60 | } 61 | } 62 | } 63 | }; 64 | 65 | 66 | nock('https://graph.facebook.com') 67 | .post('/v2.6/me/messages', payload) 68 | .query({access_token: bot.page_token}) 69 | .reply(200, dummyResponse); 70 | 71 | bot.sendImageMessage(recipient, imageUrl, function (err, result) { 72 | expect(err).to.be.null; 73 | expect(result).to.deep.equal(dummyResponse); 74 | done(); 75 | 76 | }); 77 | 78 | }); 79 | 80 | it('should send audio attachment', function (done) { 81 | 82 | var audioUrl = "https://petersapparel.com/img/shirt.png"; 83 | 84 | var payload = { 85 | "recipient": { 86 | "id": recipient 87 | }, 88 | "message": { 89 | "attachment": { 90 | "type": "audio", 91 | "payload": { 92 | "url": audioUrl 93 | } 94 | } 95 | } 96 | }; 97 | 98 | 99 | nock('https://graph.facebook.com') 100 | .post('/v2.6/me/messages', payload) 101 | .query({access_token: bot.page_token}) 102 | .reply(200, dummyResponse); 103 | 104 | bot.sendAudioAttachment(recipient, audioUrl, function (err, result) { 105 | expect(err).to.be.null; 106 | expect(result).to.deep.equal(dummyResponse); 107 | done(); 108 | 109 | }); 110 | 111 | }); 112 | 113 | it('should send video attachment', function (done) { 114 | 115 | var videoUrl = "https://petersapparel.com/bin/clip.mp4"; 116 | 117 | var payload = { 118 | "recipient": { 119 | "id": recipient 120 | }, 121 | "message": { 122 | "attachment": { 123 | "type": "audio", 124 | "payload": { 125 | "url": videoUrl 126 | } 127 | } 128 | } 129 | }; 130 | 131 | 132 | nock('https://graph.facebook.com') 133 | .post('/v2.6/me/messages', payload) 134 | .query({access_token: bot.page_token}) 135 | .reply(200, dummyResponse); 136 | 137 | bot.sendAudioAttachment(recipient, videoUrl, function (err, result) { 138 | expect(err).to.be.null; 139 | expect(result).to.deep.equal(dummyResponse); 140 | done(); 141 | 142 | }); 143 | 144 | }); 145 | 146 | it('should send file attachment', function (done) { 147 | 148 | var fileUrl = "https://petersapparel.com/bin/receipt.pdf"; 149 | 150 | var payload = { 151 | "recipient": { 152 | "id": recipient 153 | }, 154 | "message": { 155 | "attachment": { 156 | "type": "audio", 157 | "payload": { 158 | "url": fileUrl 159 | } 160 | } 161 | } 162 | }; 163 | 164 | 165 | nock('https://graph.facebook.com') 166 | .post('/v2.6/me/messages', payload) 167 | .query({access_token: bot.page_token}) 168 | .reply(200, dummyResponse); 169 | 170 | bot.sendAudioAttachment(recipient, fileUrl, function (err, result) { 171 | expect(err).to.be.null; 172 | expect(result).to.deep.equal(dummyResponse); 173 | done(); 174 | 175 | }); 176 | 177 | }); 178 | 179 | it('should send button message', function (done) { 180 | 181 | var text = "What do you want to do next?"; 182 | var buttons = [ 183 | { 184 | "type": "web_url", 185 | "url": "https://petersapparel.parseapp.com", 186 | "title": "Show Website" 187 | }, 188 | { 189 | "type": "postback", 190 | "title": "Start Chatting", 191 | "payload": "USER_DEFINED_PAYLOAD" 192 | } 193 | ]; 194 | 195 | var payload = { 196 | "recipient": { 197 | "id": recipient 198 | }, 199 | "message": { 200 | "attachment": { 201 | "type": "template", 202 | "payload": { 203 | "template_type": "button", 204 | "text": text, 205 | "buttons": buttons 206 | } 207 | } 208 | } 209 | }; 210 | 211 | nock('https://graph.facebook.com') 212 | .post('/v2.6/me/messages', payload) 213 | .query({access_token: bot.page_token}) 214 | .reply(200, dummyResponse); 215 | 216 | bot.sendButtonMessage(recipient, text, buttons, function (err, result) { 217 | expect(err).to.be.null; 218 | expect(result).to.deep.equal(dummyResponse); 219 | done(); 220 | 221 | }); 222 | 223 | }); 224 | 225 | it('should send generic/bubble message', function (done) { 226 | 227 | var elements = [ 228 | { 229 | "title": "Classic White T-Shirt", 230 | "image_url": "http://petersapparel.parseapp.com/img/item100-thumb.png", 231 | "subtitle": "Soft white cotton t-shirt is back in style", 232 | "buttons": [ 233 | { 234 | "type": "web_url", 235 | "url": "https://petersapparel.parseapp.com/view_item?item_id=100", 236 | "title": "View Item" 237 | }, 238 | { 239 | "type": "web_url", 240 | "url": "https://petersapparel.parseapp.com/buy_item?item_id=100", 241 | "title": "Buy Item" 242 | }, 243 | { 244 | "type": "postback", 245 | "title": "Bookmark Item", 246 | "payload": "USER_DEFINED_PAYLOAD_FOR_ITEM100" 247 | } 248 | ] 249 | }, 250 | { 251 | "title": "Classic Grey T-Shirt", 252 | "image_url": "http://petersapparel.parseapp.com/img/item101-thumb.png", 253 | "subtitle": "Soft gray cotton t-shirt is back in style", 254 | "buttons": [ 255 | { 256 | "type": "web_url", 257 | "url": "https://petersapparel.parseapp.com/view_item?item_id=101", 258 | "title": "View Item" 259 | }, 260 | { 261 | "type": "web_url", 262 | "url": "https://petersapparel.parseapp.com/buy_item?item_id=101", 263 | "title": "Buy Item" 264 | }, 265 | { 266 | "type": "postback", 267 | "title": "Bookmark Item", 268 | "payload": "USER_DEFINED_PAYLOAD_FOR_ITEM101" 269 | } 270 | ] 271 | } 272 | ]; 273 | 274 | var payload = { 275 | "recipient": { 276 | "id": recipient 277 | }, 278 | "message": { 279 | "attachment": { 280 | "type": "template", 281 | "payload": { 282 | "template_type": "generic", 283 | "elements": elements 284 | } 285 | } 286 | } 287 | }; 288 | 289 | nock('https://graph.facebook.com') 290 | .post('/v2.6/me/messages', payload) 291 | .query({access_token: bot.page_token}) 292 | .reply(200, dummyResponse); 293 | 294 | bot.sendBubbleMessage(recipient, elements, function (err, result) { 295 | expect(err).to.be.null; 296 | expect(result).to.deep.equal(dummyResponse); 297 | done(); 298 | 299 | }); 300 | 301 | }); 302 | 303 | it('should send receipt message', function (done) { 304 | 305 | var receipt = { 306 | "recipient_name": "Stephane Crozatier", 307 | "order_number": "12345678902", 308 | "currency": "USD", 309 | "payment_method": "Visa 2345", 310 | "order_url": "http://petersapparel.parseapp.com/order?order_id=123456", 311 | "timestamp": "1428444852", 312 | "elements": [ 313 | { 314 | "title": "Classic White T-Shirt", 315 | "subtitle": "100% Soft and Luxurious Cotton", 316 | "quantity": 2, 317 | "price": 50, 318 | "currency": "USD", 319 | "image_url": "http://petersapparel.parseapp.com/img/whiteshirt.png" 320 | }, 321 | { 322 | "title": "Classic Gray T-Shirt", 323 | "subtitle": "100% Soft and Luxurious Cotton", 324 | "quantity": 1, 325 | "price": 25, 326 | "currency": "USD", 327 | "image_url": "http://petersapparel.parseapp.com/img/grayshirt.png" 328 | } 329 | ], 330 | "address": { 331 | "street_1": "1 Hacker Way", 332 | "street_2": "", 333 | "city": "Menlo Park", 334 | "postal_code": "94025", 335 | "state": "CA", 336 | "country": "US" 337 | }, 338 | "summary": { 339 | "subtotal": 75.00, 340 | "shipping_cost": 4.95, 341 | "total_tax": 6.19, 342 | "total_cost": 56.14 343 | }, 344 | "adjustments": [ 345 | { 346 | "name": "New Customer Discount", 347 | "amount": 20 348 | }, 349 | { 350 | "name": "$10 Off Coupon", 351 | "amount": 10 352 | } 353 | ] 354 | }; 355 | 356 | var payload = { 357 | "recipient": { 358 | "id": recipient 359 | }, 360 | "message": { 361 | "attachment": { 362 | "type": "template", 363 | "payload": receipt 364 | } 365 | } 366 | }; 367 | 368 | nock('https://graph.facebook.com') 369 | .post('/v2.6/me/messages', payload) 370 | .query({access_token: bot.page_token}) 371 | .reply(200, dummyResponse); 372 | 373 | bot.sendReceiptMessage(recipient, receipt, function (err, result) { 374 | expect(err).to.be.null; 375 | expect(result).to.deep.equal(dummyResponse); 376 | done(); 377 | 378 | }); 379 | 380 | }); 381 | 382 | it('should return user\'s profile', function (done) { 383 | 384 | var response = { 385 | "first_name": "Peter", 386 | "last_name": "Chang", 387 | "profile_pic": "https://fbcdn-profile-a.akamaihd.net/hprofile-ak-xpf1/v/t1.0-1/p200x200/13055603_10105219398495383_8237637584159975445_n.jpg?oh=1d241d4b6d4dac50eaf9bb73288ea192&oe=57AF5C03&__gda__=1470213755_ab17c8c8e3a0a447fed3f272fa2179ce", 388 | "locale": "en_US", 389 | "timezone": -7, 390 | "gender": "male" 391 | }; 392 | 393 | nock('https://graph.facebook.com') 394 | .get('/v2.6/' + recipient) 395 | .query({fields: "first_name,last_name,profile_pic,locale,timezone,gender", access_token: bot.page_token}) 396 | .reply(200, response); 397 | 398 | bot.getUserProfile(recipient, function (err, profile) { 399 | expect(err).to.be.null; 400 | expect(profile).deep.equal(response); 401 | done(); 402 | }); 403 | 404 | }); 405 | 406 | it('should return bot\'s profile', function (done) { 407 | 408 | var response = { 409 | "data": [ 410 | { 411 | "get_started": { 412 | "payload": "GET_STARTED" 413 | } 414 | } 415 | ] 416 | }; 417 | 418 | nock('https://graph.facebook.com') 419 | .get('/v2.6/messenger_profile') 420 | .query({fields: 'get_started,greeting', access_token: bot.page_token}) 421 | .reply(200, response); 422 | 423 | bot.getBotProfile(['get_started', 'greeting'], function (err, profile) { 424 | expect(err).to.be.null; 425 | expect(profile).deep.equal(response); 426 | done(); 427 | }); 428 | 429 | }); 430 | 431 | }); -------------------------------------------------------------------------------- /test/test.thread.settings.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by kinnimew on 30/4/16. 3 | */ 4 | var FBBotFramework = require('../'); 5 | 6 | var nock = require('nock'); 7 | var expect = require('chai').expect; 8 | 9 | describe('Thread Settings APIs', function () { 10 | 11 | 12 | var bot; 13 | var recipient = 1; 14 | var dummyResponse = { 15 | recipient_id: '1030608157033270', 16 | message_id: 'mid.1462087493117:d0dcdd2121dce00540' 17 | }; 18 | 19 | before(function (done) { 20 | bot = new FBBotFramework({page_token: "DUMMY_PAGE_TOKEN", verify_token: "DUMMY_VERIFY_TOKEN"}, function (err) { 21 | done(); 22 | }) 23 | }); 24 | 25 | it('should set the Greeting Text', function (done) { 26 | 27 | var text = "hello world!"; 28 | 29 | var payload = { 30 | setting_type: "greeting", 31 | greeting: {text: text} 32 | }; 33 | 34 | nock('https://graph.facebook.com') 35 | .post('/v2.6/me/thread_settings', payload) 36 | .query({access_token: bot.page_token}) 37 | .reply(200, dummyResponse); 38 | 39 | bot.setGreetingText(text, function (err, result) { 40 | expect(err).to.be.null; 41 | expect(result).to.deep.equal(dummyResponse); 42 | done(); 43 | }); 44 | 45 | }); 46 | 47 | it('should set the Get Started Button', function (done) { 48 | 49 | var successfulResponse = { 50 | "result": "Successfully added new_thread's CTAs" 51 | }; 52 | 53 | var getStarted = "GET_STARTED"; 54 | 55 | var payload = { 56 | "setting_type": "call_to_actions", 57 | "thread_state": "new_thread", 58 | "call_to_actions": [ 59 | { 60 | "payload": getStarted 61 | } 62 | ] 63 | }; 64 | 65 | 66 | nock('https://graph.facebook.com') 67 | .post('/v2.6/me/thread_settings', payload) 68 | .query({access_token: bot.page_token}) 69 | .reply(200, successfulResponse); 70 | 71 | 72 | bot.setGetStartedButton(getStarted, function (err, result) { 73 | expect(err).to.be.null; 74 | expect(result).to.deep.equal(successfulResponse); 75 | done(); 76 | 77 | }); 78 | 79 | }); 80 | 81 | it('should set the Persistent Menu', function (done) { 82 | 83 | var successfulResponse = { 84 | "result": "Successfully added new_thread's CTAs" 85 | }; 86 | 87 | var menuButtons = [ 88 | { 89 | "type": "postback", 90 | "title": "Help", 91 | "payload": "DEVELOPER_DEFINED_PAYLOAD_FOR_HELP" 92 | }, 93 | { 94 | "type": "postback", 95 | "title": "Start a New Order", 96 | "payload": "DEVELOPER_DEFINED_PAYLOAD_FOR_START_ORDER" 97 | }, 98 | { 99 | "type": "web_url", 100 | "title": "View Website", 101 | "url": "http://petersapparel.parseapp.com/" 102 | } 103 | ]; 104 | 105 | var payload = { 106 | "setting_type": "call_to_actions", 107 | "thread_state": "existing_thread", 108 | "call_to_actions": menuButtons 109 | }; 110 | 111 | 112 | nock('https://graph.facebook.com') 113 | .post('/v2.6/me/thread_settings', payload) 114 | .query({access_token: bot.page_token}) 115 | .reply(200, successfulResponse); 116 | 117 | 118 | bot.setPersistentMenu(menuButtons, function (err, result) { 119 | expect(err).to.be.null; 120 | expect(result).to.deep.equal(successfulResponse); 121 | done(); 122 | 123 | }); 124 | 125 | }); 126 | 127 | 128 | }); --------------------------------------------------------------------------------