UNPKG

imubot

Version:
469 lines (427 loc) 16.5 kB
(function() { const Fs = require('fs'); const Log = require('log'); const Path = require('path'); const HttpClient = require('scoped-http-client'); const { EventEmitter } = require('events'); const async = require('async'); const User = require('./user'); const Brain = require('./brain'); const Response = require('./response'); const { Listener, TextListener } = require('./listener'); const { EnterMessage, LeaveMessage, TopicMessage, CatchAllMessage } = require('./message'); const Middleware = require('./middleware'); const DEFAULT_ADAPTERS = ['campfire', 'shell']; const DOC_SECTIONS = ['description', 'dependencies', 'configuration', 'commands', 'notes', 'author', 'authors', 'examples', 'tags', 'urls']; function Bot(adapterPath, adapter, httpd, name, alias) { if (!name) name = 'Mubot'; if (!alias) alias = false; if (!this.adapterPath) this.adapterPath = Path.join(__dirname, "adapters"); this.name = name; this.events = new EventEmitter; this.brain = new Brain(this); this.alias = alias; this.adapter = null; this.leat = {seen: {last: null}}; this.io = null; this.Response = Response; this.commands = []; this.listeners = []; this.middleware = { listener: new Middleware(this), response: new Middleware(this), receive: new Middleware(this) }; this.logger = new Log(process.env.MUBOT_LOG_LEVEL || 'info'); this.pingIntervalId = null; this.globalHttpOptions = {}; this.parseVersion(); if (httpd) this.setupExpress(); else this.setupNullRouter(); this.loadAdapter(adapter); this.adapterName = adapter; this.errorHandlers = []; this.on('error', (err, res) => this.invokeErrorHandlers(err, res)) this.onUncaughtException = err => this.emit('error', err) process.on('uncaughtException', this.onUncaughtException) } Bot.prototype.listen = function(matcher, options, callback) { this.listeners.push(new Listener(this, matcher, options, callback)); }; Bot.prototype.hear = function(regex, options, callback) { this.listeners.push(new TextListener(this, regex, options, callback)); }; Bot.prototype.respond = function(regex, options, callback) { this.hear(this.respondPattern(regex), options, callback); }; Bot.prototype.respondPattern = function(regex) { var alias, modifiers, name, newRegex, pattern, reArray, anchored; reArray = regex.toString().split('/'); reArray.shift(); modifiers = reArray.pop(); pattern = reArray[0]; if(pattern[0] === '^') { this.logger.warning("The ^ Anchor doesn't work with respond, use 'hear': " + regex.toString()) } name = this.name.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&'); if (this.alias) { alias = this.alias.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&'); newRegex = new RegExp("^\\s*[@]?(?:" + alias + "[:,]?|" + name + "[:,]?)\\s*(?:" + pattern + ")", modifiers); } else { newRegex = new RegExp("^\\s*[@]?" + name + "[:,]?\\s*(?:" + pattern + ")", modifiers); } return newRegex; }; Bot.prototype.enter = function(options, callback) { this.listen(msg => msg instanceof EnterMessage, options, callback); }; Bot.prototype.leave = function(options, callback) { this.listen(msg => msg instanceof LeaveMessage, options, callback); }; Bot.prototype.topic = function(options, callback) { this.listen(msg => msg instanceof Message.TopicMessage, options, callback); }; Bot.prototype.error = function(callback) { this.errorHandlers.push(callback); }; Bot.prototype.invokeErrorHandlers = function(err, res) { var ref, results; this.logger.error(err.stack); ref = this.errorHandlers; for (let i = 0, len = ref.length; i < len; i++) { let errorHandler = ref[i]; try { errorHandler(err, res); } catch (errErr) { this.logger.error("while invoking error handler: " + errErr + "\n" + errErr.stack); } } }; Bot.prototype.catchAll = function(options, callback) { if (!callback) { callback = options; options = {}; } this.listen(msg => msg instanceof CatchAllMessage, options, msg => { msg.message = msg.message.message; callback(msg); }) }; Bot.prototype.listenerMiddleware = function(middleware) { this.middleware.listener.register(middleware); }; Bot.prototype.responseMiddleware = function(middleware) { this.middleware.response.register(middleware); }; Bot.prototype.receiveMiddleware = function(middleware) { this.middleware.receive.register(middleware); }; Bot.prototype.receive = function(message, cb) { this.middleware.receive.execute({ response: new Response(this, message) }, this.processListeners.bind(this), cb); }; Bot.prototype.processListeners = function(context, done) { var anyListenersExecuted = false; async.detectSeries(this.listeners, (listener, cb) => { var err; try { listener.call(context.response.message, this.middleware.listener, listenerExecuted => { anyListenersExecuted = anyListenersExecuted || listenerExecuted; //Middleware.ticker(() => cb(context.response.message.done)); process.nextTick(() => cb(context.response.message.done)); }); } catch (err) { this.emit('error', err, new this.Response(this, context.response.message, [])); cb(false); } }, () => { //_ => { if (!(context.response.message instanceof CatchAllMessage) && !anyListenersExecuted) { this.logger.debug('No listeners executed; falling back to catch-all'); this.receive(new CatchAllMessage(context.response.message), done); } else { if (done) process.nextTick(done); } }) }; Bot.prototype.loadFile = function(path, file) { var ext, full; ext = Path.extname(file); full = Path.join(path, Path.basename(file, ext)); if (require.cache[require.resolve(full)]) { try { let cacheobj = require.resolve(full); this.logger.debug("require cache for " + cacheobj + " invalidated."); delete require.cache[cacheobj]; } catch (err) { this.logger.error("Unable to invalidate " + cacheobj + ": " + err.stack); } } if (require.extensions[ext]) { try { let script = require(full); if (typeof script === 'function') { script(this); this.parseHelp(Path.join(path, file)); } else { this.logger.warning("Expected " + full + " to assign a function to module.exports, got " + (typeof script)); } } catch (err) { this.logger.error("Unable to load " + full + ": " + err.stack); return process.exit(1); } } }; Bot.prototype.load = function(path) { this.logger.debug("Loading scripts from " + path); if (Fs.existsSync(path)) { let ref = Fs.readdirSync(path).sort(); let results = []; for (let i = 0, len = ref.length; i < len; i++) { let file = ref[i]; results.push(this.loadFile(path, file)); } return results; } }; Bot.prototype.loadMubotScripts = function(path, scripts) { this.logger.debug("Loading mubot-scripts from " + path + "."); for (let i = 0, len = scripts.length; i < len; i++) { this.loadFile(path, scripts[i]); } }; Bot.prototype.loadExternalScripts = function(packages) { this.logger.debug("Loading external-scripts from npm packages"); try { if (packages instanceof Array) { for (let i = 0, len = packages.length; i < len; i++) { let pkg = packages[i]; require(pkg)(this); } } else { for (let pkg in packages) { require(pkg)(this, packages[pkg]); } } } catch (err) { this.logger.error("Error loading scripts from npm package - " + err.stack); process.exit(1); } }; Bot.prototype.setupExpress = function() { var stat, sPort, secret, address, parseurl, app, basicAuth, bodyParser, cookieParser, express, fs, http, https, logger, multipart, options, pass, port, user ; user = process.env.EXPRESS_USER; pass = process.env.EXPRESS_PASSWORD; stat = process.env.EXPRESS_STATIC; secret = process.env.EXPRESS_SECRET || 'loyalty is to be placed, and unplaced, but never replaced'; port = process.env.EXPRESS_PORT || process.env.PORT || 8080; sPort = process.env.EXPRESS_SECURE_PORT || process.env.SECURE_PORT || 4343; address = process.env.EXPRESS_BIND_ADDRESS || process.env.BIND_ADDRESS || '0.0.0.0'; express = require('express'); multipart = require('connect-multiparty'); bodyParser = require('body-parser'); //basicAuth = require('basic-auth-connect'); cookieParser = require('cookie-parser'); logger = require('morgan'); app = express(); parseurl = require('parseurl') credentials = process.env.CREDENTIALS || '/etc/letsencrypt/live/leathan.xyz/'; if(process.env.MUBOT_LOG_LEVEL) app.use(logger("dev")); //To use basic auth //if (user && pass) app.use("/admin", basicAuth(user, pass)); if (stat) app.use(express.static(stat)); app.use(bodyParser.json ({ extended: true })); app.use(bodyParser.urlencoded ({ extended: true })); app.use(express.query()) ; app.use(cookieParser()) ; app.use((req, res, next) => { var href, host = req.headers.host ; // no www. present, nothing to do here if (/^www\./i.test(host)) { host = host.replace(/^www\./i, ''); href = 'https://' + host + req.url; res.statusCode = 301; res.setHeader('Location', href); res.write('Redirecting to ' + host + req.url + ''); return res.end(); } let remoteIp = req.connection.remoteAddress; if(this.leat.seen[remoteIp]) { let o = this.leat.seen[remoteIp]; ++o.times; ++o.all_times; o.last = new Date(); o.reqs.push(req); } else { this.leat.seen[remoteIp] = { times: 1, all_times: 1, last: Date.now(), reqs: [req] }; } if(Date.now() - this.leat.seen[remoteIp].last < 777 && this.leat.seen[remoteIp].times > 7) { return res.end("Throttled"); } else { this.leat.seen[remoteIp].times = 0; } next(); this.leat.seen.last = {ip: remoteIp, time: Date.now()}; }) ; https = require('https'); http = require('http'); options = { key: Fs.readFileSync (Path.join(credentials + 'privkey.pem')), cert: Fs.readFileSync(Path.join(credentials + 'fullchain.pem')), ca: Fs.readFileSync (Path.join(credentials + 'chain.pem')) }; this.server = https.createServer(options, app).listen(sPort, function() { return console.log('listening on port ' + sPort + '.'); }).on('error', e => { if(e.code == "EADDRINUSE") console.log("cant listen on port " + sPort + ", address is use.") }) this.io = require('socket.io')(this.server); http.createServer((req, res) => { res.writeHead(301, { 'Location': 'https://' + req.headers.host + req.url }); return res.end(); }).listen(port, function() { return console.log('listening on port ' + port + '.'); }).on('error', e => { if(e.code == "EADDRINUSE") console.log("cant listen on port " + port + ", address is use.") }) var subdomain = require('express-subdomain'); return this.router = app; }; Bot.prototype.setupNullRouter = function() { var msg = "A script has tried registering an HTTP route while the 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) }; }; Bot.prototype.loadAdapter = function(adapter) { var err, path; this.logger.debug("Loading adapter " + adapter); try { path = DEFAULT_ADAPTERS.includes(adapter) ? this.adapterPath + "/" + adapter : "hubot-" + adapter; this.adapter = require(path).use(this); } catch (err) { this.logger.error("Cannot load adapter " + adapter + " - " + err); process.exit(1) ; } } ; Bot.prototype.helpCommands = function() { return this.commands.sort(); } ; Bot.prototype.parseHelp = function(path) { var body, cleanedLine, currentSection, line, nextSection, lines, results, scriptDocumentation, scriptName; this.logger.debug("Parsing help for " + path); scriptName = Path.basename(path).replace(/\.(coffee|js)$/, ''); scriptDocumentation = {}; body = Fs.readFileSync(path, 'utf-8'); currentSection = null; lines = body.split("\n"); for (let i = 0, len = lines.length; i < len; i++) { line = lines[i]; if(!/(#|\/\/)/.test(line)) break; cleanedLine = line.replace(/^(#|\/\/)\s?/, "").trim(); if (cleanedLine.length === 0) continue; if (cleanedLine.toLowerCase() === 'none') continue; nextSection = cleanedLine.toLowerCase().replace(':', ''); if (DOC_SECTIONS.includes(nextSection)) { currentSection = nextSection; scriptDocumentation[currentSection] = []; } else if (currentSection) { scriptDocumentation[currentSection].push(cleanedLine.trim()); if (currentSection === 'commands') { this.commands.push(cleanedLine.trim()); } } } if (currentSection === null) { this.logger.info(path + " is using deprecated documentation syntax"); scriptDocumentation.commands = []; lines = body.split("\n"); results = []; for (let i = 0, l = lines.length; i < l; i++) { line = lines[i]; if(!/(#|\/\/)/.test(line)) break; if(!/-/.test(line)) continue; cleanedLine = line.slice(2, line.length).replace(/^(mubot|Mubot)/i, this.name).trim(); scriptDocumentation.commands.push(cleanedLine); results.push(this.commands.push(cleanedLine)); } return results; } }; Bot.prototype.send = function(envelope) { const strings = [].slice.call(arguments, 1) this.adapter.send.apply(this, [envelope].concat(strings)) }; Bot.prototype.reply = function(envelope) { const strings = [].slice.call(arguments, 1) this.adapter.reply.apply(this, [envelope].concat(strings)) }; Bot.prototype.messageRoom = function(room) { const strings = [].slice.call(arguments, 1) const envelope = { room } this.adapter.send.apply(this.adapter, [envelope].concat(strings)) }; Bot.prototype.on = function(event) { const args = [].slice.call(arguments, 1) this.events.on.apply(this, [event].concat(args)) }; Bot.prototype.emit = function(event) { const args = [].slice.call(arguments, 1) this.events.emit.apply(this, [event].concat(args)) }; Bot.prototype.run = function() { this.emit("running"); this.adapter.run(); }; Bot.prototype.shutdown = function() { if (this.pingIntervalId) clearInterval(this.pingIntervalId); process.removeListener('uncaughtException', this.onUncaughtException); this.adapter.close(); this.brain.close(); }; Bot.prototype.parseVersion = function() { const pkg = require(Path.join(__dirname, '..', 'package.json')); return this.version = pkg.version; }; Bot.prototype.http = function(url, options) { return HttpClient.create(url, this.extend({}, this.globalHttpOptions, options)).header('User-Agent', "Mubot/" + this.version); }; Bot.prototype.extend = function(obj) { const sources = [].slice.call(arguments, 1); for (let i = 0, l = sources.length; i < l; i++) { let source = sources[i]; for (let key in source) { if (!{}.hasOwnProperty.call(source, key)) continue; obj[key] = source[key]; } } return obj; }; function debounce(func, wait) { var timeout, args, context, timestamp, result; var later = function() { var last = new Date.getTime() - timestamp; if (last < wait && last >= 0) { timeout = setTimeout(later, wait - last); } else { timeout = null; } }; } module.exports = Bot; }).call(this);