├── .babelrc ├── .gitignore ├── README.md ├── dist ├── facebookbot.js ├── gentoken.js ├── index.js ├── languages.js ├── messageHandlers.js ├── slackbot.js └── storage.js ├── lib ├── facebookbot.js ├── gentoken.js ├── index.js ├── languages.js ├── messageHandlers.js ├── slackbot.js └── storage.js └── package.json /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "stage-0"], 3 | "plugins": [] 4 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # evalbot 2 | ## Installation 3 | ``` 4 | npm install --save evalbot 5 | ``` 6 | 7 | ## Getting started 8 | You need a repl.it API to use this bot, you can [get a free trial here](https://repl.it/site/api). 9 | 10 | For usage with facebook, [create a facebook app](https://developers.facebook.com/apps), 11 | then [follow these steps configure it](https://developers.facebook.com/docs/messenger-platform/product-overview) 12 | 13 | For usage with slack, simply [create a slack app](https://api.slack.com/apps/new), and 14 | add a bot user to your app. Then [create a firebase app](https://console.firebase.google.com/). 15 | 16 | You also need a google API for googl URL shortener. 17 | 18 | ### Facebook 19 | ```javascript 20 | const evalbot = require('evalbot') 21 | 22 | evalbot.start('facebook', { 23 | access_token: FB_ACCESS_TOKEN, 24 | verify_token: FB_VERIFY TOKEN, 25 | }, { 26 | port: PORT, 27 | replitApiKey: REPLIT_API_KEY, 28 | googleApiKey: GOOGLE_API_KEY, 29 | }) 30 | ``` 31 | 32 | ### Slack 33 | ```javascript 34 | const evalbot = require('evalbot') 35 | 36 | evalbot.start('slack', { 37 | clientId: SLACK_CLIENT_ID, 38 | clientSecret: SLACK_CLIENT_SECRET, 39 | redirectUri: SLACK_OAUTH_REDIRECT_URI, 40 | scopes: ['bot'], 41 | }, { 42 | port: PORT, 43 | replitApiKey: REPLIT_API_KEY, 44 | googleApiKey: GOOGLE_API_KEY, 45 | }, { 46 | apiKey: FIREBASE_API_KEY, 47 | authDomain: FIREBASE_API_KEY, 48 | databaseURL: FIREBASE_DATABASE_URL, 49 | storageBucket: FIREBASE_STORAGE_BUCKET, 50 | }) 51 | 52 | ``` 53 | 54 | Questions, suggestions, issues, and any form of contribution welcome. 55 | -------------------------------------------------------------------------------- /dist/facebookbot.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.facebookMessageEvents = undefined; 7 | 8 | var _botkit = require('botkit'); 9 | 10 | var _botkit2 = _interopRequireDefault(_botkit); 11 | 12 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 13 | 14 | exports.default = function (config) { 15 | return _botkit2.default.facebookbot(config); 16 | }; 17 | 18 | var messageEvents = 'message_received'; 19 | 20 | exports.facebookMessageEvents = messageEvents; -------------------------------------------------------------------------------- /dist/gentoken.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _crypto = require('crypto'); 8 | 9 | var _crypto2 = _interopRequireDefault(_crypto); 10 | 11 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 12 | 13 | exports.default = function (replitApiKey) { 14 | var hmac = _crypto2.default.createHmac('sha256', replitApiKey); 15 | 16 | var timeCreated = Date.now(); 17 | hmac.update(timeCreated.toString()); 18 | var msgMac = hmac.digest('base64'); 19 | 20 | return { 21 | time_created: timeCreated, 22 | msg_mac: msgMac 23 | }; 24 | }; -------------------------------------------------------------------------------- /dist/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.start = start; 7 | 8 | var _messageHandlers = require('./messageHandlers'); 9 | 10 | var _slackbot = require('./slackbot'); 11 | 12 | var _slackbot2 = _interopRequireDefault(_slackbot); 13 | 14 | var _facebookbot = require('./facebookbot'); 15 | 16 | var _facebookbot2 = _interopRequireDefault(_facebookbot); 17 | 18 | var _goo = require('goo.gl'); 19 | 20 | var _goo2 = _interopRequireDefault(_goo); 21 | 22 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 23 | 24 | function start(platform, platformConfig, serverOptions, firebaseOptions) { 25 | // Google Api Key for shortening urls (see messagneHandlers.js) 26 | _goo2.default.setKey(serverOptions.googleApiKey); 27 | 28 | var controller = void 0; 29 | var messageEvents = void 0; 30 | if (platform === 'slack') { 31 | controller = (0, _slackbot2.default)(platformConfig, firebaseOptions); 32 | messageEvents = _slackbot.slackMessageEvents; 33 | } else if (platform === 'facebook') { 34 | controller = (0, _facebookbot2.default)(platformConfig); 35 | messageEvents = _facebookbot.facebookMessageEvents; 36 | } else { 37 | throw new Error('platform ' + platform + ' not supported'); 38 | } 39 | 40 | controller.setupWebserver(serverOptions.port, function (err, webserver) { 41 | if (err) { 42 | console.error('Failed to start server'); 43 | console.error(err.message); 44 | console.error(err.stack); 45 | process.exit(); 46 | } 47 | if (platform === 'slack') { 48 | (function () { 49 | var trackBot = function trackBot(bot) { 50 | bots[bot.config.token] = bot; 51 | }; 52 | 53 | // Respawns bots incase of server restart 54 | 55 | 56 | controller.createWebhookEndpoints(controller.webserver); 57 | // Oauth redirect uri for slack 58 | controller.createOauthEndpoints(controller.webserver, function (err, req, res) { 59 | if (err) { 60 | res.status(500).send('ERROR: ' + err); 61 | return; 62 | } 63 | 64 | res.redirect('https://repl.it/site/evalbot'); 65 | }); 66 | // Slack creates a bot for each team. 67 | // Make sure we connect only once for each team 68 | var bots = {}; 69 | controller.storage.teams.all(function (err, teams) { 70 | if (err) { 71 | throw err; 72 | } 73 | 74 | teams.map(function (_ref) { 75 | var token = _ref.token; 76 | 77 | controller.spawn({ token: token }).startRTM(); 78 | }); 79 | }); 80 | 81 | controller.on('create_bot', function (bot, config) { 82 | if (bots[bot.config.token]) { 83 | // already online 84 | return; 85 | } 86 | 87 | bot.startRTM(function (err) { 88 | if (!err) { 89 | trackBot(bot); 90 | } 91 | 92 | // Send a message to person who added the bot 93 | bot.startPrivateConversation({ user: config.createdBy }, function (err, convo) { 94 | if (err) { 95 | console.log(err); 96 | return; 97 | } 98 | convo.say('I am a bot that has just joined your team'); 99 | convo.say('You must now /invite me to a channel so that I can be of use!'); 100 | }); 101 | }); 102 | }); 103 | })(); 104 | } else if (platform === 'facebook') { 105 | var bot = controller.spawn({}); 106 | controller.createWebhookEndpoints(controller.webserver, bot, function () { 107 | if (process.env.NODE_ENV !== 'production') { 108 | var localtunnel = require('localtunnel'); 109 | var tunnel = localtunnel(serverOptions.port, function (err, tunnel) { 110 | if (err) { 111 | console.log(err); 112 | process.exit(); 113 | } 114 | console.log('Your bot is available on the web at the following URL: ' + tunnel.url + '/facebook/receive'); 115 | }); 116 | tunnel.on('close', function () { 117 | console.log('Your bot is no longer available on the web at the localtunnnel.me URL.'); 118 | process.exit(); 119 | }); 120 | } 121 | console.log('fb bot started'); 122 | }); 123 | } 124 | }); 125 | 126 | controller.hears('hello', messageEvents, function (bot, message) { 127 | return bot.reply(message, 'Hello!'); 128 | }); 129 | 130 | controller.hears(['evaluate', 'eval', 'run', 'compile', '^```(.+)'], messageEvents, function (bot, message) { 131 | return (0, _messageHandlers.handleEval)(bot, message, serverOptions.replitApiKey); 132 | }); 133 | 134 | controller.hears(['langs', 'languages', 'supported languages'], messageEvents, _messageHandlers.handleLanguages); 135 | 136 | controller.hears('help', messageEvents, function (bot, message) { 137 | return bot.reply(message, 'Help page: https://repl.it/site/evalbot'); 138 | }); 139 | } -------------------------------------------------------------------------------- /dist/languages.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.getLanguageKey = getLanguageKey; 7 | exports.getSupportedLanguages = getSupportedLanguages; 8 | var supportedLanguages = ['C#', 'C', 'C++', 'C++ 11', 'F#', 'Java', 'Nodejs', 'PHP', 'Python', 'Python 3', 'Ruby', 'Rust', 'Swift']; 9 | 10 | var languagesAliases = { 11 | python3: 'python3', 12 | python: 'python', 13 | ruby: 'ruby', 14 | php: 'php', 15 | nodejs: 'nodejs', 16 | node: 'nodejs', 17 | javascript: 'nodejs', 18 | js: 'nodejs', 19 | java: 'java', 20 | cpp11: 'cpp11', 21 | 'c++11': 'cpp11', 22 | cpp: 'cpp', 23 | 'c++': 'cpp', 24 | c: 'c', 25 | csharp: 'csharp', 26 | 'c#': 'csharp', 27 | fsharp: 'fsharp', 28 | 'f#': 'fsharp', 29 | 'rust': 'rust', 30 | 'swift': 'swift' 31 | }; 32 | 33 | function getLanguageKey() { 34 | var message = arguments.length <= 0 || arguments[0] === undefined ? '' : arguments[0]; 35 | 36 | var language = message.replace(/\s/g, '').toLowerCase(); 37 | return languagesAliases[language]; 38 | } 39 | function getSupportedLanguages() { 40 | return supportedLanguages.join(',\n'); 41 | } -------------------------------------------------------------------------------- /dist/messageHandlers.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol ? "symbol" : typeof obj; }; 8 | 9 | var _slicedToArray = function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"]) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; }(); 10 | 11 | exports.handleEval = handleEval; 12 | exports.handleLanguages = handleLanguages; 13 | 14 | var _replitClient = require('replit-client'); 15 | 16 | var _replitClient2 = _interopRequireDefault(_replitClient); 17 | 18 | var _xmlhttprequest = require('xmlhttprequest'); 19 | 20 | var _languages = require('./languages'); 21 | 22 | var _gentoken = require('./gentoken'); 23 | 24 | var _gentoken2 = _interopRequireDefault(_gentoken); 25 | 26 | var _goo = require('goo.gl'); 27 | 28 | var _goo2 = _interopRequireDefault(_goo); 29 | 30 | var _entities = require('entities'); 31 | 32 | var _entities2 = _interopRequireDefault(_entities); 33 | 34 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 35 | 36 | global.XMLHttpRequest = _xmlhttprequest.XMLHttpRequest; // used for replit-client 37 | 38 | 39 | var maxMsgChunkLength = 300; 40 | 41 | function handleEval(bot, message, replitApiKey) { 42 | var langKey = void 0; 43 | var askLanguage = function askLanguage(response, convo) { 44 | convo.ask('What language should I use?', function (response, convo) { 45 | if (response.text === 'stop') { 46 | convo.say('Ok, sorry '); 47 | convo.stop(); 48 | } 49 | 50 | if (response.text === 'languages') { 51 | convo.say((0, _languages.getSupportedLanguages)()); 52 | askLanguage(response, convo); 53 | convo.next(); 54 | return; 55 | } 56 | 57 | langKey = (0, _languages.getLanguageKey)(response.text); 58 | if (!langKey) { 59 | convo.say('I\'m sorry, looks like you mistyped the language ' + 'or it\'s not supported, please try again. \n' + 'If you want to see a list of supported languages, ' + 'say `languages`'); 60 | askLanguage(response, convo); 61 | convo.next(); 62 | return; 63 | } 64 | 65 | askCode(response, convo); 66 | convo.next(); 67 | }); 68 | }; 69 | var askCode = function askCode(response, convo) { 70 | convo.ask('Type in code to eval', function (response, convo) { 71 | var code = formatCode(response.text); 72 | replitEval(replitApiKey, langKey, code).then(function (result) { 73 | handleSendResult(langKey, code, result, convo.say.bind(convo), convo.next.bind(convo)); 74 | }); 75 | }); 76 | }; 77 | 78 | var heardCommand = message.match[0]; 79 | if (heardCommand === message.text) { 80 | /** 81 | * The heard value is equal to sent text, we can 82 | * safetly assume that the message doesn't contain 83 | * language or code info. Initiate conversation 84 | **/ 85 | bot.startConversation(message, askLanguage); 86 | } else if ((0, _languages.getLanguageKey)(message.match[1])) { 87 | (function () { 88 | var langKey = (0, _languages.getLanguageKey)(message.match[1]); 89 | var code = formatCode(message.text); 90 | replitEval(replitApiKey, langKey, code).then(function (result) { 91 | return handleSendResult(langKey, code, result, bot.reply.bind(bot, message)); 92 | }); 93 | })(); 94 | } else { 95 | var _ret2 = function () { 96 | var _message$text$split = message.text.split(' '); 97 | 98 | var _message$text$split2 = _slicedToArray(_message$text$split, 3); 99 | 100 | var language = _message$text$split2[1]; 101 | var version = _message$text$split2[2]; 102 | 103 | if (!Number.isFinite(+version)) { 104 | // versions can only be finite numbers (python3/c++11) 105 | version = ''; 106 | } 107 | var langKey = (0, _languages.getLanguageKey)(language + version); 108 | var code = formatCode(message.text.substring(message.text.indexOf('```'), message.text.lastIndexOf('```') + 3)); 109 | if (!langKey || !code) { 110 | bot.reply(message, 'The language you asked for or the format is not correct.\n' + 'Your message should look like: \n' + '```@evalbot run language `​``code`​`````\n' + 'You can type `@evalbot languages` to get a list of supported languages'); 111 | return { 112 | v: void 0 113 | }; 114 | } 115 | 116 | replitEval(replitApiKey, langKey, code).then(function (result) { 117 | return handleSendResult(langKey, code, result, bot.reply.bind(bot, message)); 118 | }); 119 | }(); 120 | 121 | if ((typeof _ret2 === 'undefined' ? 'undefined' : _typeof(_ret2)) === "object") return _ret2.v; 122 | } 123 | } 124 | 125 | function handleLanguages(bot, message) { 126 | bot.reply(message, (0, _languages.getSupportedLanguages)()); 127 | } 128 | 129 | function formatCode(code) { 130 | // replace fences and language 131 | var formatted = code.replace(/(^```(\w+)?)|(```$)/g, ''); 132 | 133 | // decode xml entities 134 | formatted = _entities2.default.decodeXML(formatted); 135 | return formatted; 136 | } 137 | 138 | function replitEval(apiKey, language, code) { 139 | var repl = new _replitClient2.default('api.repl.it', '80', language, (0, _gentoken2.default)(apiKey)); 140 | 141 | var messages = ''; 142 | return repl.evaluateOnce(code, { 143 | stdout: function stdout(msg) { 144 | messages += ' ' + msg; 145 | } 146 | }).then(function (_ref) { 147 | var error = _ref.error; 148 | var data = _ref.data; 149 | return messages + '\n' + (error || '=> ' + data); 150 | }); 151 | } 152 | 153 | function getSessionShortUrl(language, code) { 154 | return _goo2.default.shorten('https://repl.it/languages/' + language + '?code=' + encodeURIComponent(code)); 155 | } 156 | 157 | function handleSendResult(language, code, codeResult, sayCommand, done) { 158 | getSessionShortUrl(language, code).then(function (url) { 159 | var urlMsg = 'Follow this link to run and edit the ' + 'code in an interactive environment: ' + url.replace('https://', ''); // Avoid slack preview 160 | 161 | if ((codeResult + urlMsg).length < maxMsgChunkLength) { 162 | sayCommand('```\n' + codeResult + '\n```\n' + urlMsg); 163 | done && done(); 164 | return; 165 | } 166 | 167 | handleSayLongResult(codeResult, sayCommand); 168 | sayCommand(urlMsg); 169 | done && done(); 170 | }); 171 | } 172 | 173 | function handleSayLongResult(message, sayCommand) { 174 | if (!message || message === '\n') { 175 | return; 176 | } 177 | 178 | var partLength = message.lastIndexOf('\n', maxMsgChunkLength); 179 | if (message.length <= maxMsgChunkLength || partLength === -1 || partLength === 0) { 180 | partLength = maxMsgChunkLength; 181 | } 182 | var messagePart = message.substr(0, partLength); 183 | sayCommand('```\n' + messagePart + '\n```\n'); 184 | 185 | handleSayLongResult(message.substr(partLength), sayCommand); 186 | } -------------------------------------------------------------------------------- /dist/slackbot.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.slackMessageEvents = undefined; 7 | 8 | exports.default = function (config, firebaseOptions) { 9 | return _botkit2.default.slackbot({ 10 | storage: (0, _storage2.default)(firebaseOptions), 11 | debug: process.env.NODE_ENV !== 'production' 12 | }).configureSlackApp(config); 13 | }; 14 | 15 | var _botkit = require('botkit'); 16 | 17 | var _botkit2 = _interopRequireDefault(_botkit); 18 | 19 | var _storage = require('./storage'); 20 | 21 | var _storage2 = _interopRequireDefault(_storage); 22 | 23 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 24 | 25 | var messageEvents = 'direct_message,direct_mention,mention'; 26 | 27 | exports.slackMessageEvents = messageEvents; -------------------------------------------------------------------------------- /dist/storage.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | exports.default = function (config) { 8 | _firebase2.default.initializeApp(config); 9 | var teamsRef = _firebase2.default.database().ref('/teams'); 10 | var channelsRef = _firebase2.default.database().ref('/channels'); 11 | var usersRef = _firebase2.default.database().ref('/users'); 12 | 13 | var storage = { 14 | teams: { 15 | get: function get(teamId, done) { 16 | teamsRef.child(teamId).once('value').then(function (data) { 17 | return done(); 18 | }, function (err) { 19 | return done(err); 20 | }); 21 | }, 22 | save: function save(teamData, done) { 23 | teamsRef.child(teamData.id).set(teamData).then(function () { 24 | return done(undefined, teamData); 25 | }, function (err) { 26 | return done(err, teamData); 27 | }); 28 | }, 29 | all: function all(done) { 30 | teamsRef.once('value').then(function (snapshot) { 31 | var teamsObj = snapshot.val(); 32 | var teamsArr = values(teamsObj); 33 | done(undefined, teamsArr); 34 | }, function (err) { 35 | return done(err); 36 | }); 37 | } 38 | }, 39 | users: { 40 | get: function get(userId, done) { 41 | usersRef.child(userId).once('value').then(function (data) { 42 | return done(); 43 | }, function (err) { 44 | return done(err); 45 | }); 46 | }, 47 | save: function save(userData, done) { 48 | usersRef.child(userData.id).set(userData).then(function () { 49 | return done(undefined, userData); 50 | }, function (err) { 51 | return done(err, userData); 52 | }); 53 | }, 54 | all: function all(done) { 55 | usersRef.once('value').then(function (snapshot) { 56 | var usersObj = snapshot.val(); 57 | var usersArr = values(usersObj); 58 | done(undefined, usersArr); 59 | }, function (err) { 60 | return done(err); 61 | }); 62 | } 63 | }, 64 | channels: { 65 | get: function get(channelId, done) { 66 | channelsRef.child(channelId).once('value').then(function (data) { 67 | return done(); 68 | }, function (err) { 69 | return done(err); 70 | }); 71 | }, 72 | save: function save(channelData, done) { 73 | channelsRef.child(channelData.id).set(channelData).then(function () { 74 | return done(undefined, channelData); 75 | }, function (err) { 76 | return done(err, channelData); 77 | }); 78 | }, 79 | all: function all(done) { 80 | channelsRef.once('value').then(function (snapshot) { 81 | var channelsObj = snapshot.val(); 82 | var channelsArr = values(channelsObj); 83 | done(undefined, channelsArr); 84 | }, function (err) { 85 | return done(err); 86 | }); 87 | } 88 | } 89 | }; 90 | 91 | return storage; 92 | }; 93 | 94 | var _firebase = require('firebase'); 95 | 96 | var _firebase2 = _interopRequireDefault(_firebase); 97 | 98 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 99 | 100 | function values(obj) { 101 | return Object.keys(obj).map(function (key) { 102 | return obj[key]; 103 | }); 104 | } -------------------------------------------------------------------------------- /lib/facebookbot.js: -------------------------------------------------------------------------------- 1 | import Botkit from 'botkit' 2 | 3 | export default (config) => Botkit.facebookbot(config) 4 | 5 | const messageEvents = 'message_received' 6 | 7 | export { messageEvents as facebookMessageEvents } 8 | -------------------------------------------------------------------------------- /lib/gentoken.js: -------------------------------------------------------------------------------- 1 | import crypto from 'crypto' 2 | 3 | export default (replitApiKey) => { 4 | const hmac = crypto.createHmac( 5 | 'sha256', 6 | replitApiKey 7 | ) 8 | 9 | const timeCreated = Date.now() 10 | hmac.update(timeCreated.toString()) 11 | const msgMac = hmac.digest('base64') 12 | 13 | return { 14 | time_created: timeCreated, 15 | msg_mac: msgMac, 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | import { handleEval, handleLanguages } from './messageHandlers' 2 | import getSlackContainer, { slackMessageEvents } from './slackbot' 3 | import getFacebookContainer, { facebookMessageEvents } from './facebookbot' 4 | import googl from 'goo.gl' 5 | 6 | export function start(platform, platformConfig, serverOptions, firebaseOptions) { 7 | // Google Api Key for shortening urls (see messagneHandlers.js) 8 | googl.setKey(serverOptions.googleApiKey) 9 | 10 | let controller 11 | let messageEvents 12 | if (platform === 'slack') { 13 | controller = getSlackContainer(platformConfig, firebaseOptions) 14 | messageEvents = slackMessageEvents 15 | } else if (platform === 'facebook') { 16 | controller = getFacebookContainer(platformConfig) 17 | messageEvents = facebookMessageEvents 18 | } else { 19 | throw new Error(`platform ${platform} not supported`) 20 | } 21 | 22 | controller.setupWebserver(serverOptions.port, (err, webserver) => { 23 | if (err) { 24 | console.error('Failed to start server') 25 | console.error(err.message) 26 | console.error(err.stack) 27 | process.exit() 28 | } 29 | if (platform === 'slack') { 30 | controller.createWebhookEndpoints(controller.webserver) 31 | // Oauth redirect uri for slack 32 | controller.createOauthEndpoints(controller.webserver, (err, req, res) => { 33 | if (err) { 34 | res.status(500).send('ERROR: ' + err) 35 | return 36 | } 37 | 38 | res.redirect('https://repl.it/site/evalbot') 39 | }) 40 | // Slack creates a bot for each team. 41 | // Make sure we connect only once for each team 42 | const bots = {} 43 | function trackBot(bot) { 44 | bots[bot.config.token] = bot 45 | } 46 | 47 | // Respawns bots incase of server restart 48 | controller.storage.teams.all((err, teams) => { 49 | if (err) { 50 | throw err 51 | } 52 | 53 | teams.map(({ token }) => { 54 | controller.spawn({ token }).startRTM() 55 | }) 56 | }) 57 | 58 | controller.on('create_bot', (bot, config) => { 59 | if (bots[bot.config.token]) { 60 | // already online 61 | return 62 | } 63 | 64 | bot.startRTM((err) => { 65 | if (!err) { 66 | trackBot(bot) 67 | } 68 | 69 | // Send a message to person who added the bot 70 | bot.startPrivateConversation( 71 | {user: config.createdBy}, 72 | (err, convo) => { 73 | if (err) { 74 | console.log(err) 75 | return 76 | } 77 | convo.say('I am a bot that has just joined your team') 78 | convo.say('You must now /invite me to a channel so that I can be of use!') 79 | } 80 | ) 81 | }) 82 | }) 83 | } else if (platform === 'facebook') { 84 | const bot = controller.spawn({}) 85 | controller.createWebhookEndpoints( 86 | controller.webserver, 87 | bot, 88 | () => { 89 | if (process.env.NODE_ENV !== 'production') { 90 | const localtunnel = require('localtunnel') 91 | const tunnel = localtunnel(serverOptions.port, (err, tunnel) => { 92 | if (err) { 93 | console.log(err) 94 | process.exit() 95 | } 96 | console.log('Your bot is available on the web at the following URL: ' + tunnel.url + '/facebook/receive') 97 | }) 98 | tunnel.on('close', function() { 99 | console.log('Your bot is no longer available on the web at the localtunnnel.me URL.') 100 | process.exit() 101 | }) 102 | } 103 | console.log('fb bot started') 104 | } 105 | ) 106 | } 107 | }) 108 | 109 | controller.hears( 110 | 'hello', 111 | messageEvents, 112 | (bot, message) => bot.reply(message, 'Hello!') 113 | ) 114 | 115 | controller.hears( 116 | ['evaluate', 'eval', 'run', 'compile', '^```(.+)'], 117 | messageEvents, 118 | (bot, message) => handleEval(bot, message, serverOptions.replitApiKey) 119 | ) 120 | 121 | controller.hears( 122 | ['langs', 'languages', 'supported languages'], 123 | messageEvents, 124 | handleLanguages 125 | ) 126 | 127 | controller.hears( 128 | 'help', 129 | messageEvents, 130 | (bot, message) => bot.reply(message, 'Help page: https://repl.it/site/evalbot') 131 | ) 132 | } 133 | -------------------------------------------------------------------------------- /lib/languages.js: -------------------------------------------------------------------------------- 1 | const supportedLanguages = [ 2 | 'C#', 3 | 'C', 4 | 'C++', 5 | 'C++ 11', 6 | 'F#', 7 | 'Java', 8 | 'Nodejs', 9 | 'PHP', 10 | 'Python', 11 | 'Python 3', 12 | 'Ruby', 13 | 'Rust', 14 | 'Swift', 15 | ] 16 | 17 | const languagesAliases = { 18 | python3: 'python3', 19 | python: 'python', 20 | ruby: 'ruby', 21 | php: 'php', 22 | nodejs: 'nodejs', 23 | node: 'nodejs', 24 | javascript: 'nodejs', 25 | js: 'nodejs', 26 | java: 'java', 27 | cpp11: 'cpp11', 28 | 'c++11': 'cpp11', 29 | cpp: 'cpp', 30 | 'c++': 'cpp', 31 | c: 'c', 32 | csharp: 'csharp', 33 | 'c#': 'csharp', 34 | fsharp: 'fsharp', 35 | 'f#': 'fsharp', 36 | 'rust': 'rust', 37 | 'swift': 'swift', 38 | } 39 | 40 | export function getLanguageKey(message = '') { 41 | const language = message.replace(/\s/g, '').toLowerCase() 42 | return languagesAliases[language] 43 | } 44 | export function getSupportedLanguages() { 45 | return supportedLanguages.join(',\n') 46 | } 47 | -------------------------------------------------------------------------------- /lib/messageHandlers.js: -------------------------------------------------------------------------------- 1 | import ReplitClient from 'replit-client' 2 | import { XMLHttpRequest } from 'xmlhttprequest' 3 | global.XMLHttpRequest = XMLHttpRequest // used for replit-client 4 | import { getLanguageKey, getSupportedLanguages } from './languages' 5 | import genToken from './gentoken' 6 | import googl from 'goo.gl' 7 | import entities from 'entities' 8 | 9 | const maxMsgChunkLength = 300 10 | 11 | export function handleEval(bot, message, replitApiKey) { 12 | let langKey 13 | const askLanguage = (response, convo) => { 14 | convo.ask('What language should I use?', (response, convo) => { 15 | if (response.text === 'stop') { 16 | convo.say('Ok, sorry ') 17 | convo.stop() 18 | } 19 | 20 | if (response.text === 'languages') { 21 | convo.say(getSupportedLanguages()) 22 | askLanguage(response, convo) 23 | convo.next() 24 | return 25 | } 26 | 27 | langKey = getLanguageKey(response.text) 28 | if (!langKey) { 29 | convo.say('I\'m sorry, looks like you mistyped the language ' + 30 | 'or it\'s not supported, please try again. \n' + 31 | 'If you want to see a list of supported languages, ' + 32 | 'say `languages`') 33 | askLanguage(response, convo) 34 | convo.next() 35 | return 36 | } 37 | 38 | askCode(response, convo) 39 | convo.next() 40 | }) 41 | } 42 | const askCode = (response, convo) => { 43 | convo.ask('Type in code to eval', (response, convo) => { 44 | const code = formatCode(response.text) 45 | replitEval(replitApiKey, langKey, code).then( 46 | result => { 47 | handleSendResult( 48 | langKey, 49 | code, 50 | result, 51 | convo.say.bind(convo), 52 | convo.next.bind(convo) 53 | ) 54 | } 55 | ) 56 | }) 57 | } 58 | 59 | const heardCommand = message.match[0] 60 | if (heardCommand === message.text) { 61 | /** 62 | * The heard value is equal to sent text, we can 63 | * safetly assume that the message doesn't contain 64 | * language or code info. Initiate conversation 65 | **/ 66 | bot.startConversation(message, askLanguage) 67 | } else if (getLanguageKey(message.match[1])) { 68 | const langKey = getLanguageKey(message.match[1]) 69 | const code = formatCode(message.text) 70 | replitEval(replitApiKey, langKey, code).then( 71 | result => handleSendResult( 72 | langKey, 73 | code, 74 | result, 75 | bot.reply.bind(bot, message) 76 | ) 77 | ) 78 | } else { 79 | let [, language, version] = message.text.split(' ') 80 | if (!Number.isFinite(+version)) { 81 | // versions can only be finite numbers (python3/c++11) 82 | version = '' 83 | } 84 | const langKey = getLanguageKey(language + version) 85 | const code = formatCode(message.text.substring( 86 | message.text.indexOf('```'), 87 | message.text.lastIndexOf('```') + 3 88 | )) 89 | if (!langKey || !code) { 90 | bot.reply( 91 | message, 92 | 'The language you asked for or the format is not correct.\n' + 93 | 'Your message should look like: \n' + 94 | '```@evalbot run language `\u200b``code`\u200b`````\n' + 95 | 'You can type `@evalbot languages` to get a list of supported languages' 96 | ) 97 | return 98 | } 99 | 100 | replitEval(replitApiKey, langKey, code).then( 101 | result => handleSendResult( 102 | langKey, 103 | code, 104 | result, 105 | bot.reply.bind(bot, message) 106 | ) 107 | ) 108 | } 109 | } 110 | 111 | export function handleLanguages(bot, message) { 112 | bot.reply(message, getSupportedLanguages()) 113 | } 114 | 115 | function formatCode(code) { 116 | // replace fences and language 117 | let formatted = code.replace(/(^```(\w+)?)|(```$)/g, '') 118 | 119 | // decode xml entities 120 | formatted = entities.decodeXML(formatted) 121 | return formatted 122 | } 123 | 124 | function replitEval(apiKey, language, code) { 125 | const repl = new ReplitClient( 126 | 'api.repl.it', 127 | '80', 128 | language, 129 | genToken(apiKey) 130 | ) 131 | 132 | let messages = '' 133 | return repl.evaluateOnce(code, { 134 | stdout: (msg) => { messages += ' ' + msg }, 135 | }).then( 136 | ({error, data}) => messages + '\n' + (error || '=> ' + data) 137 | ) 138 | } 139 | 140 | function getSessionShortUrl(language, code) { 141 | return googl.shorten( 142 | `https://repl.it/languages/${language}?code=${encodeURIComponent(code)}` 143 | ) 144 | } 145 | 146 | function handleSendResult(language, code, codeResult, sayCommand, done) { 147 | getSessionShortUrl(language, code).then( 148 | (url) => { 149 | const urlMsg = 'Follow this link to run and edit the ' + 150 | 'code in an interactive environment: ' + 151 | url.replace('https://', '') // Avoid slack preview 152 | 153 | if ((codeResult + urlMsg).length < maxMsgChunkLength) { 154 | sayCommand('```\n' + codeResult + '\n```\n' + urlMsg) 155 | done && done() 156 | return 157 | } 158 | 159 | handleSayLongResult(codeResult, sayCommand) 160 | sayCommand(urlMsg) 161 | done && done() 162 | } 163 | ) 164 | } 165 | 166 | function handleSayLongResult(message, sayCommand) { 167 | if (!message || message === '\n') { 168 | return 169 | } 170 | 171 | let partLength = message.lastIndexOf('\n', maxMsgChunkLength) 172 | if (message.length <= maxMsgChunkLength || 173 | partLength === -1 || 174 | partLength === 0) { 175 | partLength = maxMsgChunkLength 176 | } 177 | const messagePart = message.substr(0, partLength) 178 | sayCommand('```\n' + messagePart + '\n```\n') 179 | 180 | handleSayLongResult(message.substr(partLength), sayCommand) 181 | } 182 | -------------------------------------------------------------------------------- /lib/slackbot.js: -------------------------------------------------------------------------------- 1 | import Botkit from 'botkit' 2 | import storage from './storage' 3 | 4 | export default function(config, firebaseOptions) { 5 | return Botkit.slackbot({ 6 | storage: storage(firebaseOptions), 7 | debug: process.env.NODE_ENV !== 'production', 8 | }).configureSlackApp(config) 9 | } 10 | const messageEvents = 'direct_message,direct_mention,mention' 11 | 12 | export { messageEvents as slackMessageEvents } 13 | -------------------------------------------------------------------------------- /lib/storage.js: -------------------------------------------------------------------------------- 1 | import firebase from 'firebase' 2 | 3 | export default function(config) { 4 | firebase.initializeApp(config) 5 | const teamsRef = firebase.database().ref('/teams') 6 | const channelsRef = firebase.database().ref('/channels') 7 | const usersRef = firebase.database().ref('/users') 8 | 9 | const storage = { 10 | teams: { 11 | get: (teamId, done) => { 12 | teamsRef.child(teamId) 13 | .once('value') 14 | .then( 15 | (data) => done(), 16 | (err) => done(err) 17 | ) 18 | }, 19 | save: (teamData, done) => { 20 | teamsRef.child(teamData.id) 21 | .set(teamData) 22 | .then( 23 | () => done(undefined, teamData), 24 | (err) => done(err, teamData) 25 | ) 26 | }, 27 | all: (done) => { 28 | teamsRef.once('value') 29 | .then( 30 | (snapshot) => { 31 | const teamsObj = snapshot.val() 32 | const teamsArr = values(teamsObj) 33 | done(undefined, teamsArr) 34 | }, 35 | (err) => done(err) 36 | ) 37 | }, 38 | }, 39 | users: { 40 | get: (userId, done) => { 41 | usersRef.child(userId) 42 | .once('value') 43 | .then( 44 | (data) => done(), 45 | (err) => done(err) 46 | ) 47 | }, 48 | save: (userData, done) => { 49 | usersRef.child(userData.id) 50 | .set(userData) 51 | .then( 52 | () => done(undefined, userData), 53 | (err) => done(err, userData) 54 | ) 55 | }, 56 | all: (done) => { 57 | usersRef.once('value') 58 | .then( 59 | (snapshot) => { 60 | const usersObj = snapshot.val() 61 | const usersArr = values(usersObj) 62 | done(undefined, usersArr) 63 | }, 64 | (err) => done(err) 65 | ) 66 | }, 67 | }, 68 | channels: { 69 | get: (channelId, done) => { 70 | channelsRef.child(channelId) 71 | .once('value') 72 | .then( 73 | (data) => done(), 74 | (err) => done(err) 75 | ) 76 | }, 77 | save: (channelData, done) => { 78 | channelsRef.child(channelData.id) 79 | .set(channelData) 80 | .then( 81 | () => done(undefined, channelData), 82 | (err) => done(err, channelData) 83 | ) 84 | }, 85 | all: (done) => { 86 | channelsRef.once('value') 87 | .then( 88 | (snapshot) => { 89 | const channelsObj = snapshot.val() 90 | const channelsArr = values(channelsObj) 91 | done(undefined, channelsArr) 92 | }, 93 | (err) => done(err) 94 | ) 95 | }, 96 | }, 97 | } 98 | 99 | return storage 100 | } 101 | 102 | function values(obj) { 103 | return Object.keys(obj).map(key => obj[key]) 104 | } 105 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "evalbot", 3 | "version": "0.1.6", 4 | "description": "", 5 | "main": "dist/index.js", 6 | "scripts": { 7 | "build": "babel lib -d dist" 8 | }, 9 | "author": "repl.it", 10 | "license": "MIT", 11 | "devDependencies": { 12 | "babel-cli": "^6.10.1", 13 | "babel-preset-es2015": "^6.9.0", 14 | "babel-preset-stage-0": "^6.5.0", 15 | "babel-register": "^6.9.0", 16 | "localtunnel": "^1.8.1" 17 | }, 18 | "dependencies": { 19 | "botkit": "^0.2.2", 20 | "entities": "^1.1.1", 21 | "firebase": "^3.1.0", 22 | "goo.gl": "^0.1.4", 23 | "replit-client": "^0.4.0", 24 | "xmlhttprequest": "^1.8.0" 25 | } 26 | } 27 | --------------------------------------------------------------------------------