bot.js

  1. const request = require('request');
  2. const url = require('url');
  3. const npmlog = require('npmlog');
  4. const parsers = require('./parsers');
  5. const async = require('async');
  6. const dummy = require('dummy-object');
  7. const Server = require('./server');
  8. const Processor = require('./processor');
  9. /**
  10. * Communicates with the API and executes Processors on incoming updates
  11. */
  12. class Bot {
  13. /**
  14. * @param {object} options
  15. * @param {?String} options.apiUrl URL to the bot's API or null if any
  16. * request's url should be able to be
  17. * used
  18. * @param {?Server} [options.server] Server object, or null if no server
  19. * should be set. Leave blank or pass
  20. * undefined if the server should be
  21. * generated automatically.
  22. * @param {string} [options.route] The default route: e.g. if url is
  23. * https://bot.com/hook?url=https://...
  24. * pass '/webhook'
  25. * @param {Requirement} [options.parser] The default parser. Used to parse the
  26. * body from a HTTP request. As long as
  27. * a Handler's parser option is not
  28. * null, this Requirement will be added
  29. * to all Handlers.
  30. * @param {?object} [options.log] Logger object containing npmlog's
  31. * methods. Pass null to disable. If
  32. * none is passed, npmlog will be used
  33. */
  34. constructor({ apiUrl, server, route, parser, log, verbose } = {}) {
  35. this.setApiUrl(apiUrl);
  36. // Set defaults
  37. this.route = route || /.*/;
  38. this.parser = parser || parsers.json;
  39. this.verbose = typeof verbose === 'boolean' ? verbose : false;
  40. if (log === null) {
  41. this.log = dummy;
  42. } else {
  43. this.log = log || npmlog;
  44. }
  45. if (server === undefined) {
  46. server = new Server(undefined, { log: this.log });
  47. }
  48. if (server !== null) this.setServer(server);
  49. this.handlers = {};
  50. this.logPrefix = 'bot';
  51. }
  52. /** @private */
  53. setServer(server) {
  54. this.server = server;
  55. server.addBot(this);
  56. }
  57. /**
  58. * Invokes the server's listen method
  59. *
  60. * @param {number} port Port to listen on
  61. * @param {string} host Hostname for the server
  62. *
  63. * @see https://nodejs.org/api/http.html
  64. */
  65. listen(...args) {
  66. this.server.listen(...args);
  67. }
  68. /**
  69. * Verifys the value is valid and throws an error if it's not
  70. *
  71. * @private
  72. * @param {string|RegExp|null}
  73. */
  74. setApiUrl(value) {
  75. // value is set, but not a string
  76. if (value && typeof value !== 'string') {
  77. throw new Error('The passed API URL must be a string or RegExp or null ' +
  78. `and not ${typeof value}.`);
  79. }
  80. // apiUrl might also be null, allowing the bot to use any given url
  81. if (typeof value === 'string') {
  82. const parsed = url.parse(value);
  83. if (!parsed.host || !parsed.path) {
  84. throw new Error('Given API URL is invalid!');
  85. }
  86. if (parsed.path[parsed.path.length - 1] !== '/') {
  87. throw new Error("API URL must end with '/'");
  88. }
  89. }
  90. this.apiUrl = value || null;
  91. }
  92. /**
  93. * Handles the data received from the API
  94. *
  95. * @param {string} data The body supplied by the API
  96. * @param {string} passedUrl URL supplied by the API
  97. * @param {string} route The request's route
  98. * @param {ServerResponse} res Response to be sent to the API
  99. */
  100. handle(data, passedUrl, route, res) {
  101. if (this.verbose) {
  102. this.log.info(this.logPrefix, `Handling route '${route}'`);
  103. }
  104. if (this.apiUrl && passedUrl !== this.apiUrl) {
  105. res.writeHead(403, { 'Content-Type': 'text/plain' });
  106. return res.end("API URL did not match with the bot's");
  107. }
  108. async.each(this.handlers, (handler, done) => {
  109. // check if route can be used
  110. if (this.checkRoute(route, handler.route || this.route)) {
  111. new Processor({ data, bot: this, handler, apiUrl: passedUrl, route })
  112. .handle((err) => {
  113. if (process.env.TGBOT_VERBOSE) {
  114. this.log.info(`${this.logPrefix} handler`,
  115. `Handler '${handler.name}' ${err ? 'failed' : 'succeeded'}`);
  116. }
  117. done();
  118. });
  119. } else {
  120. // call the callable if route is not usable
  121. done();
  122. }
  123. }, (err) => {
  124. if (err) this.log.error(err);
  125. res.end();
  126. });
  127. }
  128. /**
  129. * Checks if current http request's route is usable by the handler
  130. *
  131. * @private
  132. * @param {string} current Current http request's route
  133. * @param {string|RegExp} route Handler's route or Bot's default route
  134. */
  135. checkRoute(current, route) {
  136. if (route instanceof RegExp) {
  137. return route.test(current);
  138. }
  139. return route === current;
  140. }
  141. /**
  142. * Register handler
  143. *
  144. * @param {Handler} handler Handler object
  145. * @param {object} [options] options for registering
  146. * @param {boolean} [options.forceReplace] Forces to replace Handler with same
  147. * name
  148. */
  149. register(handler, options = {}) {
  150. this.log.info(this.logPrefix, `Registering handler '${handler.name}'`);
  151. if (options.forceReplace // force to proceed the replace
  152. || !this.handlers[handler.name] // or handler does not yet exist
  153. || this.handlers[handler.name].replaceable) { // or marked as replaceable
  154. // add an object with the key [handler.name] to the handlers object
  155. this.handlers[handler.name] = handler;
  156. } else {
  157. throw new Error(`The Handler ${handler.name} does already exist, ` +
  158. 'is not marked as replaceable and the forceReplace option was not set');
  159. }
  160. }
  161. /**
  162. * Callback containing the API's response data
  163. *
  164. * @callback Bot~response
  165. * @param {object} error
  166. * @param {object} response
  167. * @param {String} body
  168. */
  169. /**
  170. * Sends an update to the API
  171. *
  172. * @param {string} method Is appended to the API's URL
  173. * @param {object} update The update object that should be
  174. * sent
  175. * @param {Bot~response} callback Callback containing error, response
  176. * and body
  177. * @param {object} [options] Options object
  178. * @param {boolean} [options.silent] Will not log the response if true
  179. * @param {string} [options.apiUrl] Custom API URL
  180. */
  181. send(method, update, callback, { apiUrl, silent } = {}) {
  182. request({
  183. url: (apiUrl || this.apiUrl) + method,
  184. method: 'POST',
  185. json: update
  186. }, (err, res, body) => {
  187. if (!silent) this.log.http(this.logPrefix, 'Response', body);
  188. if (callback) callback(err, res, body);
  189. });
  190. }
  191. }
  192. module.exports = Bot;