const request = require('request');const url = require('url');const npmlog = require('npmlog');const parsers = require('./parsers');const async = require('async');const dummy = require('dummy-object');const Server = require('./server');const Processor = require('./processor');/** * Communicates with the API and executes Processors on incoming updates */class Bot { /** * @param {object} options * @param {?String} options.apiUrl URL to the bot's API or null if any * request's url should be able to be * used * @param {?Server} [options.server] Server object, or null if no server * should be set. Leave blank or pass * undefined if the server should be * generated automatically. * @param {string} [options.route] The default route: e.g. if url is * https://bot.com/hook?url=https://... * pass '/webhook' * @param {Requirement} [options.parser] The default parser. Used to parse the * body from a HTTP request. As long as * a Handler's parser option is not * null, this Requirement will be added * to all Handlers. * @param {?object} [options.log] Logger object containing npmlog's * methods. Pass null to disable. If * none is passed, npmlog will be used */ constructor({ apiUrl, server, route, parser, log, verbose } = {}) { this.setApiUrl(apiUrl); // Set defaults this.route = route || /.*/; this.parser = parser || parsers.json; this.verbose = typeof verbose === 'boolean' ? verbose : false; if (log === null) { this.log = dummy; } else { this.log = log || npmlog; } if (server === undefined) { server = new Server(undefined, { log: this.log }); } if (server !== null) this.setServer(server); this.handlers = {}; this.logPrefix = 'bot'; } /** @private */ setServer(server) { this.server = server; server.addBot(this); } /** * Invokes the server's listen method * * @param {number} port Port to listen on * @param {string} host Hostname for the server * * @see https://nodejs.org/api/http.html */ listen(...args) { this.server.listen(...args); } /** * Verifys the value is valid and throws an error if it's not * * @private * @param {string|RegExp|null} */ setApiUrl(value) { // value is set, but not a string if (value && typeof value !== 'string') { throw new Error('The passed API URL must be a string or RegExp or null ' + `and not ${typeof value}.`); } // apiUrl might also be null, allowing the bot to use any given url if (typeof value === 'string') { const parsed = url.parse(value); if (!parsed.host || !parsed.path) { throw new Error('Given API URL is invalid!'); } if (parsed.path[parsed.path.length - 1] !== '/') { throw new Error("API URL must end with '/'"); } } this.apiUrl = value || null; } /** * Handles the data received from the API * * @param {string} data The body supplied by the API * @param {string} passedUrl URL supplied by the API * @param {string} route The request's route * @param {ServerResponse} res Response to be sent to the API */ handle(data, passedUrl, route, res) { if (this.verbose) { this.log.info(this.logPrefix, `Handling route '${route}'`); } if (this.apiUrl && passedUrl !== this.apiUrl) { res.writeHead(403, { 'Content-Type': 'text/plain' }); return res.end("API URL did not match with the bot's"); } async.each(this.handlers, (handler, done) => { // check if route can be used if (this.checkRoute(route, handler.route || this.route)) { new Processor({ data, bot: this, handler, apiUrl: passedUrl, route }) .handle((err) => { if (process.env.TGBOT_VERBOSE) { this.log.info(`${this.logPrefix} handler`, `Handler '${handler.name}' ${err ? 'failed' : 'succeeded'}`); } done(); }); } else { // call the callable if route is not usable done(); } }, (err) => { if (err) this.log.error(err); res.end(); }); } /** * Checks if current http request's route is usable by the handler * * @private * @param {string} current Current http request's route * @param {string|RegExp} route Handler's route or Bot's default route */ checkRoute(current, route) { if (route instanceof RegExp) { return route.test(current); } return route === current; } /** * Register handler * * @param {Handler} handler Handler object * @param {object} [options] options for registering * @param {boolean} [options.forceReplace] Forces to replace Handler with same * name */ register(handler, options = {}) { this.log.info(this.logPrefix, `Registering handler '${handler.name}'`); if (options.forceReplace // force to proceed the replace || !this.handlers[handler.name] // or handler does not yet exist || this.handlers[handler.name].replaceable) { // or marked as replaceable // add an object with the key [handler.name] to the handlers object this.handlers[handler.name] = handler; } else { throw new Error(`The Handler ${handler.name} does already exist, ` + 'is not marked as replaceable and the forceReplace option was not set'); } } /** * Callback containing the API's response data * * @callback Bot~response * @param {object} error * @param {object} response * @param {String} body */ /** * Sends an update to the API * * @param {string} method Is appended to the API's URL * @param {object} update The update object that should be * sent * @param {Bot~response} callback Callback containing error, response * and body * @param {object} [options] Options object * @param {boolean} [options.silent] Will not log the response if true * @param {string} [options.apiUrl] Custom API URL */ send(method, update, callback, { apiUrl, silent } = {}) { request({ url: (apiUrl || this.apiUrl) + method, method: 'POST', json: update }, (err, res, body) => { if (!silent) this.log.http(this.logPrefix, 'Response', body); if (callback) callback(err, res, body); }); }}module.exports = Bot;