Repository: dtesler/slackshell Branch: master Commit: 11e9862bcf06 Files: 15 Total size: 91.7 KB Directory structure: gitextract_jrepep98/ ├── .gitignore ├── README.md ├── bot.js ├── lib/ │ ├── Botkit.js │ ├── CoreBot.js │ ├── SlackBot.js │ ├── Slack_web_api.js │ ├── Slackbot_worker.js │ ├── console_logger.js │ └── storage/ │ ├── firebase_storage.js │ ├── redis_storage.js │ ├── simple_storage.js │ └── storage_test.js ├── package.json └── tests/ └── Slack_web_api.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ node_modules/ start.sh start_button.sh db/ .DS_Store */.DS_Store .env ================================================ FILE: README.md ================================================ ## Slackshell BASH terminal for your slack team ### Getting started Clone the repository on to your desired server (Ubuntu 14.04 Recommended) ``` shell git clone https://github.com/dtesler/slackshell.git ``` next, create a bot for your team at https://my.slack.com/services/new/bot ![Creating a bot](http://i.imgur.com/M7XqlcT.gif) Once you have created the bot, copy your token ![Bot Token](http://i.imgur.com/cBmDAuV.png) and then execute the following in your project directory ``` shell token={token} node bot.js ``` where *{token}* is your token that you previously copied You can now execute terminal commands (each slack user is a separate linux user) by either sending a direct message to the bot or mentioning it in a channel where it is present. ![Slackshell test](http://i.imgur.com/oqeX8KF.gif) ###### please contribute to my crap code ================================================ FILE: bot.js ================================================ var c = require('child_process'); if (!process.env.token) { console.log('Error: Specify token in environment'); process.exit(1); } var Botkit = require('./lib/Botkit.js'); var os = require('os'); var controller = Botkit.slackbot({ debug: false, }); var bot = controller.spawn({ token: process.env.token }).startRTM(); function getUID(user, callback) { c.exec('id ' + user, function (err, response, stderr) { if (response.includes('no such user')) { // Half assed way to check if there's a user, someone, again, please fix this if you can spare the time callback(false); // Basically } else { callback(parseInt(response.replace('uid=', '').split('(')[0])); // half-assed way to pull user id, only tested on ubuntu 14.04, someone please confirm and/or fix this :D } }); } controller.hears([''],'direct_message,direct_mention',function(bot, message) { bot.api.reactions.add({ // Add a heart cause why the hell not timestamp: message.ts, channel: message.channel, name: 'heart', },function(err, res) { if (err) { bot.botkit.log('Failed to add emoji reaction :(',err); } }); getUID(message.user, function (uid) { if (!uid) { // Add user c.exec('useradd -g users -s /bin/bash -p $(echo ' + parseInt(Math.random()*999999999999999).toString(36) + ' | openssl passwd -1 -stdin) ' + message.user, function (err, stdout, stderr) { if (err) { bot.reply(message, err); } else { console.log('shell user created:', message.user); getUID(message.user, function (uid) { c.exec(message.text, {uid:uid}, function (err, stdout, stderr) { if (err) bot.reply(message, err); else bot.reply(message, stdout); }); }); } }); } else { c.exec(message.text, {uid:uid}, function (err, stdout, stderr) { if (err) bot.reply(message, err); else bot.reply(message, stdout); }); } }); }); ================================================ FILE: lib/Botkit.js ================================================ var CoreBot = require(__dirname + '/CoreBot.js'); var Slackbot = require(__dirname + '/SlackBot.js'); module.exports = { core: CoreBot, slackbot: Slackbot, }; ================================================ FILE: lib/CoreBot.js ================================================ /** * This is a module that makes a bot * It expects to receive messages via the botkit.receiveMessage function * These messages are expected to match Slack's message format. **/ var mustache = require('mustache'); var simple_storage = require(__dirname + '/storage/simple_storage.js'); var ConsoleLogger = require(__dirname + '/console_logger.js'); var LogLevels = ConsoleLogger.LogLevels; function Botkit(configuration) { var botkit = { events: {}, // this will hold event handlers config: {}, // this will hold the configuration tasks: [], taskCount: 0, convoCount: 0, memory_store: { users: {}, channels: {}, teams: {}, } }; botkit.utterances = { yes: new RegExp(/^(yes|yea|yup|yep|ya|sure|ok|y|yeah|yah)/i), no: new RegExp(/^(no|nah|nope|n)/i), }; function Conversation(task, message) { this.messages = []; this.sent = []; this.transcript = []; this.events = {}; this.vars = {}; this.topics = {}; this.topic = null; this.status = 'new'; this.task = task; this.source_message = message; this.handler = null; this.responses = {}; this.capture_options = {}; this.startTime = new Date(); this.lastActive = new Date(); this.capture = function(response) { var capture_key = this.sent[this.sent.length - 1].text; response.text = response.text.trim(); if (this.capture_options.key) { capture_key = this.capture_options.key; } // capture the question that was asked // if text is an array, get 1st if (typeof(this.sent[this.sent.length - 1].text) == 'string') { response.question = this.sent[this.sent.length - 1].text; } else { response.question = this.sent[this.sent.length - 1].text[0]; } if (this.capture_options.multiple) { if (!this.responses[capture_key]) { this.responses[capture_key] = []; } this.responses[capture_key].push(response); } else { this.responses[capture_key] = response; } }; this.handle = function(message) { this.lastActive = new Date(); this.transcript.push(message); botkit.debug('HANDLING MESSAGE IN CONVO', message); // do other stuff like call custom callbacks if (this.handler) { this.capture(message); // if the handler is a normal function, just execute it! // NOTE: anyone who passes in their own handler has to call // convo.next() to continue after completing whatever it is they want to do. if (typeof(this.handler) == 'function') { this.handler(message, this); } else { // handle might be a mapping of keyword to callback. // lets see if the message matches any of the keywords var match, patterns = this.handler; for (var p = 0; p < patterns.length; p++) { if (patterns[p].pattern && (match = message.text.match(patterns[p].pattern))) { message.match = match; patterns[p].callback(message, this); return; } } // none of the messages matched! What do we do? // if a default exists, fire it! for (var p = 0; p < patterns.length; p++) { if (patterns[p].default) { patterns[p].callback(message, this); return; } } } } else { // do nothing } }; this.activate = function() { this.status = 'active'; }; /** * active includes both ACTIVE and ENDING * in order to allow the timeout end scripts to play out **/ this.isActive = function() { return (this.status == 'active' || this.status == 'ending'); }; this.deactivate = function() { this.status = 'inactive'; }; this.say = function(message) { this.addMessage(message); }; this.sayFirst = function(message) { if (typeof(message) == 'string') { message = { text: message, channel: this.source_message.channel, }; } else { message.channel = this.source_message.channel; } this.messages.unshift(message); }; this.on = function(event, cb) { botkit.debug('Setting up a handler for', event); var events = event.split(/\,/g); for (var e in events) { if (!this.events[events[e]]) { this.events[events[e]] = []; } this.events[events[e]].push(cb); } return this; }; this.trigger = function(event, data) { if (this.events[event]) { for (var e = 0; e < this.events[event].length; e++) { var res = this.events[event][e].apply(this, data); if (res === false) { return; } } } else { botkit.debug('No handler for', event); } }; // proceed to the next message after waiting for an answer this.next = function() { this.handler = null; }; this.repeat = function() { if (this.sent.length) { this.messages.push(this.sent[this.sent.length - 1]); } else { // console.log('TRIED TO REPEAT, NOTHING TO SAY'); } }; this.silentRepeat = function() { return; }; this.addQuestion = function(message, cb, capture_options, topic) { if (typeof(message) == 'string') { message = { text: message, channel: this.source_message.channel }; } else { message.channel = this.source_message.channel; } if (capture_options) { message.capture_options = capture_options; } message.handler = cb; this.addMessage(message, topic); }; this.ask = function(message, cb, capture_options) { this.addQuestion(message, cb, capture_options, this.topic || 'default'); }; this.addMessage = function(message, topic) { if (!topic) { topic = this.topic; } if (typeof(message) == 'string') { message = { text: message, channel: this.source_message.channel, }; } else { message.channel = this.source_message.channel; } if (!this.topics[topic]) { this.topics[topic] = []; } this.topics[topic].push(message); // this is the current topic, so add it here as well if (this.topic == topic) { this.messages.push(message); } }; this.changeTopic = function(topic) { this.topic = topic; if (!this.topics[topic]) { this.topics[topic] = []; } this.messages = this.topics[topic].slice(); this.handler = null; }; this.combineMessages = function(messages) { if (!messages) { return ''; }; if (messages.length > 1) { var txt = []; var last_user = null; var multi_users = false; last_user = messages[0].user; for (var x = 0; x < messages.length; x++) { if (messages[x].user != last_user) { multi_users = true; } } last_user = ''; for (var x = 0; x < messages.length; x++) { if (multi_users && messages[x].user != last_user) { last_user = messages[x].user; if (txt.length) { txt.push(''); } txt.push('<@' + messages[x].user + '>:'); } txt.push(messages[x].text); } return txt.join('\n'); } else { if (messages.length) { return messages[0].text; } else { return messages.text; } } }; this.getResponses = function() { var res = {}; for (var key in this.responses) { res[key] = { question: this.responses[key].length ? this.responses[key][0].question : this.responses[key].question, key: key, answer: this.extractResponse(key), }; } return res; }; this.getResponsesAsArray = function() { var res = []; for (var key in this.responses) { res.push({ question: this.responses[key].length ? this.responses[key][0].question : this.responses[key].question, key: key, answer: this.extractResponse(key), }); } return res; }; this.extractResponses = function() { var res = {}; for (var key in this.responses) { res[key] = this.extractResponse(key); } return res; }; this.extractResponse = function(key) { return this.combineMessages(this.responses[key]); }; this.replaceTokens = function(text) { var vars = { identity: this.task.bot.identity, responses: this.extractResponses(), origin: this.task.source_message, vars: this.vars, }; return mustache.render(text, vars); }; this.stop = function(status) { this.handler = null; this.messages = []; this.status = status || 'stopped'; botkit.debug('Conversation is over!'); this.task.conversationEnded(this); }; this.tick = function() { var now = new Date(); if (this.isActive()) { if (this.handler) { // check timeout! // how long since task started? var duration = (now.getTime() - this.task.startTime.getTime()); // how long since last active? var lastActive = (now.getTime() - this.lastActive.getTime()); if (this.task.timeLimit && // has a timelimit (duration > this.task.timeLimit) && // timelimit is up (lastActive > this.task.timeLimit) // nobody has typed for 60 seconds at least ) { if (this.topics.timeout) { this.status = 'ending'; this.changeTopic('timeout'); } else { this.stop('timeout'); } } // otherwise do nothing } else { if (this.messages.length) { if (typeof(this.messages[0].timestamp) == 'undefined' || this.messages[0].timestamp <= now.getTime()) { var message = this.messages.shift(); //console.log('HANDLING NEW MESSAGE',message); // make sure next message is delayed appropriately if (this.messages.length && this.messages[0].delay) { this.messages[0].timestamp = now.getTime() + this.messages[0].delay; } if (message.handler) { //console.log(">>>>>> SET HANDLER IN TICK"); this.handler = message.handler; } else { this.handler = null; //console.log(">>>>>>> CLEARING HANDLER BECAUSE NO HANDLER NEEDED"); } if (message.capture_options) { this.capture_options = message.capture_options; } else { this.capture_options = {}; } this.sent.push(message); this.transcript.push(message); this.lastActive = new Date(); if (message.text || message.attachments) { // clone this object so as not to modify source var outbound = JSON.parse(JSON.stringify(message)); if (typeof(message.text) == 'string') { outbound.text = this.replaceTokens(message.text); } else { outbound.text = this.replaceTokens( message.text[Math.floor(Math.random() * message.text.length)] ); } if (this.messages.length && !message.handler) { outbound.continue_typing = true; } if (typeof(message.attachments) == 'function') { outbound.attachments = message.attachments(this); } this.task.bot.say(outbound, function(err) { if (err) { botkit.log('An error occurred while sending a message: ', err); } }); } if (message.action) { if (typeof(message.action) == 'function') { message.action(this); } else if (message.action == 'repeat') { this.repeat(); } else if (message.action == 'wait') { this.silentRepeat(); } else if (message.action == 'stop') { this.stop(); } else if (message.action == 'timeout') { this.stop('timeout'); } else if (this.topics[message.action]) { this.changeTopic(message.action); } } } else { //console.log('Waiting to send next message...'); } // end immediately instad of waiting til next tick. // if it hasn't already been ended by a message action! if (this.isActive() && !this.messages.length && !this.handler) { this.status = 'completed'; botkit.debug('Conversation is over!'); this.task.conversationEnded(this); } } else if (this.sent.length) { // sent at least 1 message this.status = 'completed'; botkit.debug('Conversation is over!'); this.task.conversationEnded(this); } } } }; botkit.debug('CREATED A CONVO FOR', this.source_message.user, this.source_message.channel); this.changeTopic('default'); }; function Task(bot, message, botkit) { this.convos = []; this.botkit = botkit; this.bot = bot; this.events = {}; this.source_message = message; this.status = 'active'; this.startTime = new Date(); this.isActive = function() { return this.status == 'active'; }; this.startConversation = function(message) { var convo = new Conversation(this, message); convo.id = botkit.convoCount++; botkit.log('> [Start] ', convo.id, ' Conversation with ', message.user, 'in', message.channel); convo.activate(); this.convos.push(convo); this.trigger('conversationStarted', [convo]); return convo; }; this.conversationEnded = function(convo) { botkit.log('> [End] ', convo.id, ' Conversation with ', convo.source_message.user, 'in', convo.source_message.channel); this.trigger('conversationEnded', [convo]); convo.trigger('end', [convo]); var actives = 0; for (var c = 0; c < this.convos.length; c++) { if (this.convos[c].isActive()) { actives++; } } if (actives == 0) { this.taskEnded(); } }; this.endImmediately = function(reason) { for (var c = 0; c < this.convos.length; c++) { if (this.convos[c].isActive()) { this.convos[c].stop(reason || 'stopped'); } } }; this.taskEnded = function() { botkit.log('[End] ', this.id, ' Task for ', this.source_message.user, 'in', this.source_message.channel); this.status = 'completed'; this.trigger('end', [this]); }; this.on = function(event, cb) { botkit.debug('Setting up a handler for', event); var events = event.split(/\,/g); for (var e in events) { if (!this.events[events[e]]) { this.events[events[e]] = []; } this.events[events[e]].push(cb); } return this; }; this.trigger = function(event, data) { if (this.events[event]) { for (var e = 0; e < this.events[event].length; e++) { var res = this.events[event][e].apply(this, data); if (res === false) { return; } } } else { botkit.debug('No handler for', event); } }; this.getResponsesByUser = function() { var users = {}; // go through all conversations // extract normalized answers for (var c = 0; c < this.convos.length; c++) { var user = this.convos[c].source_message.user; users[this.convos[c].source_message.user] = {}; var convo = this.convos[c]; users[user] = convo.extractResponses(); } return users; }; this.getResponsesBySubject = function() { var answers = {}; // go through all conversations // extract normalized answers for (var c = 0; c < this.convos.length; c++) { var convo = this.convos[c]; for (var key in convo.responses) { if (!answers[key]) { answers[key] = {}; } answers[key][convo.source_message.user] = convo.extractResponse(key); } } return answers; }; this.tick = function() { for (var c = 0; c < this.convos.length; c++) { if (this.convos[c].isActive()) { this.convos[c].tick(); } } }; }; botkit.storage = { teams: { get: function(team_id, cb) { cb(null, botkit.memory_store.teams[team_id]); }, save: function(team, cb) { botkit.log('Warning: using temporary storage. Data will be lost when process restarts.'); if (team.id) { botkit.memory_store.teams[team.id] = team; cb(null, team.id); } else { cb('No ID specified'); } }, all: function(cb) { cb(null, botkit.memory_store.teams); } }, users: { get: function(user_id, cb) { cb(null, botkit.memory_store.users[user_id]); }, save: function(user, cb) { botkit.log('Warning: using temporary storage. Data will be lost when process restarts.'); if (user.id) { botkit.memory_store.users[user.id] = user; cb(null, user.id); } else { cb('No ID specified'); } }, all: function(cb) { cb(null, botkit.memory_store.users); } }, channels: { get: function(channel_id, cb) { cb(null, botkit.memory_store.channels[channel_id]); }, save: function(channel, cb) { botkit.log('Warning: using temporary storage. Data will be lost when process restarts.'); if (channel.id) { botkit.memory_store.channels[channel.id] = channel; cb(null, channel.id); } else { cb('No ID specified'); } }, all: function(cb) { cb(null, botkit.memory_store.channels); } } }; botkit.debug = function() { if (configuration.debug) { var args = []; for (var k = 0; k < arguments.length; k++) { args.push(arguments[k]); } console.log.apply(null, args); } }; botkit.log = function() { if (configuration.log || configuration.log === undefined) { //default to true var args = []; for (var k = 0; k < arguments.length; k++) { args.push(arguments[k]); } console.log.apply(null, args); } }; botkit.hears = function(keywords, events, cb) { if (typeof(keywords) == 'string') { keywords = [keywords]; } if (typeof(events) == 'string') { events = events.split(/\,/g); } var match; for (var k = 0; k < keywords.length; k++) { var keyword = keywords[k]; for (var e = 0; e < events.length; e++) { (function(keyword) { botkit.on(events[e], function(bot, message) { if (message.text) { if (match = message.text.match(new RegExp(keyword, 'i'))) { botkit.debug('I HEARD', keyword); message.match = match; cb.apply(this, [bot, message]); return false; } } }); })(keyword); } } return this; }; botkit.on = function(event, cb) { botkit.debug('Setting up a handler for', event); var events = (typeof(event) == 'string') ? event.split(/\,/g) : event; for (var e in events) { if (!this.events[events[e]]) { this.events[events[e]] = []; } this.events[events[e]].push(cb); } return this; }; botkit.trigger = function(event, data) { if (this.events[event]) { for (var e = 0; e < this.events[event].length; e++) { var res = this.events[event][e].apply(this, data); if (res === false) { return; } } } else { botkit.debug('No handler for', event); } }; botkit.startConversation = function(bot, message, cb) { botkit.startTask(bot, message, function(task, convo) { cb(null, convo); }); }; botkit.defineBot = function(unit) { if (typeof(unit) != 'function') { throw new Error('Bot definition must be a constructor function'); } this.worker = unit; }; botkit.spawn = function(config, cb) { var worker = new this.worker(this, config); if (cb) { cb(worker); } return worker; }; botkit.startTicking = function() { if (!botkit.tickInterval) { // set up a once a second tick to process messages botkit.tickInterval = setInterval(function() { botkit.tick(); }, 1000); } }; botkit.shutdown = function() { if (botkit.tickInterval) { clearInterval(botkit.tickInterval); } }; botkit.startTask = function(bot, message, cb) { var task = new Task(bot, message, this); task.id = botkit.taskCount++; botkit.log('[Start] ', task.id, ' Task for ', message.user, 'in', message.channel); var convo = task.startConversation(message); this.tasks.push(task); if (cb) { cb(task, convo); } else { return task; } }; botkit.receiveMessage = function(bot, message) { botkit.debug('RECEIVED MESSAGE'); bot.findConversation(message, function(convo) { if (convo) { convo.handle(message); } else { botkit.trigger('message_received', [bot, message]); } }); }; botkit.tick = function() { for (var t = 0; t < botkit.tasks.length; t++) { botkit.tasks[t].tick(); } for (var t = botkit.tasks.length - 1; t >= 0; t--) { if (!botkit.tasks[t].isActive()) { botkit.tasks.splice(t, 1); } } this.trigger('tick', []); }; /** * Define a default worker bot. This function should be customized outside * of Botkit and passed in as a parameter by the developer **/ botkit.worker = function(botkit, config) { this.botkit = botkit; this.config = config; this.say = function(message, cb) { botkit.debug('SAY:', message); }; this.replyWithQuestion = function(message, question, cb) { botkit.startConversation(message, function(convo) { convo.ask(question, cb); }); }; this.reply = function(src, resp) { botkit.debug('REPLY:', resp); }; this.findConversation = function(message, cb) { botkit.debug('DEFAULT FIND CONVO'); cb(null); }; }; botkit.config = configuration; if (!configuration.logLevel) { if (configuration.debug) { configuration.logLevel = 'debug'; } else if (configuration.log === false) { configuration.logLevel = 'error'; } else { configuration.logLevel = 'info'; } } if (configuration.logger) { if (typeof configuration.logger.log === 'function') { botkit.logger = configuration.logger; } else { throw new Error('Logger object does not have a `log` method!'); } } else { botkit.logger = ConsoleLogger(console, configuration.logLevel); } botkit.log = function() { botkit.log.info.apply(botkit.log, arguments); }; Object.keys(LogLevels).forEach(function(level) { botkit.log[level] = botkit.logger.log.bind(botkit.logger, level); }); botkit.debug = botkit.log.debug; if (configuration.storage) { if ( configuration.storage.teams && configuration.storage.teams.get && configuration.storage.teams.save && configuration.storage.users && configuration.storage.users.get && configuration.storage.users.save && configuration.storage.channels && configuration.storage.channels.get && configuration.storage.channels.save ) { botkit.log('** Using custom storage system.'); botkit.storage = configuration.storage; } else { throw new Error('Storage object does not have all required methods!'); } } else if (configuration.json_file_store) { botkit.log('** Using simple storage. Saving data to ' + configuration.json_file_store); botkit.storage = simple_storage({path: configuration.json_file_store}); } else { botkit.log('** No persistent storage method specified! Data may be lost when process shuts down.'); } return botkit; } module.exports = Botkit; ================================================ FILE: lib/SlackBot.js ================================================ var Botkit = require(__dirname + '/CoreBot.js'); var request = require('request'); var express = require('express'); var bodyParser = require('body-parser'); function Slackbot(configuration) { // Create a core botkit bot var slack_botkit = Botkit(configuration || {}); // customize the bot definition, which will be used when new connections // spawn! slack_botkit.defineBot(require(__dirname + '/Slackbot_worker.js')); // set up configuration for oauth // slack_app_config should contain // { clientId, clientSecret, scopes} // https://api.slack.com/docs/oauth-scopes slack_botkit.configureSlackApp = function(slack_app_config, cb) { slack_botkit.log('** Configuring app as a Slack App!'); if (!slack_app_config || !slack_app_config.clientId || !slack_app_config.clientSecret || !slack_app_config.scopes) { throw new Error('Missing oauth config details', bot); } else { slack_botkit.config.clientId = slack_app_config.clientId; slack_botkit.config.clientSecret = slack_app_config.clientSecret; if (slack_app_config.redirectUri) slack_botkit.config.redirectUri = slack_app_config.redirectUri; if (typeof(slack_app_config.scopes) == 'string') { slack_botkit.config.scopes = slack_app_config.scopes.split(/\,/); } else { slack_botkit.config.scopes = slack_app_config.scopes; } if (cb) cb(null, bot); } return slack_botkit; }; // set up a web route that is a landing page slack_botkit.createHomepageEndpoint = function(webserver) { slack_botkit.log('** Serving app landing page at : http://MY_HOST:' + slack_botkit.config.port + '/'); // FIX THIS!!! // this is obvs not right. webserver.get('/', function(req, res) { res.send('Howdy!'); }); return slack_botkit; }; // set up a web route for receiving outgoing webhooks and/or slash commands slack_botkit.createWebhookEndpoints = function(webserver) { slack_botkit.log( '** Serving webhook endpoints for Slash commands and outgoing ' + 'webhooks at: http://MY_HOST:' + slack_botkit.config.port + '/slack/receive'); webserver.post('/slack/receive', function(req, res) { // this is a slash command if (req.body.command) { var message = {}; for (var key in req.body) { message[key] = req.body[key]; } // let's normalize some of these fields to match the rtm message format message.user = message.user_id; message.channel = message.channel_id; slack_botkit.findTeamById(message.team_id, function(err, team) { // FIX THIS // this won't work for single team bots because the team info // might not be in a db if (err || !team) { slack_botkit.log.error('Received slash command, but could not load team'); } else { message.type = 'slash_command'; // HEY THERE // Slash commands can actually just send back a response // and have it displayed privately. That means // the callback needs access to the res object // to send an optional response. res.status(200); var bot = slack_botkit.spawn(team); bot.team_info = team; bot.res = res; slack_botkit.receiveMessage(bot, message); } }); } else if (req.body.trigger_word) { var message = {}; for (var key in req.body) { message[key] = req.body[key]; } // let's normalize some of these fields to match the rtm message format message.user = message.user_id; message.channel = message.channel_id; slack_botkit.findTeamById(message.team_id, function(err, team) { // FIX THIS // this won't work for single team bots because the team info // might not be in a db if (err || !team) { slack_botkit.log.error('Received outgoing webhook but could not load team'); } else { message.type = 'outgoing_webhook'; res.status(200); var bot = slack_botkit.spawn(team); bot.res = res; bot.team_info = team; slack_botkit.receiveMessage(bot, message); // outgoing webhooks are also different. They can simply return // a response instead of using the API to reply. Maybe this is // a different type of event!! } }); } }); return slack_botkit; }; slack_botkit.saveTeam = function(team, cb) { slack_botkit.storage.teams.save(team, cb); }; // look up a team's memory and configuration and return it, or // return an error! slack_botkit.findTeamById = function(id, cb) { slack_botkit.storage.teams.get(id, cb); }; slack_botkit.setupWebserver = function(port, cb) { if (!port) { throw new Error('Cannot start webserver without a port'); } if (isNaN(port)) { throw new Error('Specified port is not a valid number'); } slack_botkit.config.port = port; slack_botkit.webserver = express(); slack_botkit.webserver.use(bodyParser.json()); slack_botkit.webserver.use(bodyParser.urlencoded({ extended: true })); slack_botkit.webserver.use(express.static(__dirname + '/public')); var server = slack_botkit.webserver.listen( slack_botkit.config.port, function() { slack_botkit.log('** Starting webserver on port ' + slack_botkit.config.port); if (cb) { cb(null, slack_botkit.webserver); } }); return slack_botkit; }; // get a team url to redirect the user through oauth process slack_botkit.getAuthorizeURL = function(team_id) { var url = 'https://slack.com/oauth/authorize'; var scopes = slack_botkit.config.scopes; url = url + '?client_id=' + slack_botkit.config.clientId + '&scope=' + scopes.join(',') + '&state=botkit'; if (team_id) { url = url + '&team=' + team_id; } if (slack_botkit.config.redirectUri) { url = url + '&redirect_uri=' + slack_botkit.config.redirectUri; } return url; }; // set up a web route for redirecting users // and collecting authentication details // https://api.slack.com/docs/oauth // https://api.slack.com/docs/oauth-scopes slack_botkit.createOauthEndpoints = function(webserver, callback) { slack_botkit.log('** Serving login URL: http://MY_HOST:' + slack_botkit.config.port + '/login'); if (!slack_botkit.config.clientId) { throw new Error( 'Cannot create oauth endpoints without calling configureSlackApp() with a clientId first'); } if (!slack_botkit.config.clientSecret) { throw new Error( 'Cannot create oauth endpoints without calling configureSlackApp() with a clientSecret first'); } if (!slack_botkit.config.scopes) { throw new Error( 'Cannot create oauth endpoints without calling configureSlackApp() with a list of scopes first'); } var call_api = function(command, options, cb) { slack_botkit.log('** API CALL: ' + 'https://slack.com/api/' + command); request.post('https://slack.com/api/' + command, function(error, response, body) { slack_botkit.debug('Got response', error, body); if (!error && response.statusCode == 200) { var json = JSON.parse(body); if (json.ok) { if (cb) cb(null, json); } else { if (cb) cb(json.error, json); } } else { if (cb) cb(error); } }).form(options); }; var oauth_access = function(options, cb) { call_api('oauth.access', options, cb); }; var auth_test = function(options, cb) { call_api('auth.test', options, cb); }; webserver.get('/login', function(req, res) { res.redirect(slack_botkit.getAuthorizeURL()); }); slack_botkit.log('** Serving oauth return endpoint: http://MY_HOST:' + slack_botkit.config.port + '/oauth'); webserver.get('/oauth', function(req, res) { var code = req.query.code; var state = req.query.state; var opts = { client_id: slack_botkit.config.clientId, client_secret: slack_botkit.config.clientSecret, code: code }; if (slack_botkit.config.redirectUri) opts.redirect_uri = slack_botkit.config.redirectUri; oauth_access(opts, function(err, auth) { if (err) { if (callback) { callback(err, req, res); } else { res.status(500).send(err); } slack_botkit.trigger('oauth_error', [err]); } else { // auth contains at least: // { access_token, scope, team_name} // May also contain: // { team_id } (not in incoming_webhook scope) // info about incoming webhooks: // { incoming_webhook: { url, channel, configuration_url} } // might also include slash commands: // { commands: ??} // what scopes did we get approved for? var scopes = auth.scope.split(/\,/); // temporarily use the token we got from the oauth // we need to call auth.test to make sure the token is valid // but also so that we reliably have the team_id field! //slack_botkit.config.token = auth.access_token; auth_test({token: auth.access_token}, function(err, identity) { if (err) { if (callback) { callback(err, req, res); } else { res.status(500).send(err); } slack_botkit.trigger('oauth_error', [err]); } else { // we need to deal with any team-level provisioning info // like incoming webhooks and bot users // and also with the personal access token from the user slack_botkit.findTeamById(identity.team_id, function(err, team) { var isnew = false; if (!team) { isnew = true; team = { id: identity.team_id, createdBy: identity.user_id, url: identity.url, name: identity.team, }; } var bot = slack_botkit.spawn(team); if (auth.incoming_webhook) { auth.incoming_webhook.token = auth.access_token; auth.incoming_webhook.createdBy = identity.user_id; team.incoming_webhook = auth.incoming_webhook; bot.configureIncomingWebhook(team.incoming_webhook); slack_botkit.trigger('create_incoming_webhook', [bot, team.incoming_webhook]); } if (auth.bot) { team.bot = { token: auth.bot.bot_access_token, user_id: auth.bot.bot_user_id, createdBy: identity.user_id, }; bot.configureRTM(team.bot); slack_botkit.trigger('create_bot', [bot, team.bot]); } slack_botkit.saveTeam(team, function(err, id) { if (err) { slack_botkit.log.error('An error occurred while saving a team: ', err); if (callback) { callback(err, req, res); } else { res.status(500).send(err); } slack_botkit.trigger('error', [err]); } else { if (isnew) { slack_botkit.trigger('create_team', [bot, team]); } else { slack_botkit.trigger('update_team', [bot, team]); } slack_botkit.storage.users.get(identity.user_id, function(err, user) { isnew = false; if (!user) { isnew = true; user = { id: identity.user_id, access_token: auth.access_token, scopes: scopes, team_id: identity.team_id, user: identity.user, }; } slack_botkit.storage.users.save(user, function(err, id) { if (err) { slack_botkit.log.error( 'An error occurred while saving a user: ', err); if (callback) { callback(err, req, res); } else { res.status(500).send(err); } slack_botkit.trigger('error', [err]); } else { if (isnew) { slack_botkit.trigger('create_user', [bot, user]); } else { slack_botkit.trigger('update_user', [bot, user]); } if (callback) { callback(null, req, res); } else { res.redirect('/'); } } }); }); } }); }); } }); } }); }); return slack_botkit; }; slack_botkit.handleSlackEvents = function() { slack_botkit.log('** Setting up custom handlers for processing Slack messages'); slack_botkit.on('message_received', function(bot, message) { if (message.ok != undefined) { // this is a confirmation of something we sent. return false; } slack_botkit.debug('DEFAULT SLACK MSG RECEIVED RESPONDER'); if ('message' == message.type) { if (message.text) { message.text = message.text.trim(); } // set up a couple of special cases based on subtype if (message.subtype && message.subtype == 'channel_join') { // someone joined. maybe do something? if (message.user == bot.identity.id) { slack_botkit.trigger('bot_channel_join', [bot, message]); return false; } else { slack_botkit.trigger('user_channel_join', [bot, message]); return false; } } else if (message.subtype && message.subtype == 'group_join') { // someone joined. maybe do something? if (message.user == bot.identity.id) { slack_botkit.trigger('bot_group_join', [bot, message]); return false; } else { slack_botkit.trigger('user_group_join', [bot, message]); return false; } } else if (message.subtype) { slack_botkit.trigger(message.subtype, [bot, message]); return false; } else if (message.channel.match(/^D/)) { // this is a direct message if (message.user == bot.identity.id) { return false; } if (!message.text) { // message without text is probably an edit return false; } // remove direct mention so the handler doesn't have to deal with it var direct_mention = new RegExp('^\<\@' + bot.identity.id + '\>', 'i'); message.text = message.text.replace(direct_mention, '') .replace(/^\s+/, '').replace(/^\:\s+/, '').replace(/^\s+/, ''); message.event = 'direct_message'; slack_botkit.trigger('direct_message', [bot, message]); return false; } else { if (message.user == bot.identity.id) { return false; } if (!message.text) { // message without text is probably an edit return false; } var direct_mention = new RegExp('^\<\@' + bot.identity.id + '\>', 'i'); var mention = new RegExp('\<\@' + bot.identity.id + '\>', 'i'); if (message.text.match(direct_mention)) { // this is a direct mention message.text = message.text.replace(direct_mention, '') .replace(/^\s+/, '').replace(/^\:\s+/, '').replace(/^\s+/, ''); message.event = 'direct_mention'; slack_botkit.trigger('direct_mention', [bot, message]); return false; } else if (message.text.match(mention)) { message.event = 'mention'; slack_botkit.trigger('mention', [bot, message]); return false; } else { message.event = 'ambient'; slack_botkit.trigger('ambient', [bot, message]); return false; } } } else { // this is a non-message object, so trigger a custom event based on the type slack_botkit.trigger(message.type, [bot, message]); } }); }; // set up the RTM message handlers once slack_botkit.handleSlackEvents(); return slack_botkit; }; module.exports = Slackbot; ================================================ FILE: lib/Slack_web_api.js ================================================ var request = require('request'); module.exports = function(bot, config) { // create a nice wrapper for the Slack API var slack_api = { api_url: 'https://slack.com/api/', // this is a simple function used to call the slack web API callAPI: function(command, options, cb) { bot.log('** API CALL: ' + slack_api.api_url + command); if (!options.token) { options.token = config.token; } bot.debug(command, options); request.post(this.api_url + command, function(error, response, body) { bot.debug('Got response', error, body); if (!error && response.statusCode == 200) { var json = JSON.parse(body); if (json.ok) { if (cb) cb(null, json); } else { if (cb) cb(json.error, json); } } else { if (cb) cb(error); } }).form(options); }, auth: { test: function(options, cb) { slack_api.callAPI('auth.test', options, cb); } }, oauth: { access: function(options, cb) { slack_api.callAPIWithoutToken('oauth.access', options, cb); } }, channels: { archive: function(options, cb) { slack_api.callAPI('channels.archive', options, cb); }, create: function(options, cb) { slack_api.callAPI('channels.create', options, cb); }, history: function(options, cb) { slack_api.callAPI('channels.history', options, cb); }, info: function(options, cb) { slack_api.callAPI('channels.info', options, cb); }, invite: function(options, cb) { slack_api.callAPI('channels.invite', options, cb); }, join: function(options, cb) { slack_api.callAPI('channels.join', options, cb); }, kick: function(options, cb) { slack_api.callAPI('channels.kick', options, cb); }, leave: function(options, cb) { slack_api.callAPI('channels.leave', options, cb); }, list: function(options, cb) { slack_api.callAPI('channels.list', options, cb); }, mark: function(options, cb) { slack_api.callAPI('channels.mark', options, cb); }, rename: function(options, cb) { slack_api.callAPI('channels.rename', options, cb); }, setPurpose: function(options, cb) { slack_api.callAPI('channels.setPurpose', options, cb); }, setTopic: function(options, cb) { slack_api.callAPI('channels.setTopic', options, cb); }, unarchive: function(options, cb) { slack_api.callAPI('channels.unarchive', options, cb); } }, chat: { delete: function(options, cb) { slack_api.callAPI('chat.delete', options, cb); }, postMessage: function(options, cb) { if (options.attachments && typeof(options.attachments) != 'string') { options.attachments = JSON.stringify(options.attachments); } slack_api.callAPI('chat.postMessage', options, cb); }, update: function(options, cb) { slack_api.callAPI('chat.update', options, cb); } }, emoji: { list: function(options, cb) { slack_api.callAPI('emoji.list', options, cb); } }, files: { delete: function(options, cb) { slack_api.callAPI('files.delete', options, cb); }, info: function(options, cb) { slack_api.callAPI('files.info', options, cb); }, list: function(options, cb) { slack_api.callAPI('files.list', options, cb); }, upload: function(options, cb) { slack_api.callAPI('files.upload', options, cb); }, }, groups: { archive: function(options, cb) { slack_api.callAPI('groups.archive', options, cb); }, close: function(options, cb) { slack_api.callAPI('groups.close', options, cb); }, create: function(options, cb) { slack_api.callAPI('groups.create', options, cb); }, createChild: function(options, cb) { slack_api.callAPI('groups.createChild', options, cb); }, history: function(options, cb) { slack_api.callAPI('groups.history', options, cb); }, info: function(options, cb) { slack_api.callAPI('groups.info', options, cb); }, invite: function(options, cb) { slack_api.callAPI('groups.invite', options, cb); }, kick: function(options, cb) { slack_api.callAPI('groups.kick', options, cb); }, leave: function(options, cb) { slack_api.callAPI('groups.leave', options, cb); }, list: function(options, cb) { slack_api.callAPI('groups.list', options, cb); }, mark: function(options, cb) { slack_api.callAPI('groups.mark', options, cb); }, open: function(options, cb) { slack_api.callAPI('groups.open', options, cb); }, rename: function(options, cb) { slack_api.callAPI('groups.rename', options, cb); }, setPurpose: function(options, cb) { slack_api.callAPI('groups.setPurpose', options, cb); }, setTopic: function(options, cb) { slack_api.callAPI('groups.setTopic', options, cb); }, unarchive: function(options, cb) { slack_api.callAPI('groups.unarchive', options, cb); }, }, im: { close: function(options, cb) { slack_api.callAPI('im.close', options, cb); }, history: function(options, cb) { slack_api.callAPI('im.history', options, cb); }, list: function(options, cb) { slack_api.callAPI('im.list', options, cb); }, mark: function(options, cb) { slack_api.callAPI('im.mark', options, cb); }, open: function(options, cb) { slack_api.callAPI('im.open', options, cb); } }, mpim: { close: function(options, cb) { slack_api.callAPI('mpim.close', options, cb); }, history: function(options, cb) { slack_api.callAPI('mpim.history', options, cb); }, list: function(options, cb) { slack_api.callAPI('mpim.list', options, cb); }, mark: function(options, cb) { slack_api.callAPI('mpim.mark', options, cb); }, open: function(options, cb) { slack_api.callAPI('mpim.open', options, cb); } }, pins: { add: function(options, cb) { slack_api.callAPI('pins.add', options, cb); }, list: function(options, cb) { slack_api.callAPI('pins.list', options, cb); }, remove: function(options, cb) { slack_api.callAPI('pins.remove', options, cb); } }, reactions: { add: function(options, cb) { slack_api.callAPI('reactions.add', options, cb); }, get: function(options, cb) { slack_api.callAPI('reactions.get', options, cb); }, list: function(options, cb) { slack_api.callAPI('reactions.list', options, cb); }, remove: function(options, cb) { slack_api.callAPI('reactions.remove', options, cb); }, }, rtm: { start: function(options, cb) { slack_api.callAPI('rtm.start', options, cb); }, }, search: { all: function(options, cb) { slack_api.callAPI('search.all', options, cb); }, files: function(options, cb) { slack_api.callAPI('search.files', options, cb); }, messages: function(options, cb) { slack_api.callAPI('search.messages', options, cb); }, }, stars: { list: function(options, cb) { slack_api.callAPI('stars.list', options, cb); }, }, team: { accessLogs: function(options, cb) { slack_api.callAPI('team.accessLogs', options, cb); }, info: function(options, cb) { slack_api.callAPI('team.info', options, cb); }, }, users: { getPresence: function(options, cb) { slack_api.callAPI('users.getPresence', options, cb); }, info: function(options, cb) { slack_api.callAPI('users.info', options, cb); }, list: function(options, cb) { slack_api.callAPI('users.list', options, cb); }, setActive: function(options, cb) { slack_api.callAPI('users.setActive', options, cb); }, setPresence: function(options, cb) { slack_api.callAPI('users.setPresence', options, cb); }, } }; return slack_api; }; ================================================ FILE: lib/Slackbot_worker.js ================================================ var Ws = require('ws'); var request = require('request'); var slackWebApi = require(__dirname + '/Slack_web_api.js'); var HttpsProxyAgent = require('https-proxy-agent'); module.exports = function(botkit, config) { var bot = { botkit: botkit, config: config || {}, utterances: botkit.utterances, api: slackWebApi(botkit, config || {}) }; /** * Set up API to send incoming webhook */ bot.configureIncomingWebhook = function(options) { if (!options.url) throw new Error('No incoming webhook URL specified!'); bot.config.incoming_webhook = options; return bot; }; bot.sendWebhook = function(options, cb) { if (!bot.config.incoming_webhook || !bot.config.incoming_webhook.url) { botkit.debug('CANNOT SEND WEBHOOK!!'); return cb && cb('No webhook url specified'); } request.post(bot.config.incoming_webhook.url, function(err, res, body) { if (err) { botkit.debug('WEBHOOK ERROR', err); return cb && cb(err); } botkit.debug('WEBHOOK SUCCESS', body); cb && cb(null, body); }).form({ payload: JSON.stringify(options) }); }; bot.configureRTM = function(config) { bot.config.token = config.token; return bot; }; bot.closeRTM = function() { if (bot.rtm) bot.rtm.close(); }; bot.startRTM = function(cb) { bot.api.rtm.start({ no_unreads: true, simple_latest: true, }, function(err, res) { if (err) { return cb && cb(err); } if (!res) { return cb && cb('Invalid response from rtm.start'); } bot.identity = res.self; bot.team_info = res.team; /** * Also available: * res.users, res.channels, res.groups, res.ims, * res.bots * * Could be stored & cached for later use. */ botkit.log.notice('** BOT ID:', bot.identity.name, '...attempting to connect to RTM!'); var agent = null; var proxyUrl = process.env.https_proxy || process.env.http_proxy; if (proxyUrl) { agent = new HttpsProxyAgent(proxyUrl); } bot.rtm = new Ws(res.url, null, {agent: agent}); bot.msgcount = 1; var pingIntervalId = null; bot.rtm.on('open', function() { pingIntervalId = setInterval(function() { bot.rtm.ping(null, null, true); }, 5000); botkit.trigger('rtm_open', [bot]); bot.rtm.on('message', function(data, flags) { var message = null; try { message = JSON.parse(data); } catch (err) { console.log('** RECEIVED BAD JSON FROM SLACK'); } /** * Lets construct a nice quasi-standard botkit message * it leaves the main slack message at the root * but adds in additional fields for internal use! * (including the teams api details) */ if (message != null) { botkit.receiveMessage(bot, message); } }); botkit.startTicking(); cb && cb(null, bot, res); }); bot.rtm.on('error', function(err) { botkit.log.error('RTM websocket error!', err); botkit.trigger('rtm_close', [bot, err]); }); bot.rtm.on('close', function() { if (pingIntervalId) { clearInterval(pingIntervalId); } botkit.trigger('rtm_close', [bot]); }); }); return bot; }; bot.identifyBot = function(cb) { if (bot.identity) { bot.identifyTeam(function(err, team) { cb(null, { name: bot.identity.name, id: bot.identity.id, team_id: team }); }); } else { /** * Note: Are there scenarios other than the RTM * where we might pull identity info, perhaps from * bot.api.auth.test on a given token? */ cb('Identity Unknown: Not using RTM api'); }; }; bot.identifyTeam = function(cb) { if (bot.team_info) return cb(null, bot.team_info.id); /** * Note: Are there scenarios other than the RTM * where we might pull identity info, perhaps from * bot.api.auth.test on a given token? */ cb('Unknown Team!'); }; /** * Convenience method for creating a DM convo. */ bot.startPrivateConversation = function(message, cb) { botkit.startTask(this, message, function(task, convo) { bot._startDM(task, message.user, function(err, dm) { convo.stop(); cb(err, dm); }); }); }; bot.startConversation = function(message, cb) { botkit.startConversation(this, message, cb); }; /** * Convenience method for creating a DM convo. */ bot._startDM = function(task, user_id, cb) { bot.api.im.open({ user: user_id }, function(err, channel) { if (err) return cb(err); cb(null, task.startConversation({ channel: channel.channel.id, user: user_id })); }); }; bot.say = function(message, cb) { botkit.debug('SAY', message); /** * Construct a valid slack message. */ var slack_message = { type: message.type || 'message', channel: message.channel, text: message.text || null, username: message.username || null, parse: message.parse || null, link_names: message.link_names || null, attachments: message.attachments ? JSON.stringify(message.attachments) : null, unfurl_links: typeof message.unfurl_links !== 'undefined' ? message.unfurl_links : null, unfurl_media: typeof message.unfurl_media !== 'undefined' ? message.unfurl_media : null, icon_url: message.icon_url || null, icon_emoji: message.icon_emoji || null, }; bot.msgcount++; if (message.icon_url || message.icon_emoji || message.username) { slack_message.as_user = false; } else { slack_message.as_user = message.as_user || true; } /** * These options are not supported by the RTM * so if they are specified, we use the web API to send messages. */ if (message.attachments || message.icon_emoji || message.username || message.icon_url) { if (!bot.config.token) { throw new Error('Cannot use web API to send messages.'); } bot.api.chat.postMessage(slack_message, function(err, res) { if (err) { cb && cb(err); } else { cb && cb(null, res); } }); } else { if (!bot.rtm) throw new Error('Cannot use the RTM API to send messages.'); slack_message.id = message.id || bot.msgcount; try { bot.rtm.send(JSON.stringify(slack_message), function(err) { if (err) { cb && cb(err); } else { cb && cb(); } }); } catch (err) { /** * The RTM failed and for some reason it didn't get caught * elsewhere. This happens sometimes when the rtm has closed but * We are sending messages anyways. * Bot probably needs to reconnect! */ cb && cb(err); } } }; bot.replyPublic = function(src, resp, cb) { if (!bot.res) { cb && cb('No web response object found'); } else { var msg = {}; if (typeof(resp) == 'string') { msg.text = resp; } else { msg = resp; } msg.channel = src.channel; msg.response_type = 'in_channel'; bot.res.json(msg); cb && cb(); } }; bot.replyPublicDelayed = function(src, resp, cb) { if (!src.response_url) { cb && cb('No response_url found'); } else { var msg = {}; if (typeof(resp) == 'string') { msg.text = resp; } else { msg = resp; } msg.channel = src.channel; msg.response_type = 'in_channel'; var requestOptions = { uri: src.response_url, method: 'POST', json: msg }; request(requestOptions, function(err, resp, body) { /** * Do something? */ if (err) { botkit.log.error('Error sending slash command response:', err); cb && cb(err); } else { cb && cb(); } }); } }; bot.replyPrivate = function(src, resp, cb) { if (!bot.res) { cb && cb('No web response object found'); } else { var msg = {}; if (typeof(resp) == 'string') { msg.text = resp; } else { msg = resp; } msg.channel = src.channel; msg.response_type = 'ephemeral'; bot.res.json(msg); cb && cb(); } }; bot.replyPrivateDelayed = function(src, resp, cb) { if (!src.response_url) { cb && cb('No response_url found'); } else { var msg = {}; if (typeof(resp) == 'string') { msg.text = resp; } else { msg = resp; } msg.channel = src.channel; msg.response_type = 'ephemeral'; var requestOptions = { uri: src.response_url, method: 'POST', json: msg }; request(requestOptions, function(err, resp, body) { /** * Do something? */ if (err) { botkit.log.error('Error sending slash command response:', err); cb && cb(err); } else { cb && cb(); } }); } }; bot.reply = function(src, resp, cb) { var msg = {}; if (typeof(resp) == 'string') { msg.text = resp; } else { msg = resp; } msg.channel = src.channel; bot.say(msg, cb); }; /** * sends a typing message to the source channel * * @param {Object} src message source */ bot.startTyping = function(src) { bot.reply(src, { type: 'typing' }); }; /** * replies with message after typing delay * * @param {Object} src message source * @param {(string|Object)} resp string or object * @param {function} cb optional request callback */ bot.replyWithTyping = function(src, resp, cb) { var text; if (typeof(resp) == 'string') { text = resp; } else { text = resp.text; } var typingLength = 1200 / 60 * text.length; typingLength = typingLength > 2000 ? 2000 : typingLength; bot.startTyping(src); setTimeout(function() { bot.reply(src, resp, cb); }, typingLength); }; /** * This handles the particulars of finding an existing conversation or * topic to fit the message into... */ bot.findConversation = function(message, cb) { botkit.debug('CUSTOM FIND CONVO', message.user, message.channel); if (message.type == 'message' || message.type == 'slash_command' || message.type == 'outgoing_webhook') { for (var t = 0; t < botkit.tasks.length; t++) { for (var c = 0; c < botkit.tasks[t].convos.length; c++) { if ( botkit.tasks[t].convos[c].isActive() && botkit.tasks[t].convos[c].source_message.user == message.user && botkit.tasks[t].convos[c].source_message.channel == message.channel ) { botkit.debug('FOUND EXISTING CONVO!'); cb(botkit.tasks[t].convos[c]); return; } } } } cb(); }; if (bot.config.incoming_webhook) bot.configureIncomingWebhook(config.incoming_webhook); if (bot.config.bot) bot.configureRTM(config.bot); return bot; }; ================================================ FILE: lib/console_logger.js ================================================ var slice = Array.prototype.slice; /** * RFC 5424 syslog severity levels, see * https://tools.ietf.org/html/rfc5424#section-6.2.1 */ var levels = [ 'emergency', 'alert', 'critical', 'error', 'warning', 'notice', 'info', 'debug' ]; var levelsByName = levels.reduce(function(out, name, index) { out[name] = index; return out; }, {}); function normalizeLogLevel(level) { if (typeof level === 'string') { level = levelsByName[level]; } if (typeof level === 'number' && level >= 0 && level < levels.length) { return level; } return false; } function ConsoleLogger(_console, maxLevel, defaultLevel) { _console = _console || console; maxLevel = normalizeLogLevel(maxLevel) || 6; defaultLevel = normalizeLogLevel(defaultLevel) || 6; return { log: function(level, message) { var normalizedLevel = normalizeLogLevel(level); if (!normalizedLevel) { message = level; normalizedLevel = defaultLevel; } var levelName = levels[normalizedLevel]; if (normalizedLevel <= maxLevel) { _console.log.apply( _console, [levelName + ': ' + message].concat(slice.call(arguments, 2)) ); } } }; } ConsoleLogger.LogLevels = levelsByName; module.exports = ConsoleLogger; ================================================ FILE: lib/storage/firebase_storage.js ================================================ /* Firebase storage module for bots. Note that this storage module does not specify how to authenticate to Firebase. There are many methods of user authentication for Firebase. Please read: https://www.firebase.com/docs/web/guide/user-auth.html Supports storage of data on a team-by-team, user-by-user, and chnnel-by-channel basis. save can be used to store arbitrary object. These objects must include an id by which they can be looked up. It is recommended to use the team/user/channel id for this purpose. Example usage of save: controller.storage.teams.save({id: message.team, foo:"bar"}, function(err){ if (err) console.log(err)` }); get looks up an object by id. Example usage of get: controller.storage.teams.get(message.team, function(err, team_data){ if (err) console.log(err) else console.log(team_data) }); */ var Firebase = require('firebase'); module.exports = function(config) { if (!config && !config.firebase_uri) throw new Error('Need to provide firebase address. This should look something like ' + '"https://botkit-example.firebaseio.com/"'); var rootRef = new Firebase(config.firebase_uri); var teamsRef = rootRef.child('teams'); var usersRef = rootRef.child('users'); var channelsRef = rootRef.child('channels'); var get = function(firebaseRef) { return function(id, cb) { firebaseRef.child(id).once('value', function(records) { cb(undefined, records.val()); }, function(err) { cb(err, undefined); } ); }; }; var save = function(firebaseRef) { return function(data, cb) { var firebase_update = {}; firebase_update[data.id] = data; firebaseRef.update(firebase_update, cb); }; }; var all = function(firebaseRef) { return function(cb) { firebaseRef.once('value', function(records) { var list = []; for (key of Object.keys(records.val())) { list.push(records.val()[key]); } cb(undefined, list); }, function(err) { cb(err, undefined); } ); }; }; var storage = { teams: { get: get(teamsRef), save: save(teamsRef), all: all(teamsRef) }, channels: { get: get(channelsRef), save: save(channelsRef), all: all(channelsRef) }, users: { get: get(usersRef), save: save(usersRef), all: all(usersRef) } }; return storage; }; ================================================ FILE: lib/storage/redis_storage.js ================================================ var redis = require('redis'); //https://github.com/NodeRedis/node_redis /* * All optional * * config = { * namespace: namespace, * host: host, * port: port * } * // see * https://github.com/NodeRedis/node_redis * #options-is-an-object-with-the-following-possible-properties for a full list of the valid options */ module.exports = function(config) { config = config || {}; config.namespace = config.namespace || 'botkit:store'; var storage = {}, client = redis.createClient(config), // could pass specific redis config here methods = config.methods || ['teams', 'users', 'channels']; // Implements required API methods for (var i = 0; i < methods.length; i++) { storage[methods[i]] = function(hash) { return { get: function(id, cb) { client.hget(config.namespace + ':' + hash, id, function(err, res) { cb(err, JSON.parse(res)); }); }, save: function(object, cb) { if (!object.id) // Silently catch this error? return cb(new Error('The given object must have an id property'), {}); client.hset(config.namespace + ':' + hash, object.id, JSON.stringify(object), cb); }, all: function(cb, options) { client.hgetall(config.namespace + ':' + hash, function(err, res) { if (err) return cb(err, {}); if (null === res) return cb(err, res); var parsed; var array = []; for (var i in res) { parsed = JSON.parse(res[i]); res[i] = parsed; array.push(parsed); } cb(err, options && options.type === 'object' ? res : array); }); }, allById: function(cb) { this.all(cb, {type: 'object'}); } }; }(methods[i]); } return storage; }; ================================================ FILE: lib/storage/simple_storage.js ================================================ /* Storage module for bots. Supports storage of data on a team-by-team, user-by-user, and chnnel-by-channel basis. save can be used to store arbitrary object. These objects must include an id by which they can be looked up. It is recommended to use the team/user/channel id for this purpose. Example usage of save: controller.storage.teams.save({id: message.team, foo:"bar"}, function(err){ if (err) console.log(err) }); get looks up an object by id. Example usage of get: controller.storage.teams.get(message.team, function(err, team_data){ if (err) console.log(err) else console.log(team_data) }); */ var Store = require('jfs'); module.exports = function(config) { if (!config) { config = { path: './', }; } var teams_db = new Store(config.path + '/teams', {saveId: 'id'}); var users_db = new Store(config.path + '/users', {saveId: 'id'}); var channels_db = new Store(config.path + '/channels', {saveId: 'id'}); var objectsToList = function(cb) { return function(err, data) { if (err) { cb(err, data); } else { cb(err, Object.keys(data).map(function(key) { return data[key]; })); } }; }; var storage = { teams: { get: function(team_id, cb) { teams_db.get(team_id, cb); }, save: function(team_data, cb) { teams_db.save(team_data.id, team_data, cb); }, all: function(cb) { teams_db.all(objectsToList(cb)); } }, users: { get: function(user_id, cb) { users_db.get(user_id, cb); }, save: function(user, cb) { users_db.save(user.id, user, cb); }, all: function(cb) { users_db.all(objectsToList(cb)); } }, channels: { get: function(channel_id, cb) { channels_db.get(channel_id, cb); }, save: function(channel, cb) { channels_db.save(channel.id, channel, cb); }, all: function(cb) { channels_db.all(objectsToList(cb)); } } }; return storage; }; ================================================ FILE: lib/storage/storage_test.js ================================================ /* Tests for storage modules. This file currently test simple_storage.js, redis_storage, and firebase_storage. If you build a new storage module, you must add it to this test file before your PR will be considered. How to add it to this test file: Add the following to the bottom of this file: // Test = require('./.js')(); check(.users); check(.channels); check(.teams); */ var test = require('unit.js'); testObj0 = {id: 'TEST0', foo: 'bar0'}; testObj1 = {id: 'TEST1', foo: 'bar1'}; var testStorageMethod = function(storageMethod) { storageMethod.save(testObj0, function(err) { test.assert(!err); storageMethod.save(testObj1, function(err) { test.assert(!err); storageMethod.get(testObj0.id, function(err, data) { test.assert(!err); console.log(data); test.assert(data.foo === testObj0.foo); }); storageMethod.all(function(err, data) { test.assert(!err); console.log(data); test.assert( data[0].foo === testObj0.foo && data[1].foo === testObj1.foo || data[0].foo === testObj1.foo && data[1].foo === testObj0.foo ); }); }); }); }; console.log('If no asserts failed then the test has passed!'); // Test simple_storage var simple_storage = require('./simple_storage.js')(); testStorageMethod(simple_storage.users); testStorageMethod(simple_storage.channels); testStorageMethod(simple_storage.teams); // Test redis_storage var redis_storage = require('./redis_storage.js')({ url: 'redis://redistogo:d175f29259bd73e442eefcaeff8e78aa@tarpon.redistogo.com:11895/' }); testStorageMethod(redis_storage.users); testStorageMethod(redis_storage.channels); testStorageMethod(redis_storage.teams); // Test firebase_storage var firebase_storage = require('./firebase_storage.js')({ firebase_uri: 'https://botkit-example.firebaseio.com' }); testStorageMethod(firebase_storage.users); testStorageMethod(firebase_storage.channels); testStorageMethod(firebase_storage.teams); ================================================ FILE: package.json ================================================ { "name": "slackshell", "version": "1.0.0", "description": "BASH in slack", "main": "bot.js", "dependencies": { "body-parser": "^1.14.2", "express": "^4.13.3", "jfs": "^0.2.6", "mustache": "^2.2.1", "request": "^2.67.0", "ws": "^1.0.1", "https-proxy-agent": "^1.0.0" }, "devDependencies": { "jscs": "^2.7.0", "mocha": "^2.4.5", "should": "^8.0.2", "winston": "^2.1.1" }, "scripts": { "pretest": "jscs ./lib/", "test": "mocha tests/*.js" }, "repository": { "type": "git", "url": "https://github.com/dtesler/slackshell.git" }, "bugs": { "url": "https://github.com/dtesler/slackshell/issues" }, "homepage": "https://github.com/dtesler/slackshell", "keywords": [ "bots", "chatbots", "slack" ], "license": "ISC", "directories": { "test": "tests" } } ================================================ FILE: tests/Slack_web_api.js ================================================ var should = require('should'); var Botkit = require('../'); var path = require('path'); var tmpdir = require('os').tmpdir(); var fs = require('fs'); var winston = require('winston'); var token = process.env.TOKEN; describe('Test', function() { it('should have a token', function(done) { should.exist(token); done(); }); it('should have Botkit instance', function(done) { should.exist(Botkit); should.exist(Botkit.core); should.exist(Botkit.slackbot); done(); }); }); describe('Botkit', function() { this.timeout(5000); it('should start and then stop', function(done) { var controller = Botkit.slackbot({debug: false}); var openIsCalled = false; controller.on('rtm_open', function(bot) { should.exist(bot); openIsCalled = true; }); controller.on('rtm_close', function(bot) { should.exist(bot); openIsCalled.should.be.true; controller.shutdown(); done(); }); controller .spawn({ token: token }) .startRTM(function(err, bot, payload) { (err === null).should.be.true; should.exist(bot); bot.closeRTM(); }); }); it('should have fail with false token', function(done) { this.timeout(5000); var controller = Botkit.slackbot({debug: false}); controller .spawn({ token: '1234' }) .startRTM(function(err, bot, payload) { should.exist(err); controller.shutdown(); done(); }); }); }); describe('Log', function() { it('should use an external logging provider', function(done) { var logFile = path.join(tmpdir, 'botkit.log'); var logger = new winston.Logger({ transports: [ new (winston.transports.File)({ filename: logFile }) ] }); logger.cli(); var controller = Botkit.slackbot({ debug: true, logger: logger }); controller .spawn({ token: '1234' }) .startRTM(function(err, bot, payload) { should.exist(err); controller.shutdown(); fs.readFile(logFile, 'utf8', function(err, res) { (err === null).should.be.true; should.exist(res); done(); }); }); }); });