UNPKG

concierge-bot

Version:

Extensible general purpose chat bot.

505 lines (443 loc) 15.7 kB
const EventEmitter = require('events'); class Robot extends EventEmitter { constructor() { super(); this._registerListener(global.currentPlatform, 'uncaughtError', err => { LOG.error(err.stack); this.emit('error', err); }); this.logger = LOG; this.brain = null; //todo this.name = global.currentPlatform.name; this.Response = Response; this.commands = []; this.listeners = []; this.middleware = { listener: new Middleware(this), response: new Middleware(this), receive: new Middleware(this) }; this.globalHttpOptions = {}; this.parseVersion(); if (httpd) { this.setupExpress(); } else { this.setupNullRouter(); } } _registerListener(obj, name, callback) { this._registeredListeners.push({ obj: obj, name: name, callback }); obj.on(name, callback); } // Public: Adds a custom Listener with the provided matcher, options, and // callback // // matcher - A Function that determines whether to call the callback. // Expected to return a truthy value if the callback should be // executed. // options - An Object of additional parameters keyed on extension name // (optional). // callback - A Function that is called with a Response object if the // matcher function returns true. // // Returns nothing. listen(matcher, options, callback) { return this.listeners.push(new Listener(this, matcher, options, callback)); } // Public: Adds a Listener that attempts to match incoming messages based on // a Regex. // // regex - A Regex that determines if the callback should be called. // options - An Object of additional parameters keyed on extension name // (optional). // callback - A Function that is called with a Response object. // // Returns nothing. hear(regex, options, callback) { return this.listeners.push(new TextListener(this, regex, options, callback)); } // Public: Adds a Listener that attempts to match incoming messages directed // at the robot based on a Regex. All regexes treat patterns like they begin // with a '^' // // regex - A Regex that determines if the callback should be called. // options - An Object of additional parameters keyed on extension name // (optional). // callback - A Function that is called with a Response object. // // Returns nothing. respond(regex, options, callback) { return this.hear(this.respondPattern(regex), options, callback); } // Public: Build a regular expression that matches messages addressed // directly to the robot // // regex - A RegExp for the message part that follows the robot's name/alias // // Returns RegExp. respondPattern(regex) { let newRegex; let re = regex.toString().split('/'); re.shift(); let modifiers = re.pop(); if (re[0] && (re[0][0] === '^')) { this.logger.warning( "Anchors don't work well with respond, perhaps you want to use 'hear'"); this.logger.warning(`The regex in question was ${regex.toString()}`); } let pattern = re.join('/'); let name = this.name.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&'); if (this.alias) { let alias = this.alias.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&'); let [a,b] = Array.from(name.length > alias.length ? [name,alias] : [alias,name]); newRegex = new RegExp( `^\\s*[@]?(?:${a}[:,]?|${b}[:,]?)\\s*(?:${pattern})`, modifiers ); } else { newRegex = new RegExp( `^\\s*[@]?${name}[:,]?\\s*(?:${pattern})`, modifiers ); } return newRegex; } enter(options, callback) { return this.listen( (msg => msg instanceof EnterMessage), options, callback ); } leave(options, callback) { return this.listen( (msg => msg instanceof LeaveMessage), options, callback ); } // Public: Adds a Listener that triggers when anyone changes the topic. // // options - An Object of additional parameters keyed on extension name // (optional). // callback - A Function that is called with a Response object. // // Returns nothing. topic(options, callback) { return this.listen( (msg => msg instanceof TopicMessage), options, callback ); } error (callback) { this.on('error', err => { try { return callback(err); } catch (errErr) { return LOG.error(`while invoking error handler: ${errErr}\n${errErr.stack}`); } }); } catchAll (options, callback = null) { if (!callback) { callback = options; options = {}; } } // Public: Adds a Listener that triggers when no other text matchers match. // // options - An Object of additional parameters keyed on extension name // (optional). // callback - A Function that is called with a Response object. // // Returns nothing. catchAll(options, callback) { // `options` is optional; need to isolate the real callback before // wrapping it with logic below if ((callback == null)) { callback = options; options = {}; } return this.listen( (msg => msg instanceof CatchAllMessage), options, (function(msg) { msg.message = msg.message.message; return callback(msg); }) ); } // Public: Registers new middleware for execution after matching but before // Listener callbacks // // middleware - A function that determines whether or not a given matching // Listener should be executed. The function is called with // (context, next, done). If execution should // continue (next middleware, Listener callback), the middleware // should call the 'next' function with 'done' as an argument. // If not, the middleware should call the 'done' function with // no arguments. // // Returns nothing. listenerMiddleware(middleware) { this.middleware.listener.register(middleware); return undefined; } // Public: Registers new middleware for execution as a response to any // message is being sent. // // middleware - A function that examines an outgoing message and can modify // it or prevent its sending. The function is called with // (context, next, done). If execution should continue, // the middleware should call next(done). If execution should stop, // the middleware should call done(). To modify the outgoing message, // set context.string to a new message. // // Returns nothing. responseMiddleware(middleware) { this.middleware.response.register(middleware); return undefined; } // Public: Registers new middleware for execution before matching // // middleware - A function that determines whether or not listeners should be // checked. The function is called with (context, next, done). If // ext, next, done). If execution should continue to the next // middleware or matching phase, it should call the 'next' // function with 'done' as an argument. If not, the middleware // should call the 'done' function with no arguments. // // Returns nothing. receiveMiddleware(middleware) { this.middleware.receive.register(middleware); return undefined; } // Public: Passes the given message to any interested Listeners after running // receive middleware. // // message - A Message instance. Listeners can flag this message as 'done' to // prevent further execution. // // cb - Optional callback that is called when message processing is complete // // Returns nothing. // Returns before executing callback receive(message, cb) { // When everything is finished (down the middleware stack and back up), // pass control back to the robot return this.middleware.receive.execute( {response: new Response(this, message)}, this.processListeners.bind(this), cb ); } // Public: Loads a file in path. // // path - A String path on the filesystem. // file - A String filename in path on the filesystem. // // Returns nothing. loadFile(path, file) { let ext = Path.extname(file); let full = Path.join(path, Path.basename(file, ext)); if (require.extensions[ext]) { try { let script = require(full); if (typeof script === 'function') { script(this); return this.parseHelp(Path.join(path, file)); } else { return this.logger.warning(`Expected ${full} to assign a function to module.exports, got ${typeof script}`); } } catch (error) { this.logger.error(`Unable to load ${full}: ${error.stack}`); return process.exit(1); } } } // Public: Loads every script in the given path. // // path - A String path on the filesystem. // // Returns nothing. load(path) { this.logger.debug(`Loading scripts from ${path}`); if (Fs.existsSync(path)) { return Array.from(Fs.readdirSync(path).sort()).map((file) => this.loadFile(path, file)); } } // Public: Load scripts specified in the `hubot-scripts.json` file. // // path - A String path to the hubot-scripts files. // scripts - An Array of scripts to load. // // Returns nothing. loadHubotScripts(path, scripts) { this.logger.debug(`Loading hubot-scripts from ${path}`); return Array.from(scripts).map((script) => this.loadFile(path, script)); } // Public: Load scripts from packages specified in the // `external-scripts.json` file. // // packages - An Array of packages containing hubot scripts to load. // // Returns nothing. loadExternalScripts(packages) { this.logger.debug("Loading external-scripts from npm packages"); try { let pkg; if (packages instanceof Array) { return (() => { let result = []; for (pkg of Array.from(packages)) { result.push(require(pkg)(this)); } return result; })(); } else { return (() => { let result1 = []; for (pkg in packages) { let scripts = packages[pkg]; result1.push(require(pkg)(this, scripts)); } return result1; })(); } } catch (err) { this.logger.error(`Error loading scripts from npm package - ${err.stack}`); return process.exit(1); } } // Setup the Express server's defaults. // // Returns nothing. setupExpress() { let user = process.env.EXPRESS_USER; let pass = process.env.EXPRESS_PASSWORD; let stat = process.env.EXPRESS_STATIC; let port = process.env.EXPRESS_PORT || process.env.PORT || 8080; let address = process.env.EXPRESS_BIND_ADDRESS || process.env.BIND_ADDRESS || '0.0.0.0'; let express = require('express'); let multipart = require('connect-multiparty'); let app = express(); app.use((req, res, next) => { res.setHeader("X-Powered-By", `hubot/${this.name}`); return next(); }); if (user && pass) { app.use(express.basicAuth(user, pass)); } app.use(express.query()); app.use(express.json()); app.use(express.urlencoded()); // replacement for deprecated express.multipart/connect.multipart // limit to 100mb, as per the old behavior app.use(multipart({maxFilesSize: 100 * 1024 * 1024})); if (stat) { app.use(express.static(stat)); } try { this.server = app.listen(port, address); this.router = app; } catch (error) { let err = error; this.logger.error(`Error trying to start HTTP server: ${err}\n${err.stack}`); process.exit(1); } let herokuUrl = process.env.HEROKU_URL; if (herokuUrl) { if (!/\/$/.test(herokuUrl)) { herokuUrl += '/'; } return this.pingIntervalId = setInterval(() => { return HttpClient.create(`${herokuUrl}hubot/ping`).post()((err, res, body) => { return this.logger.info('keep alive ping!'); }); } , 5 * 60 * 1000); } } // Setup an empty router object // // returns nothing setupNullRouter() { let msg = "A script has tried registering a HTTP route while the HTTP server is disabled with --disabled-httpd."; return this.router = { get: ()=> this.logger.warning(msg), post: ()=> this.logger.warning(msg), put: ()=> this.logger.warning(msg), delete: ()=> this.logger.warning(msg) }; } // Load the adapter Hubot is going to use. // // path - A String of the path to adapter if local. // adapter - A String of the adapter name to use. // // Returns nothing. loadAdapter(adapter) { this.logger.debug(`Loading adapter ${adapter}`); try { let path = Array.from(HUBOT_DEFAULT_ADAPTERS).includes(adapter) ? `${this.adapterPath}/${adapter}` : `hubot-${adapter}`; return this.adapter = require(path).use(this); } catch (err) { this.logger.error(`Cannot load adapter ${adapter} - ${err}`); return process.exit(1); } } // Public: Help Commands for Running Scripts. // // Returns an Array of help commands for running scripts. helpCommands() { return this.commands.sort(); } // Public: A helper send function which delegates to the adapter's send // function. // // envelope - A Object with message, room and user details. // strings - One or more Strings for each message to send. // // Returns nothing. send(envelope, ...strings) { return this.adapter.send(envelope, ...Array.from(strings)); } // Public: A helper reply function which delegates to the adapter's reply // function. // // envelope - A Object with message, room and user details. // strings - One or more Strings for each message to send. // // Returns nothing. reply(envelope, ...strings) { return this.adapter.reply(envelope, ...Array.from(strings)); } // Public: A helper send function to message a room that the robot is in. // // room - String designating the room to message. // strings - One or more Strings for each message to send. // // Returns nothing. messageRoom(room, ...strings) { let envelope = { room }; return this.adapter.send(envelope, ...Array.from(strings)); } // Public: Kick off the event loop for the adapter // // Returns nothing. run() { this.emit("running"); return this.adapter.run(); } shutdown() { global.currentPlatform.shutdown(); } parseVersion() { return global.currentPlatform.packageInfo.version; } http(url, options) { return require('scoped-http-client').create(url, options).header('User-Agent', `Concierge/${this.parseVersion()}`); } }