UNPKG

mangrove-botkit

Version:

Building blocks for Building Bots

1,433 lines (1,170 loc) 48.5 kB
/** * This is a module that makes a bot * It expects to receive messages via the botkit.ingest function **/ var mustache = require('mustache'); var simple_storage = require(__dirname + '/storage/simple_storage.js'); var ConsoleLogger = require(__dirname + '/console_logger.js'); var LogLevels = ConsoleLogger.LogLevels; var ware = require('ware'); var clone = require('clone'); var fs = require('fs'); var studio = require('./Studio.js'); var os = require('os'); var async = require('async'); var PKG_VERSION = require('../package.json').version; var express = require('express'); var bodyParser = require('body-parser'); function Botkit(configuration) { var botkit = { events: {}, // this will hold event handlers config: {}, // this will hold the configuration tasks: [], taskCount: 0, convoCount: 0, my_version: null, my_user_agent: null, memory_store: { users: {}, channels: {}, teams: {}, } }; /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/ // TODO: externalize this into some sort of utterances.json file botkit.utterances = { yes: new RegExp(/^(yes|yea|yup|yep|ya|sure|ok|y|yeah|yah)/i), no: new RegExp(/^(no|nah|nope|n)/i), quit: new RegExp(/^(quit|cancel|end|stop|done|exit|nevermind|never mind)/i) }; /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/ // define some middleware points where custom functions // can plug into key points of botkits process botkit.middleware = { spawn: ware(), ingest: ware(), normalize: ware(), categorize: ware(), receive: ware(), heard: ware(), // best place for heavy i/o because fewer messages capture: ware(), format: ware(), send: ware(), }; botkit.ingest = function(bot, payload, source) { // keep an unmodified copy of the message payload.raw_message = clone(payload); payload._pipeline = { stage: 'ingest', }; botkit.middleware.ingest.run(bot, payload, source, function(err, bot, payload, source) { if (err) { console.error('An error occured in the ingest middleware: ', err); return; } botkit.normalize(bot, payload); }); }; botkit.normalize = function(bot, payload) { payload._pipeline.stage = 'normalize'; botkit.middleware.normalize.run(bot, payload, function(err, bot, message) { if (err) { console.error('An error occured in the normalize middleware: ', err); return; } if (!message.type) { message.type = 'message_received'; } botkit.categorize(bot, message); }); }; botkit.categorize = function(bot, message) { message._pipeline.stage = 'categorize'; botkit.middleware.categorize.run(bot, message, function(err, bot, message) { if (err) { console.error('An error occured in the categorize middleware: ', err); return; } botkit.receiveMessage(bot, message); }); }; botkit.receiveMessage = function(bot, message) { message._pipeline.stage = 'receive'; botkit.middleware.receive.run(bot, message, function(err, bot, message) { if (err) { console.error('An error occured in the receive middleware: ', err); return; } else { botkit.debug('RECEIVED MESSAGE'); bot.findConversation(message, function(convo) { if (convo) { convo.handle(message); } else { botkit.trigger(message.type, [bot, message]); } }); } }); }; /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/ function Conversation(task, message) { this.messages = []; this.sent = []; this.transcript = []; this.context = { user: message.user, channel: message.channel, bot: task.bot, }; this.events = {}; this.vars = {}; this.threads = {}; this.thread = 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(); /** will be pointing to a callback which will be called after timeout, * conversation will be not be ended and should be taken care by callback */ this.timeOutHandler = null; this.collectResponse = function(key, value) { this.responses[key] = value; }; this.capture = function(response, cb) { var that = this; var capture_key = this.sent[this.sent.length - 1].text; botkit.middleware.capture.run(that.task.bot, response, that, function(err, bot, response, convo) { if (response.text) { response.text = response.text.trim(); } else { response.text = ''; } if (that.capture_options.key != undefined) { capture_key = that.capture_options.key; } // capture the question that was asked // if text is an array, get 1st if (typeof(that.sent[that.sent.length - 1].text) == 'string') { response.question = that.sent[that.sent.length - 1].text; } else if (Array.isArray(that.sent[that.sent.length - 1].text)) { response.question = that.sent[that.sent.length - 1].text[0]; } else { response.question = ''; } if (that.capture_options.multiple) { if (!that.responses[capture_key]) { that.responses[capture_key] = []; } that.responses[capture_key].push(response); } else { that.responses[capture_key] = response; } if (cb) cb(response); }); }; this.handle = function(message) { var that = this; 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, function(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(that.handler) == 'function') { that.handler(message, that); } else { // handle might be a mapping of keyword to callback. // lets see if the message matches any of the keywords var match, patterns = that.handler; for (var p = 0; p < patterns.length; p++) { if (patterns[p].pattern && botkit.hears_test([patterns[p].pattern], message)) { botkit.middleware.heard.run(that.task.bot, message, function(err, bot, message) { patterns[p].callback(message, that); }); 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) { botkit.middleware.heard.run(that.task.bot, message, function(err, bot, message) { patterns[p].callback(message, that); }); return; } } } }); } else { // do nothing } }; this.setVar = function(field, value) { if (!this.vars) { this.vars = {}; } this.vars[field] = value; }; this.activate = function() { this.task.trigger('conversationStarted', [this]); this.task.botkit.trigger('conversationStarted', [this.task.bot, this]); 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 { } }; // 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, thread) { 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, thread); }; this.ask = function(message, cb, capture_options) { this.addQuestion(message, cb, capture_options, this.thread || 'default'); }; this.addMessage = function(message, thread) { if (!thread) { thread = this.thread; } if (typeof(message) == 'string') { message = { text: message, channel: this.source_message.channel, }; } else { message.channel = this.source_message.channel; } if (!this.threads[thread]) { this.threads[thread] = []; } this.threads[thread].push(message); // this is the current topic, so add it here as well if (this.thread == thread) { this.messages.push(message); } }; // how long should the bot wait while a user answers? this.setTimeout = function(timeout) { this.task.timeLimit = timeout; }; // For backwards compatibility, wrap gotoThread in its previous name this.changeTopic = function(topic) { this.gotoThread(topic); }; this.hasThread = function(thread) { return (this.threads[thread] != undefined); }; this.transitionTo = function(thread, message) { // add a new transition thread // add this new message to it // set that message action to execute the actual transition // then change threads to transition thread var num = 1; while (this.hasThread('transition_' + num)) { num++; } var threadname = 'transition_' + num; if (typeof(message) == 'string') { message = { text: message, action: thread }; } else { message.action = thread; } this.addMessage(message, threadname); this.gotoThread(threadname); }; this.beforeThread = function(thread, callback) { if (!this.before_hooks) { this.before_hooks = {}; } if (!this.before_hooks[thread]) { this.before_hooks[thread] = []; } this.before_hooks[thread].push(callback); }; this.gotoThread = function(thread) { var that = this; that.next_thread = thread; that.processing = true; var makeChange = function() { if (!that.hasThread(that.next_thread)) { if (that.next_thread == 'default') { that.threads[that.next_thread] = []; } else { botkit.debug('WARN: gotoThread() to an invalid thread!', thread); that.stop('unknown_thread'); return; } } that.thread = that.next_thread; that.messages = that.threads[that.next_thread].slice(); that.handler = null; that.processing = false; }; if (that.before_hooks && that.before_hooks[that.next_thread]) { // call any beforeThread hooks in sequence async.eachSeries(this.before_hooks[that.next_thread], function(before_hook, next) { before_hook(that, next); }, function(err) { if (!err) { makeChange(); } }); } else { makeChange(); } }; this.combineMessages = function(messages) { if (!messages) { return ''; } if (Array.isArray(messages) && !messages.length) { 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.replaceAttachmentTokens = function(attachments) { if (attachments && attachments.length) { for (var a = 0; a < attachments.length; a++) { for (var key in attachments[a]) { if (typeof(attachments[a][key]) == 'string') { attachments[a][key] = this.replaceTokens(attachments[a][key]); } else { attachments[a][key] = this.replaceAttachmentTokens(attachments[a][key]); } } } } else { for (var a in attachments) { if (typeof(attachments[a]) == 'string') { attachments[a] = this.replaceTokens(attachments[a]); } else { attachments[a] = this.replaceAttachmentTokens(attachments[a]); } } } return attachments; }; this.replaceTokens = function(text) { var vars = { identity: this.task.bot.identity, responses: this.extractResponses(), origin: this.task.source_message, vars: this.vars, }; var rendered = ''; try { rendered = mustache.render(text, vars); } catch (err) { botkit.log('Error in message template. Mustache failed with error: ', err); rendered = text; }; return rendered; }; this.stop = function(status) { this.handler = null; this.messages = []; this.status = status || 'stopped'; botkit.debug('Conversation is over with status ' + this.status); this.task.conversationEnded(this); }; // was this conversation successful? // return true if it was completed // otherwise, return false // false could indicate a variety of failed states: // manually stopped, timed out, etc this.successful = function() { // if the conversation is still going, it can't be successful yet if (this.isActive()) { return false; } if (this.status == 'completed') { return true; } else { return false; } }; this.cloneMessage = function(message) { // clone this object so as not to modify source var outbound = clone(message); if (typeof(message.text) == 'string') { outbound.text = this.replaceTokens(message.text); } else if (message.text) { outbound.text = this.replaceTokens( message.text[Math.floor(Math.random() * message.text.length)] ); } if (outbound.attachments) { outbound.attachments = this.replaceAttachmentTokens(outbound.attachments); } if (outbound.attachment) { // pick one variation of the message text at random if (outbound.attachment.payload.text && typeof(outbound.attachment.payload.text) != 'string') { outbound.attachment.payload.text = this.replaceTokens( outbound.attachment.payload.text[ Math.floor(Math.random() * outbound.attachment.payload.text.length) ] ); } outbound.attachment = this.replaceAttachmentTokens([outbound.attachment])[0]; } if (this.messages.length && !message.handler) { outbound.continue_typing = true; } if (typeof(message.attachments) == 'function') { outbound.attachments = message.attachments(this); } return outbound; }; this.onTimeout = function(handler) { if (typeof(handler) == 'function') { this.timeOutHandler = handler; } else { botkit.debug('Invalid timeout function passed to onTimeout'); } }; this.tick = function() { var now = new Date(); if (this.isActive()) { if (this.processing) { // do nothing. The bot is waiting for async process to complete. } else 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 timeoutHandler is set then call it, otherwise follow the normal flow // this will not break others code, after the update if (this.timeOutHandler) { this.timeOutHandler(this); } else if (this.hasThread('on_timeout')) { this.status = 'ending'; this.gotoThread('on_timeout'); } else { this.stop('timeout'); } } // otherwise do nothing } else { if (this.messages.length) { if (this.sent.length && !this.sent[this.sent.length - 1].sent ) { return; } if (this.task.bot.botkit.config.require_delivery && this.sent.length && !this.sent[this.sent.length - 1].delivered ) { return; } 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.lastActive = new Date(); // is there any text? // or an attachment? (facebook) // or multiple attachments (slack) if (message.text || message.attachments || message.attachment) { var outbound = this.cloneMessage(message); var that = this; outbound.sent_timestamp = new Date().getTime(); that.sent.push(outbound); that.transcript.push(outbound); this.task.bot.reply(this.source_message, outbound, function(err, sent_message) { if (err) { botkit.log('An error occurred while sending a message: ', err); // even though an error occured, set sent to true // this will allow the conversation to keep going even if one message fails // TODO: make a message that fails to send _resend_ at least once that.sent[that.sent.length - 1].sent = true; that.sent[that.sent.length - 1].api_response = err; } else { that.sent[that.sent.length - 1].sent = true; that.sent[that.sent.length - 1].api_response = sent_message; // if sending via slack's web api, there is no further confirmation // so we can mark the message delivered if (that.task.bot.type == 'slack' && sent_message && sent_message.ts) { that.sent[that.sent.length - 1].delivered = true; } that.trigger('sent', [sent_message]); } }); } 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.threads[message.action]) { this.gotoThread(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.processing) { this.stop('completed'); } } else if (this.sent.length) { // sent at least 1 message this.stop('completed'); } } } }; botkit.debug('CREATED A CONVO FOR', this.source_message.user, this.source_message.channel); this.gotoThread('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.createConversation = function(message) { var convo = new Conversation(this, message); convo.id = botkit.convoCount++; this.convos.push(convo); return convo; }; this.startConversation = function(message) { var convo = this.createConversation(message); botkit.log('> [Start] ', convo.id, ' Conversation with ', message.user, 'in', message.channel); convo.activate(); 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]); this.botkit.trigger('conversationEnded', [bot, 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; } } } }; 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); } } }; /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/ /** * hears_regexp - default string matcher uses regular expressions * * @param {array} tests patterns to match * @param {object} message message object with various fields * @return {boolean} whether or not a pattern was matched */ botkit.hears_regexp = function(tests, message) { for (var t = 0; t < tests.length; t++) { if (message.text) { // the pattern might be a string to match (including regular expression syntax) // or it might be a prebuilt regular expression var test = null; if (typeof(tests[t]) == 'string') { try { test = new RegExp(tests[t], 'i'); } catch (err) { botkit.log('Error in regular expression: ' + tests[t] + ': ' + err); return false; } if (!test) { return false; } } else { test = tests[t]; } if (match = message.text.match(test)) { message.match = match; return true; } } } return false; }; /** * changeEars - change the default matching function * * @param {function} new_test a function that accepts (tests, message) and returns a boolean */ botkit.changeEars = function(new_test) { botkit.hears_test = new_test; }; botkit.hears = function(keywords, events, middleware_or_cb, cb) { // the third parameter is EITHER a callback handler // or a middleware function that redefines how the hear works var test_function = botkit.hears_test; if (cb) { test_function = middleware_or_cb; } else { cb = middleware_or_cb; } if (typeof(keywords) == 'string') { keywords = [keywords]; } if (keywords instanceof RegExp) { keywords = [keywords]; } if (typeof(events) == 'string') { events = events.split(/\,/g).map(function(str) { return str.trim(); }); } for (var e = 0; e < events.length; e++) { (function(keywords, test_function) { botkit.on(events[e], function(bot, message) { if (test_function && test_function(keywords, message)) { botkit.debug('I HEARD', keywords); botkit.middleware.heard.run(bot, message, function(err, bot, message) { cb.apply(this, [bot, message]); botkit.trigger('heard_trigger', [bot, keywords, message]); }); return false; } }); })(keywords, test_function); } 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; } } } }; botkit.startConversation = function(bot, message, cb) { botkit.startTask(bot, message, function(task, convo) { cb(null, convo); }); }; botkit.createConversation = function(bot, message, cb) { var task = new Task(bot, message, this); task.id = botkit.taskCount++; var convo = task.createConversation(message); this.tasks.push(task); 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); // mutate the worker so that we can call middleware worker.say = function(message, cb) { var platform_message = {}; botkit.middleware.send.run(worker, message, function(err, worker, message) { if (err) { botkit.log('An error occured in the send middleware:: ' + err); } else { botkit.middleware.format.run(worker, message, platform_message, function(err, worker, message, platform_message) { if (err) { botkit.log('An error occured in the format middleware: ' + err); } else { worker.send(platform_message, cb); } }); } }); }; // add platform independent convenience methods worker.startConversation = function(message, cb) { botkit.startConversation(worker, message, cb); }; worker.createConversation = function(message, cb) { botkit.createConversation(worker, message, cb); }; botkit.middleware.spawn.run(worker, function(err, worker) { if (err) { botkit.log('Error in middlware.spawn.run: ' + err); } else { botkit.trigger('spawned', [worker]); 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(); }, 1500); } }; 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.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', []); }; // Provide a fairly simple Express-based webserver botkit.setupWebserver = function(port, cb) { if (!port) { throw new Error('Cannot start webserver without a port'); } var static_dir = process.cwd() + '/public'; if (botkit.config && botkit.config.webserver && botkit.config.webserver.static_dir) static_dir = botkit.config.webserver.static_dir; botkit.config.port = port; botkit.webserver = express(); botkit.webserver.use(bodyParser.json()); botkit.webserver.use(bodyParser.urlencoded({ extended: true })); botkit.webserver.use(express.static(static_dir)); var server = botkit.webserver.listen( botkit.config.port, botkit.config.hostname, function() { botkit.log('** Starting webserver on port ' + botkit.config.port); if (cb) { cb(null, botkit.webserver); } botkit.trigger('webserver_up', [botkit.webserver]); }); return botkit; }; /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/ /** * 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.userAgent = function() { if (!botkit.my_user_agent) { // set user agent to Botkit var ua = 'Botkit/' + botkit.version(); // add OS info ua = ua + ' ' + os.platform() + '/' + os.release(); // add Node info ua = ua + ' ' + 'node/' + process.version.replace('v', ''); botkit.my_user_agent = ua; } return botkit.my_user_agent; }; botkit.version = function() { if (!botkit.my_version) { botkit.my_version = PKG_VERSION; } return botkit.my_version; }; botkit.config = configuration; /** Default the application to listen to the 0.0.0.0, the default * for node's http module. Developers can specify a hostname or IP * address to override this. **/ if (!botkit.config.hostname) { botkit.config.hostname = '0.0.0.0'; }; 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 (!botkit.config.disable_startup_messages) { console.log('Initializing Botkit v' + botkit.version()); } 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.'); } // set the default set of ears to use the regular expression matching botkit.changeEars(botkit.hears_regexp); //enable Botkit Studio studio(botkit); return botkit; } module.exports = Botkit;