├── .eslintrc ├── .gitignore ├── LICENSE ├── README.md ├── app.json ├── bot.js ├── package.json ├── server ├── controllers.js ├── index.js └── routes.js └── test.js /.eslintrc: -------------------------------------------------------------------------------- 1 | extends: standard 2 | rules: 3 | arrow-parens: 0 4 | eqeqeq: 0 5 | no-return-assign: 0 # fails for arrow functions 6 | no-var: 2 7 | semi: [2, always] 8 | space-before-function-paren: [2, never] 9 | yoda: 0 10 | arrow-spacing: 2 11 | dot-location: [2, "property"] 12 | prefer-arrow-callback: 2 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 18 | .grunt 19 | 20 | # node-waf configuration 21 | .lock-wscript 22 | 23 | # Compiled binary addons (http://nodejs.org/api/addons.html) 24 | build/Release 25 | 26 | # Dependency directory 27 | node_modules 28 | 29 | # Optional npm cache directory 30 | .npm 31 | 32 | # Optional REPL history 33 | .node_repl_history 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Pelle Almquist 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bot-wat 2 | Hi, I'm a messenger bot based on Gary Bernhardt's Wat talk (https://www.destroyallsoftware.com/talks/wat). I'm the REPL of bots. Feed me some JavaScript. 3 | 4 | 5 | 6 | Forked from https://github.com/peralmq/koa-facebook-messenger-echo-bot 7 | 8 | # Deploy 9 | Deploy to Heroku and set two environment variables 10 | ``` 11 | heroku create 12 | heroku git:remote -a 13 | heroku config set PAGE_TOKEN= VERIFY_TOKEN= -a 14 | ``` 15 | Or just click: 16 | 17 | [![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy) 18 | 19 | # Development 20 | `npm test` 21 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bot-wat", 3 | "description": "Hi, I'm a messenger bot based on Gary Bernhardt's Wat talk (https://www.destroyallsoftware.com/talks/wat).", 4 | "repository": "https://github.com/peralmq/bot-wat.git", 5 | "keywords": [ 6 | "bot", 7 | "facebook", 8 | "messenger", 9 | "koa" 10 | ], 11 | "env": { 12 | "PAGE_TOKEN": { 13 | "description": "Facebook page token" 14 | }, 15 | "VERIFY_TOKEN": { 16 | "description": "Facebook verification token" 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /bot.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const safeEval = require('safe-eval'); 4 | 5 | function echo(event) { 6 | return new Promise((resolve, reject) => { 7 | let result; 8 | try { 9 | result = safeEval(event.message.text); 10 | } catch (error) { 11 | result = error.toString(); 12 | } 13 | 14 | console.log(`Evaluated ${event.message.text} to ${result}.`); 15 | resolve({ 16 | messageText: result, 17 | senderId: event.sender.id 18 | }); 19 | }); 20 | } 21 | 22 | module.exports = { 23 | reactToMessage: echo 24 | }; 25 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bot-wat", 3 | "version": "1.0.0", 4 | "description": "Hi, I'm a messenger bot based on Gary Bernhardt's Wat talk (https://www.destroyallsoftware.com/talks/wat).", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node server/index.js", 8 | "test": "mocha" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/peralmq/bot-wat.git" 13 | }, 14 | "keywords": [ 15 | "bot", 16 | "facebook", 17 | "messenger", 18 | "koa" 19 | ], 20 | "author": "Pelle Almquist ", 21 | "license": "MIT", 22 | "bugs": { 23 | "url": "https://github.com/peralmq/bot-wat/issues" 24 | }, 25 | "homepage": "https://github.com/peralmq/bot-wat#readme", 26 | "dependencies": { 27 | "koa": "^2.0.0", 28 | "koa-bodyparser": "^3.0.0", 29 | "koa-router": "^7.0.1", 30 | "request": "^2.71.0", 31 | "safe-eval": "^0.2.0" 32 | }, 33 | "devDependencies": { 34 | "eslint": "^2.5.3", 35 | "eslint-config-standard": "^5.1.0", 36 | "eslint-plugin-babel": "^3.1.0", 37 | "eslint-plugin-promise": "^1.1.0", 38 | "eslint-plugin-standard": "^1.3.1", 39 | "mocha": "^2.0.1", 40 | "should": "^8.3.1" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /server/controllers.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const querystring = require('querystring'); 3 | const request = require('request'); 4 | 5 | function requestp(options) { 6 | return new Promise((resolve, reject) => { 7 | request(options, (err, response) => { 8 | if (!err) { 9 | return resolve(response); 10 | } 11 | reject(err); 12 | }); 13 | }); 14 | } 15 | 16 | module.exports = function( 17 | PAGE_TOKEN, 18 | VERIFY_TOKEN, 19 | bot 20 | ) { 21 | function sendMessage(options) { 22 | if (options.senderId && options.messageText) { 23 | return requestp({ 24 | url: 'https://graph.facebook.com/v2.6/me/messages', 25 | qs: {access_token: process.env.PAGE_TOKEN}, 26 | method: 'POST', 27 | json: { 28 | recipient: {id: options.senderId}, 29 | message: {text: JSON.stringify(options.messageText)} 30 | } 31 | }); 32 | } 33 | throw new Error('bot.reactToMessage needs to return senderId and messageText'); 34 | } 35 | 36 | return { 37 | react: ctx => { 38 | const promises = ctx.request.body.entry 39 | .map(entry => { 40 | return entry.messaging 41 | .map(event => { 42 | return bot.reactToMessage(event) 43 | .then(sendMessage); 44 | }); 45 | }); 46 | 47 | Promise.all(promises) 48 | .then(_ => ctx.status = 200) 49 | .catch(_ => ctx.status = 500); 50 | }, 51 | verify: ctx => { 52 | const query = querystring.parse(ctx.request.url); 53 | if (query['hub.verify_token'] === process.env.VERIFY_TOKEN) { 54 | ctx.body = query['hub.challenge']; 55 | } else { 56 | ctx.body = 'Bad verify_token'; 57 | } 58 | } 59 | }; 60 | }; 61 | -------------------------------------------------------------------------------- /server/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const Koa = require('koa'); 3 | const server = new Koa(); 4 | 5 | const bot = require('../bot'); 6 | if (!process.env.PAGE_TOKEN) { 7 | throw new Error('Missing environment variable PAGE_TOKEN'); 8 | } 9 | if (!process.env.VERIFY_TOKEN) { 10 | throw new Error('Missing environment variable VERIFY_TOKEN'); 11 | } 12 | const controllers = require('./controllers')( 13 | process.env.PAGE_TOKEN, 14 | process.env.VERIFY_TOKEN, 15 | bot 16 | ); 17 | const routes = require('./routes')(controllers); 18 | 19 | server.use(routes); 20 | server.listen(process.env.PORT || 3000); 21 | -------------------------------------------------------------------------------- /server/routes.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const router = require('koa-router')(); 3 | const bodyparser = require('koa-bodyparser'); 4 | 5 | module.exports = function(controllers) { 6 | router.use(bodyparser()); 7 | router.get('/', controllers.verify); 8 | router.post('/', controllers.react); 9 | return router.routes(); 10 | }; 11 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | /* global it, describe */ 2 | 'use strict'; 3 | require('should'); 4 | 5 | const bot = require('./bot'); 6 | 7 | describe('wat', () => { 8 | describe('reactToMessage', () => { 9 | it('should evaluate JavaSript expressions', done => { 10 | bot.reactToMessage({ 11 | message: {text: '1+1'}, 12 | sender: {id: 1} 13 | }) 14 | .then(actual => { 15 | actual.messageText.should.equal(2); 16 | done(); 17 | }) 18 | .catch(done); 19 | }); 20 | it('should handle unsafe expressions', done => { 21 | bot.reactToMessage({ 22 | message: {text: 'process'}, 23 | sender: {id: 1} 24 | }) 25 | .then(actual => { 26 | actual.messageText.should.match(/process is not defined/); 27 | done(); 28 | }) 29 | .catch(done); 30 | }); 31 | }); 32 | }); 33 | --------------------------------------------------------------------------------