Source: components/cli.js

  1. "use strict";
  2. var AppServiceRegistration = require("matrix-appservice").AppServiceRegistration;
  3. var ConfigValidator = require("./config-validator");
  4. var fs = require("fs");
  5. var nopt = require("nopt");
  6. var path = require("path");
  7. var yaml = require("js-yaml");
  8. var DEFAULT_PORT = 8090;
  9. var DEFAULT_FILENAME = "registration.yaml";
  10. const log = require("./logging").get("cli");
  11. /**
  12. * @constructor
  13. * @param {Object} opts CLI options
  14. * @param {Cli~runBridge} opts.run The function called when you should run the bridge.
  15. * @param {Cli~generateRegistration} opts.generateRegistration The function
  16. * called when you should generate a registration.
  17. * @param {Object=} opts.bridgeConfig Bridge-specific config info. If null, no
  18. * --config option will be present in the CLI. Default: null.
  19. * @param {boolean=} opts.bridgeConfig.affectsRegistration True to make the
  20. * --config option required when generating the registration. The parsed config
  21. * can be accessed via <code>Cli.getConfig()</code>.
  22. * @param {string|Object=} opts.bridgeConfig.schema Path to a schema YAML file
  23. * (string) or the parsed schema file (Object).
  24. * @param {Object=} opts.bridgeConfig.defaults The default options for the
  25. * config file.
  26. * @param {boolean=} opts.enableRegistration Enable '--generate-registration'.
  27. * Default True.
  28. * @param {string=} opts.registrationPath The path to write the registration
  29. * file to. Users can overwrite this with -f.
  30. * @param {boolean=} opts.enableLocalpart Enable '--localpart [-l]'. Default: false.
  31. */
  32. function Cli(opts) {
  33. this.opts = opts || {};
  34. if (this.opts.enableRegistration === undefined) {
  35. this.opts.enableRegistration = true;
  36. }
  37. if (!this.opts.run || typeof this.opts.run !== "function") {
  38. throw new Error("Requires 'run' function.");
  39. }
  40. if (this.opts.enableRegistration && !this.opts.generateRegistration) {
  41. throw new Error(
  42. "Registration generation is enabled but no " +
  43. "'generateRegistration' function has been provided"
  44. );
  45. }
  46. this.opts.enableLocalpart = Boolean(this.opts.enableLocalpart);
  47. this.opts.registrationPath = this.opts.registrationPath || DEFAULT_FILENAME;
  48. this.opts.port = this.opts.port || DEFAULT_PORT;
  49. this._bridgeConfig = null;
  50. }
  51. /**
  52. * Get the loaded and parsed bridge config. Only set after run() has been called.
  53. * @return {?Object} The config
  54. */
  55. Cli.prototype.getConfig = function() {
  56. return this._bridgeConfig;
  57. };
  58. /**
  59. * Get the path to the registration file. This may be different to the one supplied
  60. * in the constructor if the user passed a -f flag.
  61. * @return {string} The path to the registration file.
  62. */
  63. Cli.prototype.getRegistrationFilePath = function() {
  64. return this.opts.registrationPath;
  65. };
  66. /**
  67. * Run the app from the command line. Will parse sys args.
  68. */
  69. Cli.prototype.run = function() {
  70. var args = nopt({
  71. "generate-registration": Boolean,
  72. "config": path,
  73. "url": String,
  74. "localpart": String,
  75. "port": Number,
  76. "file": path,
  77. "help": Boolean
  78. }, {
  79. "c": "--config",
  80. "u": "--url",
  81. "r": "--generate-registration",
  82. "l": "--localpart",
  83. "p": "--port",
  84. "f": "--file",
  85. "h": "--help"
  86. });
  87. if (args.file) {
  88. this.opts.registrationPath = args.file;
  89. }
  90. if (this.opts.enableRegistration && args["generate-registration"]) {
  91. if (!args.url) {
  92. this._printHelp();
  93. console.log("Missing --url [-u]");
  94. process.exit(1);
  95. }
  96. if (args.port) {
  97. this._printHelp();
  98. console.log("--port [-p] is not valid when generating a registration file.");
  99. process.exit(1);
  100. }
  101. if (this.opts.bridgeConfig && this.opts.bridgeConfig.affectsRegistration) {
  102. if (!args.config) {
  103. this._printHelp();
  104. console.log("Missing --config [-c]");
  105. process.exit(1);
  106. }
  107. this._assignConfigFile(args.config);
  108. }
  109. this._generateRegistration(args.url, args.localpart);
  110. return;
  111. }
  112. if (args.help || (this.opts.bridgeConfig && !args.config)) {
  113. this._printHelp();
  114. process.exit(0);
  115. return;
  116. }
  117. if (args.localpart) {
  118. this._printHelp();
  119. console.log(
  120. "--localpart [-l] can only be provided when generating a registration."
  121. );
  122. process.exit(1);
  123. return;
  124. }
  125. if (args.port) {
  126. this.opts.port = args.port;
  127. }
  128. this._assignConfigFile(args.config);
  129. this._startWithConfig(this._bridgeConfig);
  130. };
  131. Cli.prototype._assignConfigFile = function(configFilePath) {
  132. var configFile = (this.opts.bridgeConfig && configFilePath) ? configFilePath : null;
  133. var config = this._loadConfig(configFile);
  134. this._bridgeConfig = config;
  135. };
  136. Cli.prototype._loadConfig = function(filename) {
  137. if (!filename) { return {}; }
  138. log.info("Loading config file %s", filename);
  139. var cfg = this._loadYaml(filename);
  140. if (typeof cfg === "string") {
  141. throw new Error("Config file " + filename + " isn't valid YAML.");
  142. }
  143. if (!this.opts.bridgeConfig.schema) {
  144. return cfg;
  145. }
  146. var validator = new ConfigValidator(this.opts.bridgeConfig.schema);
  147. return validator.validate(cfg, this.opts.bridgeConfig.defaults);
  148. };
  149. Cli.prototype._generateRegistration = function(appServiceUrl, localpart) {
  150. if (!appServiceUrl) {
  151. throw new Error("Missing app service URL");
  152. }
  153. var self = this;
  154. var reg = new AppServiceRegistration(appServiceUrl);
  155. if (localpart) {
  156. reg.setSenderLocalpart(localpart);
  157. }
  158. this.opts.generateRegistration.bind(this)(reg, function(completeReg) {
  159. reg = completeReg;
  160. reg.outputAsYaml(self.opts.registrationPath);
  161. console.log("Output registration to: " + self.opts.registrationPath);
  162. process.exit(0);
  163. });
  164. };
  165. Cli.prototype._startWithConfig = function(config) {
  166. this.opts.run(
  167. this.opts.port, config,
  168. AppServiceRegistration.fromObject(this._loadYaml(this.opts.registrationPath))
  169. );
  170. };
  171. Cli.prototype._loadYaml = function(fpath) {
  172. return yaml.safeLoad(fs.readFileSync(fpath, 'utf8'));
  173. };
  174. Cli.prototype._printHelp = function() {
  175. var help = {
  176. "--help -h": "Display this help message",
  177. "--file -f": "The registration file to load or save to."
  178. };
  179. var appPart = (process.argv[0] === "node" ?
  180. // node file/path
  181. (process.argv[0] + " " + path.relative(process.cwd(), process.argv[1])) :
  182. // app-name
  183. process.argv[0]
  184. );
  185. var usages = [];
  186. if (this.opts.enableRegistration) {
  187. help["--generate-registration -r"] = "Create a registration YAML file " +
  188. "for this application service";
  189. help["--url -u"] = "Registration Option. Required if -r is set. The URL " +
  190. "where the application service is listening for HS requests";
  191. if (this.opts.enableLocalpart) {
  192. help["--localpart -l"] = "Registration Option. Valid if -r is set. " +
  193. "The user_id localpart to assign to the AS.";
  194. }
  195. var regUsage = "-r [-f /path/to/save/registration.yaml] " +
  196. "-u 'http://localhost:6789/appservice'";
  197. if (this.opts.bridgeConfig && this.opts.bridgeConfig.affectsRegistration) {
  198. regUsage += " -c CONFIG_FILE";
  199. }
  200. if (this.opts.enableLocalpart) {
  201. regUsage += " [-l my-app-service]";
  202. }
  203. usages.push(regUsage);
  204. }
  205. if (this.opts.bridgeConfig) {
  206. help["--config -c"] = "The config file to load";
  207. usages.push("-c CONFIG_FILE [-f /path/to/load/registration.yaml] [-p NUMBER]");
  208. }
  209. else {
  210. usages.push("[-f /path/to/load/registration.yaml] [-p NUMBER]");
  211. }
  212. help["--port -p"] = "The port to listen on for HS requests";
  213. console.log("Usage:\n");
  214. console.log("Generating an application service registration file:");
  215. console.log("%s %s\n", appPart, usages[0]);
  216. console.log("Running an application service with an existing registration file:");
  217. console.log("%s %s", appPart, usages[1]);
  218. console.log("\nOptions:");
  219. Object.keys(help).forEach(function(k) {
  220. console.log(" %s", k);
  221. console.log(" %s", help[k]);
  222. });
  223. };
  224. module.exports = Cli;
  225. /**
  226. * Invoked when you should generate a registration.
  227. * @callback Cli~generateRegistration
  228. * @param {AppServiceRegistration} reg A new registration object with the app
  229. * service url provided by <code>--url</code> set.
  230. * @param {Function} callback The callback that you should invoke when the
  231. * registration has been generated. It should be called with the
  232. * <code>AppServiceRegistration</code> provided in this function.
  233. * @example
  234. * generateRegistration: function(reg, callback) {
  235. * reg.setHomeserverToken(AppServiceRegistration.generateToken());
  236. * reg.setAppServiceToken(AppServiceRegistration.generateToken());
  237. * reg.setSenderLocalpart("my_first_bot");
  238. * callback(reg);
  239. * }
  240. */
  241. /**
  242. * Invoked when you should run the bridge.
  243. * @callback Cli~runBridge
  244. * @param {Number} port The port to listen on.
  245. * @param {?Object} config The loaded and parsed config.
  246. * @param {AppServiceRegistration} reg The registration to run as.
  247. * @example
  248. * runBridge: function(port, config, reg) {
  249. * // configure bridge
  250. * // listen on port
  251. * }
  252. */