├── .travis.yml ├── .gitignore ├── nitrous.json ├── example ├── echo.js └── echo-express.js ├── test ├── get-profile.js ├── send-message.js ├── middleware-get.js ├── basic.js ├── remove-field-settings.js ├── set-field-settings.js ├── sender-actions.js └── middleware-post.js ├── CHANGELOG.md ├── package.json ├── LICENSE.md ├── CONTRIBUTING.md ├── CODE_OF_CONDUCT.md ├── index.js └── README.md /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '6' 4 | - '8' 5 | - '10' 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | .nyc_output/ 4 | coverage/ 5 | npm-debug.log 6 | .idea 7 | -------------------------------------------------------------------------------- /nitrous.json: -------------------------------------------------------------------------------- 1 | { 2 | "template": "nodejs", 3 | "ports": [3000], 4 | "name": "messenger-bot", 5 | "description": "A Node client for the Facebook Messenger Platform", 6 | "scripts": { 7 | "post-create": "echo 'running npm install - this might take awhile...' && npm install && sed -i 's/listen(3000)/listen(3000, \"0.0.0.0\")/g' example/echo.js", 8 | "Start Messenger Echo Bot": "cd ~/code/messenger-bot && node example/echo.js" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /example/echo.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const http = require('http') 3 | const Bot = require('../') 4 | 5 | let bot = new Bot({ 6 | token: 'PAGE_TOKEN', 7 | verify: 'VERIFY_TOKEN', 8 | app_secret: 'APP_SECRET' 9 | }) 10 | 11 | bot.on('error', (err) => { 12 | console.log(err.message) 13 | }) 14 | 15 | bot.on('message', (payload, reply) => { 16 | let text = payload.message.text 17 | 18 | bot.getProfile(payload.sender.id, (err, profile) => { 19 | if (err) throw err 20 | 21 | reply({ text }, (err) => { 22 | if (err) throw err 23 | 24 | console.log(`Echoed back to ${profile.first_name} ${profile.last_name}: ${text}`) 25 | }) 26 | }) 27 | }) 28 | 29 | http.createServer(bot.middleware()).listen(3000) 30 | console.log('Echo bot server running at port 3000.') 31 | -------------------------------------------------------------------------------- /test/get-profile.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const tap = require('tap') 3 | const nock = require('nock') 4 | const Bot = require('../') 5 | 6 | tap.test('bot.getProfile() - successful request', (t) => { 7 | let bot = new Bot({ 8 | token: 'foo' 9 | }) 10 | 11 | let response = { 12 | first_name: 'Cool', 13 | last_name: 'Kid', 14 | profile_pic: 'url', 15 | locale: 'en', 16 | timezone: 'PST', 17 | gender: 'M' 18 | } 19 | 20 | nock('https://graph.facebook.com').get('/v2.12/1').query({ 21 | fields: 'first_name,last_name,profile_pic,locale,timezone,gender', 22 | access_token: 'foo' 23 | }).reply(200, response) 24 | 25 | return bot.getProfile(1, (err, profile) => { 26 | t.error(err, 'response should not be error') 27 | t.deepEquals(profile, response, 'response is correct') 28 | t.end() 29 | }).catch(t.threw) 30 | }) 31 | -------------------------------------------------------------------------------- /test/send-message.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const tap = require('tap') 3 | const nock = require('nock') 4 | const Bot = require('../') 5 | 6 | tap.test('bot.getProfile() - successful request', (t) => { 7 | let bot = new Bot({ 8 | token: 'foo' 9 | }) 10 | 11 | let payload = { 12 | recipient: { id: 1 }, 13 | message: { text: 'hello!' } 14 | } 15 | 16 | let response = { 17 | recipient_id: '1008372609250235', 18 | message_id: 'mid.1456970487936:c34767dfe57ee6e339' 19 | } 20 | 21 | nock('https://graph.facebook.com').post('/v2.12/me/messages', payload).query({ 22 | access_token: 'foo' 23 | }).reply(200, response) 24 | 25 | return bot.sendMessage(1, payload.message, (err, body) => { 26 | t.error(err, 'response should not be error') 27 | t.deepEquals(body, response, 'response is correct') 28 | t.end() 29 | }).catch(t.threw) 30 | }) 31 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 2.4.0 4 | 5 | Added support for new Messenger Platform features (#26). This includes: 6 | * `bot.setGetStartedButton` 7 | * `bot.setPersistentMenu` 8 | * `bot.sendSenderAction` 9 | 10 | See the README for full documentation. 11 | 12 | ## 2.0.0 13 | 14 | * [BREAKING] Removed `bot.verify()` middleware. Instead, specify the option `verify` when creating the bot instance, and the middleware will be automatically set up. Example: 15 | ```js 16 | let bot = new Bot({ 17 | token: 'PAGE_TOKEN', 18 | verify: 'VERIFY_TOKEN' 19 | }) 20 | ``` 21 | * [BREAKING] Added `error` event to bot instance. This will require you to create a `bot.on('error')` handler, otherwise Node will throw the error instead. 22 | * Add message integrity checking. Specify your app's secret as `app_secret` in the options of the bot instance. You can find this on your Facebook app's dashboard. 23 | * Add testing for all functions. 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "messenger-bot", 3 | "version": "2.5.0", 4 | "description": "FB Messenger Platform client", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "standard && tap test/*.js --cov" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/remixz/messenger-bot.git" 12 | }, 13 | "keywords": [ 14 | "messenger", 15 | "api", 16 | "client", 17 | "bot" 18 | ], 19 | "author": "Zach Bruggeman , Thomas Stig Jacobsen ", 20 | "license": "MIT", 21 | "bugs": { 22 | "url": "https://github.com/remixz/messenger-bot/issues" 23 | }, 24 | "homepage": "https://github.com/remixz/messenger-bot#readme", 25 | "dependencies": { 26 | "request": "^2.71.0", 27 | "request-promise": "^4.1.1" 28 | }, 29 | "devDependencies": { 30 | "nock": "^9.0.0", 31 | "standard": "^11.0.1", 32 | "tap": "^12.0.0" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Zach Bruggeman and contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /example/echo-express.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const http = require('http') 3 | const express = require('express') 4 | const bodyParser = require('body-parser') 5 | const Bot = require('../') 6 | 7 | let bot = new Bot({ 8 | token: 'PAGE_TOKEN', 9 | verify: 'VERIFY_TOKEN', 10 | app_secret: 'APP_SECRET' 11 | }) 12 | 13 | bot.on('error', (err) => { 14 | console.log(err.message) 15 | }) 16 | 17 | bot.on('message', (payload, reply) => { 18 | let text = payload.message.text 19 | 20 | bot.getProfile(payload.sender.id, (err, profile) => { 21 | if (err) throw err 22 | 23 | reply({ text }, (err) => { 24 | if (err) throw JSON.stringify(err) 25 | 26 | console.log(`Echoed back to ${profile.first_name} ${profile.last_name}: ${text}`) 27 | }) 28 | }) 29 | }) 30 | 31 | let app = express() 32 | 33 | app.use(bodyParser.json()) 34 | app.use(bodyParser.urlencoded({ 35 | extended: true 36 | })) 37 | 38 | app.get('/', (req, res) => { 39 | return bot._verify(req, res) 40 | }) 41 | 42 | app.post('/', (req, res) => { 43 | bot._handleMessage(req.body) 44 | res.end(JSON.stringify({status: 'ok'})) 45 | }) 46 | 47 | http.createServer(app).listen(3000) 48 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Righteous! I'm happy that you want to contribute. :smile: 4 | 5 | * Make sure that you're read and understand the [Code of Conduct](CODE_OF_CONDUCT.md). 6 | 7 | ## messenger-bot is an [OPEN Open Source Project](http://openopensource.org/) 8 | 9 | ### What? 10 | 11 | Individuals making significant and valuable contributions are given 12 | commit-access to the project to contribute as they see fit. This project 13 | is more like an open wiki than a standard guarded open source project. 14 | 15 | ### Rules 16 | 17 | There are a few basic ground-rules for contributors: 18 | 19 | 1. **No `--force` pushes** or modifying the Git history in any way. *(Exception: I use `git am -3` sometimes to clean up pull requests, and then commit them to the repo.)* 20 | 2. **Non-master branches** ought to be used for ongoing work. 21 | 3. **External API changes and significant modifications** ought to be subject to an **internal pull-request** to solicit feedback from other contributors. 22 | 4. Internal pull-requests to solicit feedback are *encouraged* for any other non-trivial contribution but left to the discretion of the contributor. 23 | 5. Contributors should adhere to the [JavaScript Standard code-style](https://github.com/feross/standard). 24 | 25 | ### Releases 26 | 27 | Declaring formal releases remains the prerogative of the project maintainer. 28 | 29 | ### Changes to this arrangement 30 | 31 | This is an experiment and feedback is welcome! This document may also be 32 | subject to pull-requests or changes by contributors where you believe 33 | you have something valuable to add or change. 34 | 35 | Get a copy of this manifesto as [markdown](https://raw.githubusercontent.com/openopensource/openopensource.github.io/master/Readme.md) and use it in your own projects. 36 | -------------------------------------------------------------------------------- /test/middleware-get.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const tap = require('tap') 3 | const http = require('http') 4 | const request = require('request') 5 | const Bot = require('../') 6 | 7 | tap.test('GET webhook with correct verify token', (t) => { 8 | let bot = new Bot({ 9 | token: 'foo', 10 | verify: '$ecr3ts!' 11 | }) 12 | 13 | bot.on('error', (err) => { 14 | t.error(err, 'bot instance returned error') 15 | t.end() 16 | }) 17 | 18 | let server = http.createServer(bot.middleware()).listen(0, () => { 19 | let address = server.address() 20 | 21 | request({ 22 | url: `http://localhost:${address.port}`, 23 | method: 'GET', 24 | qs: { 25 | 'hub.verify_token': '$ecr3ts!', 26 | 'hub.challenge': 'g0od_job!' 27 | } 28 | }, (err, res, body) => { 29 | t.error(err, 'response should not error') 30 | t.equals(res.statusCode, 200, 'request should return status code 200') 31 | t.equals(body, 'g0od_job!', 'response body should be hub.challenge value') 32 | t.end() 33 | }) 34 | }) 35 | 36 | t.tearDown(() => { 37 | server.close() 38 | }) 39 | }) 40 | 41 | tap.test('GET webhook with incorrect verify token', (t) => { 42 | let bot = new Bot({ 43 | token: 'foo', 44 | verify: '$ecr3ts!' 45 | }) 46 | 47 | bot.on('error', (err) => { 48 | t.error(err, 'bot instance returned error') 49 | t.end() 50 | }) 51 | 52 | let server = http.createServer(bot.middleware()).listen(0, () => { 53 | let address = server.address() 54 | 55 | request({ 56 | url: `http://localhost:${address.port}`, 57 | method: 'GET', 58 | qs: { 59 | 'hub.verify_token': 'WRONG', 60 | 'hub.challenge': 'g0od_job!' 61 | } 62 | }, (err, res, body) => { 63 | t.error(err, 'response should not error') 64 | t.equals(res.statusCode, 200, 'request should return status code 200') 65 | t.equals(body, 'Error, wrong validation token', 'response body returned error') 66 | t.end() 67 | }) 68 | }) 69 | 70 | t.tearDown(() => { 71 | server.close() 72 | }) 73 | }) 74 | -------------------------------------------------------------------------------- /test/basic.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const tap = require('tap') 3 | const nock = require('nock') 4 | const http = require('http') 5 | const request = require('request') 6 | const Bot = require('../') 7 | 8 | tap.test('basic initialization', (t) => { 9 | let bot 10 | 11 | t.doesNotThrow(() => { bot = new Bot({ token: 'foo' }) }, 'creating bot does not throw') 12 | t.type(bot, 'object', 'bot class is initiated correctly') 13 | t.equals(bot.token, 'foo', 'bot token is stored correctly') 14 | t.type(bot.middleware, 'function', 'bot.middleware is a function') 15 | t.type(bot.middleware(), 'function', 'bot.middleware returns a function') 16 | t.type(bot.sendMessage, 'function', 'bot.sendMessage is a function') 17 | t.type(bot.getProfile, 'function', 'bot.getProfile is a function') 18 | 19 | t.end() 20 | }) 21 | 22 | tap.test('initialization with changed graph_url ', (t) => { 23 | let bot 24 | 25 | t.doesNotThrow(() => { bot = new Bot({ token: 'foo', graph_url: 'http://example.com' }) }, 'creating bot does not throw') 26 | t.type(bot, 'object', 'bot class is initiated correctly') 27 | t.equals(bot.token, 'foo', 'bot token is stored correctly') 28 | t.equals(bot.graph_url, 'http://example.com', 'bot graph_url is stored correctly') 29 | 30 | t.end() 31 | }) 32 | 33 | tap.test('missing token paramater', (t) => { 34 | t.throws(() => new Bot(), 'bot without token specified should throw') 35 | t.end() 36 | }) 37 | 38 | tap.test('middleware runs', (t) => { 39 | let bot = new Bot({ 40 | token: 'foo' 41 | }) 42 | 43 | let server = http.createServer(bot.middleware()).listen(0, () => { 44 | let address = server.address() 45 | 46 | request.get(`http://localhost:${address.port}/_status`, (err, res, body) => { 47 | t.error(err, 'should not error') 48 | 49 | t.deepEquals(JSON.parse(body), { status: 'ok' }, 'status endpoint should return ok') 50 | 51 | t.tearDown(() => { 52 | server.close() 53 | }) 54 | t.end() 55 | }) 56 | }) 57 | }) 58 | 59 | tap.test('debug works', (t) => { 60 | let bot = new Bot({ 61 | token: 'foo', 62 | debug: 'all' 63 | }) 64 | 65 | let payload = { 66 | recipient: { id: 1 }, 67 | message: { text: 'hello!' } 68 | } 69 | 70 | let response = { 71 | recipient_id: '1008372609250235', 72 | message_id: 'mid.1456970487936:c34767dfe57ee6e339', 73 | __debug__: {} 74 | } 75 | 76 | nock('https://graph.facebook.com').post('/v2.12/me/messages', payload).query({ 77 | access_token: 'foo', 78 | debug: 'all' 79 | }).reply(200, response) 80 | 81 | return bot.sendMessage(1, payload.message, (err, body) => { 82 | t.error(err, 'response should not be error') 83 | t.deepEquals(body, response, 'response is correct') 84 | t.end() 85 | }).catch(t.threw) 86 | }) 87 | -------------------------------------------------------------------------------- /test/remove-field-settings.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const tap = require('tap') 3 | const nock = require('nock') 4 | const Bot = require('../') 5 | 6 | tap.test('remove field settings - get started button - successful request', (t) => { 7 | let bot = new Bot({ 8 | token: 'foo' 9 | }) 10 | 11 | let payload = { 12 | fields: ['get_started'] 13 | } 14 | 15 | let response = { 16 | result: 'Successfully deleted get started button' 17 | } 18 | 19 | nock('https://graph.facebook.com') 20 | .delete('/v2.12/me/messenger_profile', payload) 21 | .query({ 22 | access_token: 'foo' 23 | }) 24 | .reply(200, response) 25 | 26 | return bot.removeGetStartedButton((err, profile) => { 27 | t.error(err, 'response should not be error') 28 | t.deepEquals(profile, response, 'response is correct') 29 | t.end() 30 | }).catch(t.threw) 31 | }) 32 | 33 | tap.test('remove field settings - persistent menu - successful request', (t) => { 34 | let bot = new Bot({ 35 | token: 'foo' 36 | }) 37 | 38 | let payload = { 39 | fields: ['persistent_menu'] 40 | } 41 | 42 | let response = { 43 | result: 'Successfully deleted persistent menu' 44 | } 45 | 46 | nock('https://graph.facebook.com') 47 | .delete('/v2.12/me/messenger_profile', payload) 48 | .query({ 49 | access_token: 'foo' 50 | }) 51 | .reply(200, response) 52 | 53 | return bot.removePersistentMenu((err, profile) => { 54 | t.error(err, 'response should not be error') 55 | t.deepEquals(profile, response, 'response is correct') 56 | t.end() 57 | }).catch(t.threw) 58 | }) 59 | 60 | tap.test('remove field settings - domain whitelist - successful request', (t) => { 61 | let bot = new Bot({ 62 | token: 'foo' 63 | }) 64 | 65 | let payload = { 66 | fields: ['whitelisted_domains'] 67 | } 68 | 69 | let response = { 70 | result: 'Successfully deleted persistent menu' 71 | } 72 | 73 | nock('https://graph.facebook.com') 74 | .delete('/v2.12/me/messenger_profile', payload) 75 | .query({ 76 | access_token: 'foo' 77 | }) 78 | .reply(200, response) 79 | 80 | return bot.removeDomainWhitelist((err, profile) => { 81 | t.error(err, 'response should not be error') 82 | t.deepEquals(profile, response, 'response is correct') 83 | t.end() 84 | }).catch(t.threw) 85 | }) 86 | 87 | tap.test('remove field settings - greeting text - successful request', (t) => { 88 | let bot = new Bot({ 89 | token: 'foo' 90 | }) 91 | 92 | let payload = { 93 | fields: ['greeting'] 94 | } 95 | 96 | let response = { 97 | result: 'Successfully deleted persistent menu' 98 | } 99 | 100 | nock('https://graph.facebook.com') 101 | .delete('/v2.12/me/messenger_profile', payload) 102 | .query({ 103 | access_token: 'foo' 104 | }) 105 | .reply(200, response) 106 | 107 | return bot.removeGreeting((err, profile) => { 108 | t.error(err, 'response should not be error') 109 | t.deepEquals(profile, response, 'response is correct') 110 | t.end() 111 | }).catch(t.threw) 112 | }) 113 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at . All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [http://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: http://contributor-covenant.org 74 | [version]: http://contributor-covenant.org/version/1/4/ 75 | -------------------------------------------------------------------------------- /test/set-field-settings.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const tap = require('tap') 3 | const nock = require('nock') 4 | const Bot = require('../') 5 | 6 | tap.test('set field settings - get started button - successful request', (t) => { 7 | let bot = new Bot({ 8 | token: 'foo' 9 | }) 10 | 11 | let payload = { 12 | 'get_started': ['USER_DEFINED_PAYLOAD'] 13 | } 14 | 15 | let response = { 16 | result: 'Successfully added get started button' 17 | } 18 | 19 | nock('https://graph.facebook.com') 20 | .post('/v2.12/me/messenger_profile', payload) 21 | .query({ 22 | access_token: 'foo' 23 | }) 24 | .reply(200, response) 25 | 26 | let getStartedPayload = ['USER_DEFINED_PAYLOAD'] 27 | 28 | return bot.setGetStartedButton(getStartedPayload, (err, profile) => { 29 | t.error(err, 'response should not be error') 30 | t.deepEquals(profile, response, 'response is correct') 31 | t.end() 32 | }).catch(t.threw) 33 | }) 34 | 35 | tap.test('set field settings - persistent menu - successful request', (t) => { 36 | let bot = new Bot({ 37 | token: 'foo' 38 | }) 39 | 40 | let payload = { 41 | 'persistent_menu': ['USER_DEFINED_PAYLOAD'] 42 | } 43 | 44 | let response = { 45 | result: 'Successfully added persistent menu' 46 | } 47 | 48 | nock('https://graph.facebook.com') 49 | .post('/v2.12/me/messenger_profile', payload) 50 | .query({ 51 | access_token: 'foo' 52 | }) 53 | .reply(200, response) 54 | 55 | let persistentMenuPayload = ['USER_DEFINED_PAYLOAD'] 56 | 57 | return bot.setPersistentMenu(persistentMenuPayload, (err, profile) => { 58 | t.error(err, 'response should not be error') 59 | t.deepEquals(profile, response, 'response is correct') 60 | t.end() 61 | }).catch(t.threw) 62 | }) 63 | 64 | tap.test('set field settings - domain whitelist - successful request', (t) => { 65 | let bot = new Bot({ 66 | token: 'foo' 67 | }) 68 | 69 | let payload = { 70 | 'whitelisted_domains': ['USER_DEFINED_PAYLOAD'] 71 | } 72 | 73 | let response = { 74 | result: 'Successfully added domain whitelist' 75 | } 76 | 77 | nock('https://graph.facebook.com') 78 | .post('/v2.12/me/messenger_profile', payload) 79 | .query({ 80 | access_token: 'foo' 81 | }) 82 | .reply(200, response) 83 | 84 | let domainWhitelistPayload = ['USER_DEFINED_PAYLOAD'] 85 | 86 | return bot.setDomainWhitelist(domainWhitelistPayload, (err, profile) => { 87 | t.error(err, 'response should not be error') 88 | t.deepEquals(profile, response, 'response is correct') 89 | t.end() 90 | }).catch(t.threw) 91 | }) 92 | 93 | tap.test('set field settings - greeting - successful request', (t) => { 94 | let bot = new Bot({ 95 | token: 'foo' 96 | }) 97 | 98 | let payload = { 99 | 'greeting': ['USER_DEFINED_PAYLOAD'] 100 | } 101 | 102 | let response = { 103 | result: 'Successfully added greeting' 104 | } 105 | 106 | nock('https://graph.facebook.com') 107 | .post('/v2.12/me/messenger_profile', payload) 108 | .query({ 109 | access_token: 'foo' 110 | }) 111 | .reply(200, response) 112 | 113 | let greetingPayload = ['USER_DEFINED_PAYLOAD'] 114 | 115 | return bot.setGreeting(greetingPayload, (err, profile) => { 116 | t.error(err, 'response should not be error') 117 | t.deepEquals(profile, response, 'response is correct') 118 | t.end() 119 | }).catch(t.threw) 120 | }) 121 | -------------------------------------------------------------------------------- /test/sender-actions.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const tap = require('tap') 3 | const http = require('http') 4 | const request = require('request') 5 | const nock = require('nock') 6 | const Bot = require('../') 7 | 8 | tap.test('actions.setTyping(true)', (t) => { 9 | let bot = new Bot({ 10 | token: 'foo' 11 | }) 12 | 13 | let payload = { 14 | recipient: { 15 | id: 982337261802700 16 | }, 17 | sender_action: 'typing_on' 18 | } 19 | 20 | let response = { 21 | recipient_id: 982337261802700 22 | } 23 | 24 | nock('https://graph.facebook.com') 25 | .post('/v2.12/me/messages', payload) 26 | .query({ 27 | access_token: 'foo' 28 | }) 29 | .reply(200, response) 30 | 31 | bot.on('error', (err) => { 32 | t.error(err, 'bot instance returned error') 33 | t.end() 34 | }) 35 | 36 | bot.on('message', (payload, reply, actions) => { 37 | t.type(payload, 'object', 'payload should be an object') 38 | t.type(reply, 'function', 'reply convenience function should exist') 39 | t.type(actions, 'object', 'actions should be an object') 40 | t.equals(payload.message.text, 'Test äëï', 'correct message was sent') 41 | 42 | actions.setTyping(true, (err, profile) => { 43 | t.error(err, 'response should not be error') 44 | t.deepEquals(profile, response, 'response is correct') 45 | t.end() 46 | }) 47 | }) 48 | 49 | let server = http.createServer(bot.middleware()).listen(0, () => { 50 | let address = server.address() 51 | 52 | request({ 53 | url: `http://localhost:${address.port}`, 54 | method: 'POST', 55 | headers: { 56 | 'Content-Type': 'application/json' 57 | }, 58 | body: '{"object":"page","entry":[{"id":1751036168465324,"time":1460923697656,"messaging":[{"sender":{"id":982337261802700},"recipient":{"id":1751036168465324},"timestamp":1460923697635,"message":{"mid":"mid.1460923697625:5c96e8279b55505308","seq":614,"text":"Test \\u00e4\\u00eb\\u00ef"}}]}]}' 59 | }, (err, res, body) => { 60 | t.error(err, 'response should not error') 61 | t.equals(res.statusCode, 200, 'request should return status code 200') 62 | t.deepEquals(JSON.parse(body), { status: 'ok' }, 'response should be okay') 63 | }) 64 | }) 65 | 66 | t.tearDown(() => { 67 | server.close() 68 | }) 69 | }) 70 | 71 | tap.test('actions.setTyping(false)', (t) => { 72 | let bot = new Bot({ 73 | token: 'foo' 74 | }) 75 | 76 | let payload = { 77 | recipient: { 78 | id: 982337261802700 79 | }, 80 | sender_action: 'typing_off' 81 | } 82 | 83 | let response = { 84 | recipient_id: 982337261802700 85 | } 86 | 87 | nock('https://graph.facebook.com') 88 | .post('/v2.12/me/messages', payload) 89 | .query({ 90 | access_token: 'foo' 91 | }) 92 | .reply(200, response) 93 | 94 | bot.on('error', (err) => { 95 | t.error(err, 'bot instance returned error') 96 | t.end() 97 | }) 98 | 99 | bot.on('message', (payload, reply, actions) => { 100 | t.type(payload, 'object', 'payload should be an object') 101 | t.type(reply, 'function', 'reply convenience function should exist') 102 | t.type(actions, 'object', 'actions should be an object') 103 | t.equals(payload.message.text, 'Test äëï', 'correct message was sent') 104 | 105 | actions.setTyping(false, (err, profile) => { 106 | t.error(err, 'response should not be error') 107 | t.deepEquals(profile, response, 'response is correct') 108 | t.end() 109 | }) 110 | }) 111 | 112 | let server = http.createServer(bot.middleware()).listen(0, () => { 113 | let address = server.address() 114 | 115 | request({ 116 | url: `http://localhost:${address.port}`, 117 | method: 'POST', 118 | headers: { 119 | 'Content-Type': 'application/json' 120 | }, 121 | body: '{"object":"page","entry":[{"id":1751036168465324,"time":1460923697656,"messaging":[{"sender":{"id":982337261802700},"recipient":{"id":1751036168465324},"timestamp":1460923697635,"message":{"mid":"mid.1460923697625:5c96e8279b55505308","seq":614,"text":"Test \\u00e4\\u00eb\\u00ef"}}]}]}' 122 | }, (err, res, body) => { 123 | t.error(err, 'response should not error') 124 | t.equals(res.statusCode, 200, 'request should return status code 200') 125 | t.deepEquals(JSON.parse(body), { status: 'ok' }, 'response should be okay') 126 | }) 127 | }) 128 | 129 | t.tearDown(() => { 130 | server.close() 131 | }) 132 | }) 133 | 134 | tap.test('actions.markRead()', (t) => { 135 | let bot = new Bot({ 136 | token: 'foo' 137 | }) 138 | 139 | let payload = { 140 | recipient: { 141 | id: 982337261802700 142 | }, 143 | sender_action: 'mark_seen' 144 | } 145 | 146 | let response = { 147 | recipient_id: 982337261802700 148 | } 149 | 150 | nock('https://graph.facebook.com') 151 | .post('/v2.12/me/messages', payload) 152 | .query({ 153 | access_token: 'foo' 154 | }) 155 | .reply(200, response) 156 | 157 | bot.on('error', (err) => { 158 | t.error(err, 'bot instance returned error') 159 | t.end() 160 | }) 161 | 162 | bot.on('message', (payload, reply, actions) => { 163 | t.type(payload, 'object', 'payload should be an object') 164 | t.type(reply, 'function', 'reply convenience function should exist') 165 | t.type(actions, 'object', 'actions should be an object') 166 | t.equals(payload.message.text, 'Test äëï', 'correct message was sent') 167 | 168 | actions.markRead((err, profile) => { 169 | t.error(err, 'response should not be error') 170 | t.deepEquals(profile, response, 'response is correct') 171 | t.end() 172 | }) 173 | }) 174 | 175 | let server = http.createServer(bot.middleware()).listen(0, () => { 176 | let address = server.address() 177 | 178 | request({ 179 | url: `http://localhost:${address.port}`, 180 | method: 'POST', 181 | headers: { 182 | 'Content-Type': 'application/json' 183 | }, 184 | body: '{"object":"page","entry":[{"id":1751036168465324,"time":1460923697656,"messaging":[{"sender":{"id":982337261802700},"recipient":{"id":1751036168465324},"timestamp":1460923697635,"message":{"mid":"mid.1460923697625:5c96e8279b55505308","seq":614,"text":"Test \\u00e4\\u00eb\\u00ef"}}]}]}' 185 | }, (err, res, body) => { 186 | t.error(err, 'response should not error') 187 | t.equals(res.statusCode, 200, 'request should return status code 200') 188 | t.deepEquals(JSON.parse(body), { status: 'ok' }, 'response should be okay') 189 | }) 190 | }) 191 | 192 | t.tearDown(() => { 193 | server.close() 194 | }) 195 | }) 196 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const url = require('url') 3 | const qs = require('querystring') 4 | const EventEmitter = require('events').EventEmitter 5 | const request = require('request-promise') 6 | const crypto = require('crypto') 7 | 8 | class Bot extends EventEmitter { 9 | constructor (opts) { 10 | super() 11 | 12 | opts = opts || {} 13 | if (!opts.token) { 14 | throw new Error('Missing page token. See FB documentation for details: https://developers.facebook.com/docs/messenger-platform/quickstart') 15 | } 16 | this.graph_url = opts.graph_url ? opts.graph_url : 'https://graph.facebook.com/v2.12/' 17 | this.token = opts.token 18 | this.app_secret = opts.app_secret || false 19 | this.verify_token = opts.verify || false 20 | this.debug = opts.debug || false 21 | } 22 | 23 | getProfile (id, cb) { 24 | return request({ 25 | method: 'GET', 26 | uri: `${this.graph_url}${id}`, 27 | qs: this._getQs({fields: 'first_name,last_name,profile_pic,locale,timezone,gender'}), 28 | json: true 29 | }) 30 | .then(body => { 31 | if (body.error) return Promise.reject(body.error) 32 | if (!cb) return body 33 | cb(null, body) 34 | }) 35 | .catch(err => { 36 | if (!cb) return Promise.reject(err) 37 | cb(err) 38 | }) 39 | } 40 | 41 | sendMessage (recipient, payload, cb, messagingType = null, tag = null) { 42 | let options = { 43 | method: 'POST', 44 | uri: this.graph_url + 'me/messages', 45 | qs: this._getQs(), 46 | json: { 47 | recipient: { id: recipient }, 48 | message: payload 49 | } 50 | } 51 | if (messagingType === 'MESSAGE_TAG') { 52 | if (tag != null) { 53 | options.json.tag = tag 54 | } else { 55 | cb(new Error('You must specify a Tag')) 56 | } 57 | } 58 | if (messagingType != null) { 59 | options.json.messaging_type = messagingType 60 | } 61 | return request(options) 62 | .then(body => { 63 | if (body.error) return Promise.reject(body.error) 64 | if (!cb) return body 65 | cb(null, body) 66 | }) 67 | .catch(err => { 68 | if (!cb) return Promise.reject(err) 69 | cb(err) 70 | }) 71 | } 72 | 73 | sendSenderAction (recipient, senderAction, cb) { 74 | return request({ 75 | method: 'POST', 76 | uri: this.graph_url + 'me/messages', 77 | qs: this._getQs(), 78 | json: { 79 | recipient: { 80 | id: recipient 81 | }, 82 | sender_action: senderAction 83 | } 84 | }) 85 | .then(body => { 86 | if (body.error) return Promise.reject(body.error) 87 | if (!cb) return body 88 | cb(null, body) 89 | }) 90 | .catch(err => { 91 | if (!cb) return Promise.reject(err) 92 | cb(err) 93 | }) 94 | } 95 | 96 | setField (field, payload, cb) { 97 | return request({ 98 | method: 'POST', 99 | uri: this.graph_url + 'me/messenger_profile', 100 | qs: this._getQs(), 101 | json: { 102 | [field]: payload 103 | } 104 | }) 105 | .then(body => { 106 | if (body.error) return Promise.reject(body.error) 107 | if (!cb) return body 108 | cb(null, body) 109 | }) 110 | .catch(err => { 111 | if (!cb) return Promise.reject(err) 112 | cb(err) 113 | }) 114 | } 115 | 116 | deleteField (field, cb) { 117 | return request({ 118 | method: 'DELETE', 119 | uri: this.graph_url + 'me/messenger_profile', 120 | qs: this._getQs(), 121 | json: { 122 | fields: [field] 123 | } 124 | }) 125 | .then(body => { 126 | if (body.error) return Promise.reject(body.error) 127 | if (!cb) return body 128 | cb(null, body) 129 | }) 130 | .catch(err => { 131 | if (!cb) return Promise.reject(err) 132 | cb(err) 133 | }) 134 | } 135 | 136 | unlinkAccount (psid, cb) { 137 | return request({ 138 | method: 'POST', 139 | uri: this.graph_url + 'me/unlink_accounts', 140 | qs: this._getQs(), 141 | json: { 142 | psid: psid 143 | } 144 | }) 145 | .then(body => { 146 | if (body.error) return Promise.reject(body.error) 147 | if (!cb) return body 148 | cb(null, body) 149 | }) 150 | .catch(err => { 151 | if (!cb) return Promise.reject(err) 152 | cb(err) 153 | }) 154 | } 155 | 156 | getAttachmentUploadId (url, isReusable, type, cb) { 157 | return request({ 158 | method: 'POST', 159 | uri: this.graph_url + 'me/message_attachments', 160 | qs: this._getQs(), 161 | json: { 162 | message: { 163 | attachment: { 164 | type: type, 165 | payload: { 166 | is_reusable: isReusable, 167 | url: url 168 | } 169 | } 170 | } 171 | } 172 | }) 173 | .then(body => { 174 | if (body.error) return Promise.reject(body.error) 175 | if (!cb) return body 176 | cb(null, body) 177 | }) 178 | .catch(err => { 179 | if (!cb) return Promise.reject(err) 180 | cb(err) 181 | }) 182 | } 183 | 184 | setGetStartedButton (payload, cb) { 185 | return this.setField('get_started', payload, cb) 186 | } 187 | 188 | setPersistentMenu (payload, cb) { 189 | return this.setField('persistent_menu', payload, cb) 190 | } 191 | 192 | setDomainWhitelist (payload, cb) { 193 | return this.setField('whitelisted_domains', payload, cb) 194 | } 195 | 196 | setGreeting (payload, cb) { 197 | return this.setField('greeting', payload, cb) 198 | } 199 | 200 | removeGetStartedButton (cb) { 201 | return this.deleteField('get_started', cb) 202 | } 203 | 204 | removePersistentMenu (cb) { 205 | return this.deleteField('persistent_menu', cb) 206 | } 207 | 208 | removeDomainWhitelist (cb) { 209 | return this.deleteField('whitelisted_domains', cb) 210 | } 211 | 212 | removeGreeting (cb) { 213 | return this.deleteField('greeting', cb) 214 | } 215 | 216 | middleware () { 217 | return (req, res) => { 218 | // we always write 200, otherwise facebook will keep retrying the request 219 | res.writeHead(200, { 'Content-Type': 'application/json' }) 220 | if (req.url === '/_status') return res.end(JSON.stringify({status: 'ok'})) 221 | if (this.verify_token && req.method === 'GET') return this._verify(req, res) 222 | if (req.method !== 'POST') return res.end() 223 | 224 | let body = '' 225 | 226 | req.on('data', (chunk) => { 227 | body += chunk 228 | }) 229 | 230 | req.on('end', () => { 231 | // check message integrity 232 | if (this.app_secret) { 233 | let hmac = crypto.createHmac('sha1', this.app_secret) 234 | hmac.update(body) 235 | 236 | if (req.headers['x-hub-signature'] !== `sha1=${hmac.digest('hex')}`) { 237 | this.emit('error', new Error('Message integrity check failed')) 238 | return res.end(JSON.stringify({status: 'not ok', error: 'Message integrity check failed'})) 239 | } 240 | } 241 | 242 | let parsed = JSON.parse(body) 243 | if (parsed.entry[0].messaging !== null && typeof parsed.entry[0].messaging[0] !== 'undefined') { 244 | this._handleMessage(parsed) 245 | } 246 | 247 | res.end(JSON.stringify({status: 'ok'})) 248 | }) 249 | } 250 | } 251 | 252 | _getQs (qs) { 253 | if (typeof qs === 'undefined') { 254 | qs = {} 255 | } 256 | qs['access_token'] = this.token 257 | 258 | if (this.debug) { 259 | qs['debug'] = this.debug 260 | } 261 | 262 | return qs 263 | } 264 | 265 | _handleMessage (json) { 266 | let entries = json.entry 267 | 268 | entries.forEach((entry) => { 269 | let events = entry.messaging 270 | 271 | events.forEach((event) => { 272 | // handle inbound messages and echos 273 | if (event.message) { 274 | if (event.message.is_echo) { 275 | this._handleEvent('echo', event) 276 | } else { 277 | this._handleEvent('message', event) 278 | } 279 | } 280 | 281 | // handle postbacks 282 | if (event.postback) { 283 | this._handleEvent('postback', event) 284 | } 285 | 286 | // handle message delivered 287 | if (event.delivery) { 288 | this._handleEvent('delivery', event) 289 | } 290 | 291 | // handle message read 292 | if (event.read) { 293 | this._handleEvent('read', event) 294 | } 295 | 296 | // handle authentication 297 | if (event.optin) { 298 | this._handleEvent('authentication', event) 299 | } 300 | 301 | // handle referrals (e.g. m.me links) 302 | if (event.referral) { 303 | this._handleEvent('referral', event) 304 | } 305 | 306 | // handle account_linking 307 | if (event.account_linking && event.account_linking.status) { 308 | if (event.account_linking.status === 'linked') { 309 | this._handleEvent('accountLinked', event) 310 | } else if (event.account_linking.status === 'unlinked') { 311 | this._handleEvent('accountUnlinked', event) 312 | } 313 | } 314 | }) 315 | }) 316 | } 317 | 318 | _getActionsObject (event) { 319 | return { 320 | setTyping: (typingState, cb) => { 321 | let senderTypingAction = typingState ? 'typing_on' : 'typing_off' 322 | this.sendSenderAction(event.sender.id, senderTypingAction, cb) 323 | }, 324 | markRead: this.sendSenderAction.bind(this, event.sender.id, 'mark_seen') 325 | } 326 | } 327 | 328 | _verify (req, res) { 329 | let query = qs.parse(url.parse(req.url).query) 330 | 331 | if (query['hub.verify_token'] === this.verify_token) { 332 | return res.end(query['hub.challenge']) 333 | } 334 | 335 | return res.end('Error, wrong validation token') 336 | } 337 | 338 | _handleEvent (type, event) { 339 | this.emit(type, event, this.sendMessage.bind(this, event.sender.id), this._getActionsObject(event)) 340 | } 341 | } 342 | 343 | module.exports = Bot 344 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # messenger-bot 2 | [![Build Status](https://travis-ci.org/remixz/messenger-bot.svg?branch=master)](https://travis-ci.org/remixz/messenger-bot) 3 | [![Coverage Status](https://coveralls.io/repos/github/remixz/messenger-bot/badge.svg?branch=master)](https://coveralls.io/github/remixz/messenger-bot?branch=master) 4 | [![npm version](https://img.shields.io/npm/v/messenger-bot.svg)](https://www.npmjs.com/package/messenger-bot) 5 | [![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg)](http://standardjs.com/) 6 | 7 | 8 | A Node client for the [Facebook Messenger Platform](https://developers.facebook.com/docs/messenger-platform). 9 | 10 | Requires Node >=4.0.0. 11 | 12 | ## Installation 13 | 14 | ```bash 15 | npm install messenger-bot 16 | ``` 17 | 18 | ## Example 19 | 20 | See more examples in [the examples folder.](https://github.com/remixz/messenger-bot/tree/master/example) 21 | 22 | Run this example in the cloud: [![Nitrous Quickstart](https://nitrous-image-icons.s3.amazonaws.com/quickstart.svg)](https://www.nitrous.io/quickstart) 23 | * Setup `PAGE_TOKEN`, `VERIFY_TOKEN`, `APP_SECRET` and start the example by `Run > Start Messenger Echo Bot`. 24 | * Your Webhook URL is available at `Preview > 3000` in the IDE. 25 | 26 | ```js 27 | const http = require('http') 28 | const Bot = require('messenger-bot') 29 | 30 | let bot = new Bot({ 31 | token: 'PAGE_TOKEN', 32 | verify: 'VERIFY_TOKEN', 33 | app_secret: 'APP_SECRET' 34 | }) 35 | 36 | bot.on('error', (err) => { 37 | console.log(err.message) 38 | }) 39 | 40 | bot.on('message', (payload, reply) => { 41 | let text = payload.message.text 42 | 43 | bot.getProfile(payload.sender.id, (err, profile) => { 44 | if (err) throw err 45 | 46 | reply({ text }, (err) => { 47 | if (err) throw err 48 | 49 | console.log(`Echoed back to ${profile.first_name} ${profile.last_name}: ${text}`) 50 | }) 51 | }) 52 | }) 53 | 54 | http.createServer(bot.middleware()).listen(3000) 55 | console.log('Echo bot server running at port 3000.') 56 | ``` 57 | 58 | ## Usage 59 | 60 | ### Functions 61 | 62 | #### `let bot = new Bot(opts)` 63 | 64 | Returns a new Bot instance. 65 | 66 | `opts` - Object 67 | 68 | * `token` - String: Your Page Access Token, found in your App settings. Required. 69 | * `verify` - String: A verification token for the first-time setup of your webhook. Optional, but will be required by Facebook when you first set up your webhook. 70 | * `app_secret` - String: Your App Secret token used for message integrity check. If specified, every POST request will be tested for spoofing. Optional. 71 | 72 | #### `bot.middleware()` 73 | 74 | The main middleware for your bot's webhook. Returns a function. Usage: 75 | 76 | ```js 77 | const http = require('http') 78 | const Bot = require('messenger-bot') 79 | 80 | let bot = new Bot({ 81 | token: 'PAGE_TOKEN', 82 | verify: 'VERIFY_TOKEN' 83 | }) 84 | 85 | http.createServer(bot.middleware()).listen(3000) 86 | ``` 87 | 88 | As well, it mounts `/_status`, which will return `{"status": "ok"}` if the middleware is running. If `verify` is specified in the bot options, it will mount a handler for `GET` requests that verifies the webhook. 89 | 90 | #### `bot.sendMessage(recipient, payload, [callback], [messagingType], [tag])` 91 | 92 | Sends a message with the `payload` to the target `recipient`, and calls the callback if any. Returns a promise. See [Send API](https://developers.facebook.com/docs/messenger-platform/send-api-reference#request). 93 | 94 | * `recipient` - Number: The Facebook ID of the intended recipient. 95 | * `payload` - Object: The message payload. Should follow the [Send API format](https://developers.facebook.com/docs/messenger-platform/send-api-reference). 96 | * `callback` - (Optional) Function: Called with `(err, info)` once the request has completed. `err` contains an error, if any, and `info` contains the response from Facebook, usually with the new message's ID. 97 | * `messagingType` - (Optional) String: The message type. [Supported Messaging Type](https://developers.facebook.com/docs/messenger-platform/send-messages#messaging_types). 98 | * `tag` - (Optional) String: The tag's message. [Supported Tags](https://developers.facebook.com/docs/messenger-platform/send-messages/message-tags#supported_tags). 99 | 100 | #### `bot.getAttachmentUploadId(url, is_reusable, type, [callback])` 101 | 102 | Sends the media to the Attachment Upload API and calls the callback if the upload is successful, including the `attachment_id`. See [Attachment Upload API](https://developers.facebook.com/docs/messenger-platform/reference/attachment-upload-api). 103 | 104 | * `url` - String: Link where can be fetched the media. 105 | * `is_reusable` - Boolean: Defined if the saved asset will be sendable to other message recipients. 106 | * `type` - String: The type of media. Can be one of: `image`, `video`, `audio`, `file`. 107 | * `callback` - (Optional) Function: Called with `(err, info)` once the request has completed. `err` contains an error, if any, and `info` contains the response from Facebook, usually with the media's ID. 108 | 109 | #### `bot.sendSenderAction(recipient, senderAction, [callback])` 110 | 111 | Sends the sender action `senderAction` to the target `recipient`, and calls the callback if any. Returns a promise. 112 | 113 | * `recipient` - Number: The Facebook ID of the intended recipient. 114 | * `senderAction` - String: The sender action to execute. Can be one of: `typing_on`, 'typing_off', 'mark_seen'. See the [Sender Actions API reference](https://developers.facebook.com/docs/messenger-platform/send-api-reference/sender-actions) for more information. 115 | * `callback` - (Optional) Function: Called with `(err, info)` once the request has completed. `err` contains an error, if any, and `info` contains the response from Facebook, usually with the new message's ID. 116 | 117 | #### `bot.unlinkAccount(psid, [callback])` 118 | 119 | Unlinks the user with the corresponding `psid`, and calls the callback if any. Returns a promise. See [Account Unlink Endpoint].(https://developers.facebook.com/docs/messenger-platform/identity/account-linking?locale=en_US#unlink) 120 | 121 | * `psid` - Number: The Facebook ID of the user who has to be logged out. 122 | * `callback` - (Optional) Function: Called with `(err, info)` once the request has completed. `err` contains an error, if any, and `info` contains the response from Facebook. 123 | 124 | #### `bot.setGetStartedButton(payload, [callback])` 125 | #### `bot.setPersistentMenu(payload, [callback])` 126 | 127 | Sets settings for the Get Started Button / Persistent Menu. See the [Messenger Profile Reference](https://developers.facebook.com/docs/messenger-platform/messenger-profile) for what to put in the `payload`. 128 | 129 | #### `bot.removeGetStartedButton([callback])` 130 | #### `bot.removePersistentMenu([callback])` 131 | 132 | Removes the Get Started Button / Persistent Menu. 133 | 134 | #### `bot.getProfile(target, [callback])` 135 | 136 | Returns a promise of the profile information of the `target`, also called in the `callback` if any. See [User Profile API](https://developers.facebook.com/docs/messenger-platform/send-api-reference#user_profile_request). 137 | 138 | * `target` - Number: The Facebook ID of the intended target. 139 | * `callback` - (Optional) Function: Called with `(err, profile)` once the request has completed. `err` contains an error, if any, and `info` contains the response from Facebook, in this format: 140 | 141 | ```json 142 | { 143 | "first_name": "Zach", 144 | "last_name": "Bruggeman", 145 | "profile_pic": "", 146 | "locale": "en", 147 | "timezone": "PST", 148 | "gender": "M" 149 | } 150 | ``` 151 | 152 | #### `bot._handleMessage(payload)` 153 | 154 | The underlying method used by `bot.middleware()` to parse the message payload, and fire the appropriate events. Use this if you've already implemented your own middleware or route handlers to receive the webhook request, and just want to fire the events on the bot instance. See [the echo bot implemented in Express](https://github.com/remixz/messenger-bot/blob/master/example/echo-express.js) for an example. 155 | 156 | * `payload` - Object: The payload sent by Facebook to the webhook. 157 | 158 | #### `bot._verify(req, res)` 159 | 160 | The underlying method used by `bot.middleware()` for the initial webhook verification. Use this if you've already implemented your own middleware or route handlers, and wish to handle the request without implementing `bot.middleware()`. See [the echo bot implemented in Express](https://github.com/remixz/messenger-bot/blob/master/example/echo-express.js) for an example. 161 | 162 | * `req` - Request: The `http` request object. 163 | * `res` - Response: The `http` response object. 164 | 165 | ### Events 166 | 167 | #### bot.on('message', (payload, reply, actions)) 168 | 169 | Triggered when a message is sent to the bot. 170 | 171 | * `payload` - Object: An object containing the message event's payload from Facebook. See [Facebook's documentation](https://developers.facebook.com/docs/messenger-platform/webhook-reference#received_message) for the format. 172 | * `reply` - Function: A convenience function that calls `bot.sendMessage`, with the recipient automatically set to the message sender's Facebook ID. Example usage: 173 | 174 | ```js 175 | bot.on('message', (payload, reply, actions) => { 176 | reply({ text: 'hey!'}, (err, info) => {}) 177 | }) 178 | ``` 179 | 180 | * `actions` - Object: An object with two functions: `setTyping(status: Boolean)`, and `markRead()`. 181 | 182 | #### bot.on('postback', (payload, reply, actions)) 183 | 184 | Triggered when a postback is triggered by the sender in Messenger. 185 | 186 | * `payload` - Object: An object containing the postback event's payload from Facebook. See [Facebook's documentation](https://developers.facebook.com/docs/messenger-platform/webhook-reference#postback) for the format. 187 | * `reply` - Function: A convenience function that calls `bot.sendMessage`, with the recipient automatically set to the message sender's Facebook ID. Example usage: 188 | * `actions` - Object: An object with two functions: `setTyping(status: Boolean)`, and `markRead()`. 189 | 190 | ```js 191 | bot.on('postback', (payload, reply, actions) => { 192 | reply({ text: 'hey!'}, (err, info) => {}) 193 | }) 194 | ``` 195 | 196 | #### bot.on('delivery', (payload, reply, actions)) 197 | 198 | Triggered when a message has been successfully delivered. 199 | 200 | * `payload` - Object: An object containing the delivery event's payload from Facebook. See [Facebook's documentation](https://developers.facebook.com/docs/messenger-platform/webhook-reference#message_delivery) for the format. 201 | * `reply` - Function: A convenience function that calls `bot.sendMessage`, with the recipient automatically set to the message sender's Facebook ID. Example usage: 202 | * `actions` - Object: An object with two functions: `setTyping(status: Boolean)`, and `markRead()`. 203 | 204 | ```js 205 | bot.on('delivery', (payload, reply, actions) => { 206 | reply({ text: 'hey!'}, (err, info) => {}) 207 | }) 208 | ``` 209 | 210 | #### bot.on('authentication', (payload, reply, actions)) 211 | 212 | Triggered when a user authenticates with the "Send to Messenger" plugin. 213 | 214 | * `payload` - Object: An object containing the authentication event's payload from Facebook. See [Facebook's documentation](https://developers.facebook.com/docs/messenger-platform/webhook-reference#auth) for the format. 215 | * `reply` - Function: A convenience function that calls `bot.sendMessage`, with the recipient automatically set to the message sender's Facebook ID. Example usage: 216 | * `actions` - Object: An object with two functions: `setTyping(status: Boolean)`, and `markRead()`. 217 | 218 | ```js 219 | bot.on('authentication', (payload, reply, actions) => { 220 | reply({ text: 'thanks!'}, (err, info) => {}) 221 | }) 222 | ``` 223 | 224 | #### bot.on('referral', (payload, reply, actions)) 225 | 226 | Triggered when an m.me link is used with a referral param and only in a case this user already has a thread with this bot (for new threads see 'postback' event) 227 | 228 | * `payload` - Object: An object containing the authentication event's payload from Facebook. See [Facebook's documentation](https://developers.facebook.com/docs/messenger-platform/webhook-reference/referral) for the format. 229 | * `reply` - Function: A convenience function that calls `bot.sendMessage`, with the recipient automatically set to the message sender's Facebook ID. Example usage: 230 | * `actions` - Object: An object with two functions: `setTyping(status: Boolean)`, and `markRead()`. 231 | 232 | ```js 233 | bot.on('referral', (payload, reply, actions) => { 234 | reply({ text: 'welcome!'}, (err, info) => {}) 235 | }) 236 | ``` 237 | 238 | #### bot.on('accountLinked', (payload, reply, actions)) 239 | 240 | Triggered when an account is linked with the [Account Linking Process](https://developers.facebook.com/docs/messenger-platform/identity/account-linking?locale=en_US#linking_process). 241 | 242 | * `payload` - Object: An object containing the linking account event's payload from Facebook. See [Facebook's documentation](https://developers.facebook.com/docs/messenger-platform/reference/webhook-events/messaging_account_linking) for the format. 243 | * `reply` - Function: A convenience function that calls `bot.sendMessage`, with the recipient automatically set to the message sender's Facebook ID. Example usage: 244 | * `actions` - Object: An object with two functions: `setTyping(status: Boolean)`, and `markRead()`. 245 | 246 | ```js 247 | bot.on('accountLinked', (payload, reply, actions) => { 248 | reply({ text: 'Logged in!'}, (err, info) => {}) 249 | }) 250 | ``` 251 | 252 | #### bot.on('accountUnlinked', (payload, reply, actions)) 253 | 254 | Triggered when an account is unlinked with the [Account Unlink Endpoint](https://developers.facebook.com/docs/messenger-platform/identity/account-linking?locale=en_US#unlink) or with an [Log Out Button](https://developers.facebook.com/docs/messenger-platform/reference/buttons/logout). 255 | 256 | * `payload` - Object: An object containing the unlinking account event's payload from Facebook. See [Facebook's documentation](https://developers.facebook.com/docs/messenger-platform/reference/webhook-events/messaging_account_linking) for the format. 257 | * `reply` - Function: A convenience function that calls `bot.sendMessage`, with the recipient automatically set to the message sender's Facebook ID. Example usage: 258 | * `actions` - Object: An object with two functions: `setTyping(status: Boolean)`, and `markRead()`. 259 | 260 | ```js 261 | bot.on('accountLinked', (payload, reply, actions) => { 262 | reply({ text: 'Logged out!'}, (err, info) => {}) 263 | }) 264 | ``` 265 | -------------------------------------------------------------------------------- /test/middleware-post.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const tap = require('tap') 3 | const http = require('http') 4 | const request = require('request') 5 | const Bot = require('../') 6 | 7 | tap.test('POST webhook with no signature check', (t) => { 8 | let bot = new Bot({ 9 | token: 'foo' 10 | }) 11 | 12 | bot.on('error', (err) => { 13 | t.error(err, 'bot instance returned error') 14 | t.end() 15 | }) 16 | 17 | bot.on('message', (payload, reply, actions) => { 18 | t.type(payload, 'object', 'payload should be an object') 19 | t.type(reply, 'function', 'reply convenience function should exist') 20 | t.type(actions, 'object', 'actions should be an object') 21 | t.equals(payload.message.text, 'Test äëï', 'correct message was sent') 22 | }) 23 | 24 | let server = http.createServer(bot.middleware()).listen(0, () => { 25 | let address = server.address() 26 | 27 | request({ 28 | url: `http://localhost:${address.port}`, 29 | method: 'POST', 30 | headers: { 31 | 'Content-Type': 'application/json' 32 | }, 33 | body: '{"object":"page","entry":[{"id":1751036168465324,"time":1460923697656,"messaging":[{"sender":{"id":982337261802700},"recipient":{"id":1751036168465324},"timestamp":1460923697635,"message":{"mid":"mid.1460923697625:5c96e8279b55505308","seq":614,"text":"Test \\u00e4\\u00eb\\u00ef"}}]}]}' 34 | }, (err, res, body) => { 35 | t.error(err, 'response should not error') 36 | t.equals(res.statusCode, 200, 'request should return status code 200') 37 | t.deepEquals(JSON.parse(body), { status: 'ok' }, 'response should be okay') 38 | t.end() 39 | }) 40 | }) 41 | 42 | t.tearDown(() => { 43 | server.close() 44 | }) 45 | }) 46 | 47 | tap.test('POST webhook with signature check', (t) => { 48 | let bot = new Bot({ 49 | token: 'foo', 50 | app_secret: 'e6af24be1d683c8c911949f897eea1f6' 51 | }) 52 | 53 | bot.on('error', (err) => { 54 | t.error(err, 'bot instance returned error') 55 | t.end() 56 | }) 57 | 58 | bot.on('message', (payload, reply, actions) => { 59 | t.type(payload, 'object', 'payload should be an object') 60 | t.type(reply, 'function', 'reply convenience function should exist') 61 | t.type(actions, 'object', 'actions should be an object') 62 | t.equals(payload.message.text, 'Test äëï', 'correct message was sent') 63 | }) 64 | 65 | let server = http.createServer(bot.middleware()).listen(0, () => { 66 | let address = server.address() 67 | 68 | request({ 69 | url: `http://localhost:${address.port}`, 70 | method: 'POST', 71 | headers: { 72 | 'Content-Type': 'application/json', 73 | 'x-hub-signature': 'sha1=f1a4569dcf02a9829a15696d949b386b7d6d0272' 74 | }, 75 | body: '{"object":"page","entry":[{"id":1751036168465324,"time":1460923697656,"messaging":[{"sender":{"id":982337261802700},"recipient":{"id":1751036168465324},"timestamp":1460923697635,"message":{"mid":"mid.1460923697625:5c96e8279b55505308","seq":614,"text":"Test \\u00e4\\u00eb\\u00ef"}}]}]}' 76 | }, (err, res, body) => { 77 | t.error(err, 'response should not error') 78 | t.equals(res.statusCode, 200, 'request should return 200 status code') 79 | t.deepEquals(JSON.parse(body), { status: 'ok' }, 'response should be okay') 80 | t.end() 81 | }) 82 | }) 83 | 84 | t.tearDown(() => { 85 | server.close() 86 | }) 87 | }) 88 | 89 | tap.test('POST webhook with incorrect signature', (t) => { 90 | let bot = new Bot({ 91 | token: 'foo', 92 | app_secret: 'e6af24be1d683c8c911949f897eea1f6' 93 | }) 94 | 95 | bot.on('error', (err) => { 96 | t.equals(err.message, 'Message integrity check failed', 'correct error should be thrown') 97 | }) 98 | 99 | let server = http.createServer(bot.middleware()).listen(0, () => { 100 | let address = server.address() 101 | 102 | request({ 103 | url: `http://localhost:${address.port}`, 104 | method: 'POST', 105 | headers: { 106 | 'Content-Type': 'application/json', 107 | 'x-hub-signature': 'sha1=DONEZO' 108 | }, 109 | body: '{"object":"page","entry":[{"id":1751036168465324,"time":1460923697656,"messaging":[{"sender":{"id":982337261802700},"recipient":{"id":1751036168465324},"timestamp":1460923697635,"message":{"mid":"mid.1460923697625:5c96e8279b55505308","seq":614,"text":"Test \\u00e4\\u00eb\\u00ef"}}]}]}' 110 | }, (err, res, body) => { 111 | t.error(err, 'response should not error') 112 | t.equals(res.statusCode, 200, 'request should return 200 status code') 113 | t.deepEquals(JSON.parse(body), { status: 'not ok', error: 'Message integrity check failed' }, 'response should return error') 114 | t.end() 115 | }) 116 | }) 117 | 118 | t.tearDown(() => { 119 | server.close() 120 | }) 121 | }) 122 | 123 | tap.test('POST webhook with echo', (t) => { 124 | let bot = new Bot({ 125 | token: 'foo' 126 | }) 127 | 128 | bot.on('error', (err) => { 129 | t.error(err, 'bot instance returned error') 130 | t.end() 131 | }) 132 | 133 | bot.on('echo', (payload, reply, actions) => { 134 | t.type(payload, 'object', 'payload should be an object') 135 | t.type(reply, 'function', 'reply convenience function should exist') 136 | t.type(actions, 'object', 'actions should be an object') 137 | t.equals(payload.message.text, 'Test äëï', 'correct echo text was sent') 138 | }) 139 | 140 | let server = http.createServer(bot.middleware()).listen(0, () => { 141 | let address = server.address() 142 | 143 | request({ 144 | url: `http://localhost:${address.port}`, 145 | method: 'POST', 146 | headers: { 147 | 'Content-Type': 'application/json' 148 | }, 149 | body: '{"object":"page","entry":[{"id":1751036168465324,"time":1460923697656,"messaging":[{"sender":{"id":"251415921884267"},"recipient":{"id":961373540645425},"timestamp":1474195344285,"message":{"is_echo":true,"app_id":518384185035814,"mid":"mid.1474195344232:c667d9bc3076277d14","seq":6546,"text":"Test \\u00e4\\u00eb\\u00ef"}}]}]}' 150 | }, (err, res, body) => { 151 | t.error(err, 'response should not error') 152 | t.equals(res.statusCode, 200, 'request should return 200 status code') 153 | t.deepEquals(JSON.parse(body), { status: 'ok' }, 'response should be okay') 154 | t.end() 155 | }) 156 | }) 157 | 158 | t.tearDown(() => { 159 | server.close() 160 | }) 161 | }) 162 | 163 | tap.test('POST webhook with postback', (t) => { 164 | let bot = new Bot({ 165 | token: 'foo' 166 | }) 167 | 168 | bot.on('error', (err) => { 169 | t.error(err, 'bot instance returned error') 170 | t.end() 171 | }) 172 | 173 | bot.on('postback', (payload, reply, actions) => { 174 | t.type(payload, 'object', 'payload should be an object') 175 | t.type(reply, 'function', 'reply convenience function should exist') 176 | t.type(actions, 'object', 'actions should be an object') 177 | t.equals(payload.postback.payload, 'Test äëï', 'correct postback payload was sent') 178 | }) 179 | 180 | let server = http.createServer(bot.middleware()).listen(0, () => { 181 | let address = server.address() 182 | 183 | request({ 184 | url: `http://localhost:${address.port}`, 185 | method: 'POST', 186 | headers: { 187 | 'Content-Type': 'application/json' 188 | }, 189 | body: '{"object":"page","entry":[{"id":1751036168465324,"time":1460923697656,"messaging":[{"sender":{"id":982337261802700},"recipient":{"id":1751036168465324},"timestamp":1460923697635,"postback":{"payload":"Test \\u00e4\\u00eb\\u00ef"}}]}]}' 190 | }, (err, res, body) => { 191 | t.error(err, 'response should not error') 192 | t.equals(res.statusCode, 200, 'request should return 200 status code') 193 | t.deepEquals(JSON.parse(body), { status: 'ok' }, 'response should be okay') 194 | t.end() 195 | }) 196 | }) 197 | 198 | t.tearDown(() => { 199 | server.close() 200 | }) 201 | }) 202 | 203 | tap.test('POST webhook with delivery receipt', (t) => { 204 | let bot = new Bot({ 205 | token: 'foo' 206 | }) 207 | 208 | bot.on('error', (err) => { 209 | t.error(err, 'bot instance returned error') 210 | t.end() 211 | }) 212 | 213 | bot.on('postback', (payload, reply, actions) => { 214 | t.type(payload, 'object', 'payload should be an object') 215 | t.type(reply, 'function', 'reply convenience function should exist') 216 | t.type(actions, 'object', 'actions should be an object') 217 | t.equals(payload.delivery.watermark, '1460923697635', 'correct delivery receipt watermark was sent') 218 | }) 219 | 220 | let server = http.createServer(bot.middleware()).listen(0, () => { 221 | let address = server.address() 222 | 223 | request({ 224 | url: `http://localhost:${address.port}`, 225 | method: 'POST', 226 | headers: { 227 | 'Content-Type': 'application/json' 228 | }, 229 | body: '{"object":"page","entry":[{"id":1751036168465324,"time":1460923697656,"messaging":[{"sender":{"id":982337261802700},"recipient":{"id":1751036168465324},"timestamp":1460923697635,"delivery":{"mid":"mid.1460923697625:5c96e8279b55505308","seq":614, "watermark": 1460923697635}}]}]}' 230 | }, (err, res, body) => { 231 | t.error(err, 'response should not error') 232 | t.equals(res.statusCode, 200, 'request should return 200 status code') 233 | t.deepEquals(JSON.parse(body), { status: 'ok' }, 'response should be okay') 234 | t.end() 235 | }) 236 | }) 237 | 238 | t.tearDown(() => { 239 | server.close() 240 | }) 241 | }) 242 | 243 | tap.test('POST webhook with authentication callback', (t) => { 244 | let bot = new Bot({ 245 | token: 'foo' 246 | }) 247 | 248 | bot.on('error', (err) => { 249 | t.error(err, 'bot instance returned error') 250 | t.end() 251 | }) 252 | 253 | bot.on('authentication', (payload, reply, actions) => { 254 | t.type(payload, 'object', 'authentication should be an object') 255 | t.type(reply, 'function', 'reply convenience function should exist') 256 | t.type(actions, 'object', 'actions should be an object') 257 | t.equals(payload.optin.ref, 'bar', 'correct data ref was sent') 258 | }) 259 | 260 | let server = http.createServer(bot.middleware()).listen(0, () => { 261 | let address = server.address() 262 | 263 | request({ 264 | url: `http://localhost:${address.port}`, 265 | method: 'POST', 266 | headers: { 267 | 'Content-Type': 'application/json' 268 | }, 269 | body: '{"object":"page","entry":[{"id":510249619162304,"time":1461167227231,"messaging":[{"sender":{"id":1066835436691078},"recipient":{"id":510249619162304},"timestamp":1461167227231,"optin":{"ref":"bar"}}]}]}' 270 | }, (err, res, body) => { 271 | t.error(err, 'response should not error') 272 | t.equals(res.statusCode, 200, 'request should return 200 status code') 273 | t.deepEquals(JSON.parse(body), { status: 'ok' }, 'response should be okay') 274 | t.end() 275 | }) 276 | }) 277 | 278 | t.tearDown(() => { 279 | server.close() 280 | }) 281 | }) 282 | 283 | tap.test('POST webhook with read callback', (t) => { 284 | let bot = new Bot({ 285 | token: 'foo' 286 | }) 287 | 288 | bot.on('error', (err) => { 289 | t.error(err, 'bot instance returned error') 290 | t.end() 291 | }) 292 | 293 | bot.on('read', (payload, reply, actions) => { 294 | t.type(payload, 'object', 'authentication should be an object') 295 | t.type(reply, 'function', 'reply convenience function should exist') 296 | t.type(actions, 'object', 'actions should be an object') 297 | t.equals(payload.read.watermark, 1461167227231, 'correct message watermark was sent') 298 | }) 299 | 300 | let server = http.createServer(bot.middleware()).listen(0, () => { 301 | let address = server.address() 302 | 303 | request({ 304 | url: `http://localhost:${address.port}`, 305 | method: 'POST', 306 | headers: { 307 | 'Content-Type': 'application/json' 308 | }, 309 | body: '{"object":"page","entry":[{"id":510249619162304,"time":1461167227231,"messaging":[{"sender":{"id":1066835436691078},"recipient":{"id":510249619162304},"timestamp":1461167227231,"read":{"watermark":1461167227231}}]}]}' 310 | }, (err, res, body) => { 311 | t.error(err, 'response should not error') 312 | t.equals(res.statusCode, 200, 'request should return 200 status code') 313 | t.deepEquals(JSON.parse(body), { status: 'ok' }, 'response should be okay') 314 | t.end() 315 | }) 316 | }) 317 | 318 | t.tearDown(() => { 319 | server.close() 320 | }) 321 | }) 322 | 323 | tap.test('POST webhook with account_linking (linked) callback', (t) => { 324 | let bot = new Bot({ 325 | token: 'foo' 326 | }) 327 | 328 | bot.on('error', (err) => { 329 | t.error(err, 'bot instance returned error') 330 | t.end() 331 | }) 332 | 333 | bot.on('accountLinked', (payload, reply, actions) => { 334 | t.type(payload, 'object', 'authentication should be an object') 335 | t.type(reply, 'function', 'reply convenience function should exist') 336 | t.type(actions, 'object', 'actions should be an object') 337 | t.equals(payload.account_linking.status, 'linked', 'correct linking status was sent') 338 | }) 339 | 340 | let server = http.createServer(bot.middleware()).listen(0, () => { 341 | let address = server.address() 342 | 343 | request({ 344 | url: `http://localhost:${address.port}`, 345 | method: 'POST', 346 | headers: { 347 | 'Content-Type': 'application/json' 348 | }, 349 | body: '{"object":"page","entry":[{"id":510249619162304,"time":1461167227231,"messaging":[{"sender":{"id":1066835436691078},"recipient":{"id":510249619162304},"timestamp":1461167227231,"account_linking":{"status":"linked"}}]}]}' 350 | }, (err, res, body) => { 351 | t.error(err, 'response should not error') 352 | t.equals(res.statusCode, 200, 'request should return 200 status code') 353 | t.deepEquals(JSON.parse(body), { status: 'ok' }, 'response should be okay') 354 | t.end() 355 | }) 356 | }) 357 | 358 | t.tearDown(() => { 359 | server.close() 360 | }) 361 | }) 362 | 363 | tap.test('POST webhook with account_linking (unlinked) callback', (t) => { 364 | let bot = new Bot({ 365 | token: 'foo' 366 | }) 367 | 368 | bot.on('error', (err) => { 369 | t.error(err, 'bot instance returned error') 370 | t.end() 371 | }) 372 | 373 | bot.on('accountUnlinked', (payload, reply, actions) => { 374 | t.type(payload, 'object', 'authentication should be an object') 375 | t.type(reply, 'function', 'reply convenience function should exist') 376 | t.type(actions, 'object', 'actions should be an object') 377 | t.equals(payload.account_linking.status, 'unlinked', 'correct linking status was sent') 378 | }) 379 | 380 | let server = http.createServer(bot.middleware()).listen(0, () => { 381 | let address = server.address() 382 | 383 | request({ 384 | url: `http://localhost:${address.port}`, 385 | method: 'POST', 386 | headers: { 387 | 'Content-Type': 'application/json' 388 | }, 389 | body: '{"object":"page","entry":[{"id":510249619162304,"time":1461167227231,"messaging":[{"sender":{"id":1066835436691078},"recipient":{"id":510249619162304},"timestamp":1461167227231,"account_linking":{"status":"unlinked"}}]}]}' 390 | }, (err, res, body) => { 391 | t.error(err, 'response should not error') 392 | t.equals(res.statusCode, 200, 'request should return 200 status code') 393 | t.deepEquals(JSON.parse(body), { status: 'ok' }, 'response should be okay') 394 | t.end() 395 | }) 396 | }) 397 | 398 | t.tearDown(() => { 399 | server.close() 400 | }) 401 | }) 402 | --------------------------------------------------------------------------------