Source: components/cli.js

"use strict";
var AppServiceRegistration = require("matrix-appservice").AppServiceRegistration;
var ConfigValidator = require("./config-validator");
var fs = require("fs");
var nopt = require("nopt");
var path = require("path");
var yaml = require("js-yaml");

var DEFAULT_PORT = 8090;
var DEFAULT_FILENAME = "registration.yaml";

/**
 * @constructor
 * @param {Object} opts CLI options
 * @param {Cli~runBridge} opts.run The function called when you should run the bridge.
 * @param {Cli~generateRegistration} opts.generateRegistration The function
 * called when you should generate a registration.
 * @param {Object=} opts.bridgeConfig Bridge-specific config info. If null, no
 * --config option will be present in the CLI. Default: null.
 * @param {boolean=} opts.bridgeConfig.affectsRegistration True to make the
 * --config option required when generating the registration. The parsed config
 * can be accessed via <code>Cli.getConfig()</code>.
 * @param {string|Object=} opts.bridgeConfig.schema Path to a schema YAML file
 * (string) or the parsed schema file (Object).
 * @param {Object=} opts.bridgeConfig.defaults The default options for the
 * config file.
 * @param {boolean=} opts.enableRegistration Enable '--generate-registration'.
 * Default True.
 * @param {string=} opts.registrationPath The path to write the registration
 * file to. Users can overwrite this with -f.
 * @param {boolean=} opts.enableLocalpart Enable '--localpart [-l]'. Default: false.
 */
function Cli(opts) {
    this.opts = opts || {};
    if (this.opts.enableRegistration === undefined) {
        this.opts.enableRegistration = true;
    }
    if (!this.opts.run || typeof this.opts.run !== "function") {
        throw new Error("Requires 'run' function.");
    }

    if (this.opts.enableRegistration && !this.opts.generateRegistration) {
        throw new Error(
            "Registration generation is enabled but no " +
            "'generateRegistration' function has been provided"
        );
    }
    this.opts.enableLocalpart = Boolean(this.opts.enableLocalpart);

    this.opts.registrationPath = this.opts.registrationPath || DEFAULT_FILENAME;
    this.opts.port = this.opts.port || DEFAULT_PORT;
    this._bridgeConfig = null;
}

/**
 * Get the loaded and parsed bridge config. Only set after run() has been called.
 * @return {?Object} The config
 */
Cli.prototype.getConfig = function() {
    return this._bridgeConfig;
};

/**
 * Get the path to the registration file. This may be different to the one supplied
 * in the constructor if the user passed a -f flag.
 * @return {string} The path to the registration file.
 */
Cli.prototype.getRegistrationFilePath = function() {
    return this.opts.registrationPath;
};

/**
 * Run the app from the command line. Will parse sys args.
 */
Cli.prototype.run = function() {
    var args = nopt({
        "generate-registration": Boolean,
        "config": path,
        "url": String,
        "localpart": String,
        "port": Number,
        "file": path,
        "help": Boolean
    }, {
        "c": "--config",
        "u": "--url",
        "r": "--generate-registration",
        "l": "--localpart",
        "p": "--port",
        "f": "--file",
        "h": "--help"
    });

    if (args.file) {
        this.opts.registrationPath = args.file;
    }

    if (this.opts.enableRegistration && args["generate-registration"]) {
        if (!args.url) {
            this._printHelp();
            console.log("Missing --url [-u]");
            process.exit(1);
        }
        if (args.port) {
            this._printHelp();
            console.log("--port [-p] is not valid when generating a registration file.");
            process.exit(1);
        }
        if (this.opts.bridgeConfig && this.opts.bridgeConfig.affectsRegistration) {
            if (!args.config) {
                this._printHelp();
                console.log("Missing --config [-c]");
                process.exit(1);
            }
            this._assignConfigFile(args.config);
        }
        this._generateRegistration(args.url, args.localpart);
        return;
    }

    if (args.help || (this.opts.bridgeConfig && !args.config)) {
        this._printHelp();
        process.exit(0);
        return;
    }
    if (args.localpart) {
        this._printHelp();
        console.log(
            "--localpart [-l] can only be provided when generating a registration."
        );
        process.exit(1);
        return;
    }

    if (args.port) {
        this.opts.port = args.port;
    }
    this._assignConfigFile(args.config);
    this._startWithConfig(this._bridgeConfig);
};

Cli.prototype._assignConfigFile = function(configFilePath) {
    var configFile = (this.opts.bridgeConfig && configFilePath) ? configFilePath : null;
    var config = this._loadConfig(configFile);
    this._bridgeConfig = config;
};

Cli.prototype._loadConfig = function(filename) {
    if (!filename) { return {}; }
    console.log("Loading config file %s", filename);
    var cfg = this._loadYaml(filename);
    if (typeof cfg === "string") {
        throw new Error("Config file " + filename + " isn't valid YAML.");
    }
    if (!this.opts.bridgeConfig.schema) {
        return cfg;
    }
    var validator = new ConfigValidator(this.opts.bridgeConfig.schema);
    return validator.validate(cfg, this.opts.bridgeConfig.defaults);
};

Cli.prototype._generateRegistration = function(appServiceUrl, localpart) {
    if (!appServiceUrl) {
        throw new Error("Missing app service URL");
    }
    var self = this;
    var reg = new AppServiceRegistration(appServiceUrl);
    if (localpart) {
        reg.setSenderLocalpart(localpart);
    }
    this.opts.generateRegistration.bind(this)(reg, function(completeReg) {
        reg = completeReg;
        reg.outputAsYaml(self.opts.registrationPath);
        console.log("Output registration to: " + self.opts.registrationPath);
        process.exit(0);
    });
};

Cli.prototype._startWithConfig = function(config) {
    this.opts.run(
        this.opts.port, config,
        AppServiceRegistration.fromObject(this._loadYaml(this.opts.registrationPath))
    );
};

Cli.prototype._loadYaml = function(fpath) {
    return yaml.safeLoad(fs.readFileSync(fpath, 'utf8'));
};

Cli.prototype._printHelp = function() {
    var help = {
        "--help -h": "Display this help message",
        "--file -f": "The registration file to load or save to."
    };
    var appPart = (process.argv[0] === "node" ?
        // node file/path
        (process.argv[0] + " " + path.relative(process.cwd(), process.argv[1])) :
        // app-name
        process.argv[0]
    );
    var usages = [];

    if (this.opts.enableRegistration) {
        help["--generate-registration -r"] = "Create a registration YAML file " +
        "for this application service";
        help["--url -u"] = "Registration Option. Required if -r is set. The URL " +
        "where the application service is listening for HS requests";
        if (this.opts.enableLocalpart) {
            help["--localpart -l"] = "Registration Option. Valid if -r is set. " +
            "The user_id localpart to assign to the AS.";
        }
        var regUsage = "-r [-f /path/to/save/registration.yaml] " +
            "-u 'http://localhost:6789/appservice'";
        if (this.opts.bridgeConfig && this.opts.bridgeConfig.affectsRegistration) {
            regUsage += " -c CONFIG_FILE";
        }
        if (this.opts.enableLocalpart) {
            regUsage += " [-l my-app-service]";
        }
        usages.push(regUsage);
    }
    if (this.opts.bridgeConfig) {
        help["--config -c"] = "The config file to load";
        usages.push("-c CONFIG_FILE [-f /path/to/load/registration.yaml] [-p NUMBER]");
    }
    else {
        usages.push("[-f /path/to/load/registration.yaml] [-p NUMBER]");
    }
    help["--port -p"] = "The port to listen on for HS requests";

    console.log("Usage:\n");
    console.log("Generating an application service registration file:");
    console.log("%s %s\n", appPart, usages[0]);
    console.log("Running an application service with an existing registration file:");
    console.log("%s %s", appPart, usages[1]);

    console.log("\nOptions:");
    Object.keys(help).forEach(function(k) {
        console.log("  %s", k);
        console.log("      %s", help[k]);
    });
};

module.exports = Cli;

/**
 * Invoked when you should generate a registration.
 * @callback Cli~generateRegistration
 * @param {AppServiceRegistration} reg A new registration object with the app
 * service url provided by <code>--url</code> set.
 * @param {Function} callback The callback that you should invoke when the
 * registration has been generated. It should be called with the
 * <code>AppServiceRegistration</code> provided in this function.
 * @example
 * generateRegistration: function(reg, callback) {
 *   reg.setHomeserverToken(AppServiceRegistration.generateToken());
 *   reg.setAppServiceToken(AppServiceRegistration.generateToken());
 *   reg.setSenderLocalpart("my_first_bot");
 *   callback(reg);
 * }
 */

 /**
 * Invoked when you should run the bridge.
 * @callback Cli~runBridge
 * @param {Number} port The port to listen on.
 * @param {?Object} config The loaded and parsed config.
 * @param {AppServiceRegistration} reg The registration to run as.
 * @example
 * runBridge: function(port, config, reg) {
 *   // configure bridge
 *   // listen on port
 * }
 */