From 02c981b0702ca614a42fd21d7f0b123176589100 Mon Sep 17 00:00:00 2001 From: turt2live Date: Mon, 11 Sep 2017 20:50:43 -0600 Subject: [PATCH] [BREAKING] Allow possibility of disabling Scalar upstream This is a breaking change because of the structure change for upstreams. Instead of being an object, it is now a list. Existing configurations are not guaranteed to work. Adds #108 and starts work on #22 (upstream config). --- config/default.yaml | 3 +- src/DimensionApi.js | 41 ++++++++++----- src/ScalarApi.js | 12 ++++- src/UpstreamConfiguration.js | 50 +++++++++++++++++++ src/integration/impl/StubbedFactory.js | 11 +++- src/integration/impl/irc/IRCApi.js | 2 +- src/integration/impl/irc/IRCFactory.js | 26 ++++++---- src/integration/impl/rss/RSSFactory.js | 26 ++++++---- .../impl/simple_bot/SimpleBotFactory.js | 19 +++++-- .../impl/simple_widget/SimpleWidgetFactory.js | 11 +++- .../impl/travisci/TravisCiFactory.js | 26 ++++++---- src/integration/index.js | 14 ++++++ src/scalar/ScalarClient.js | 3 +- src/scalar/VectorScalarClient.js | 3 +- src/storage/DimensionStore.js | 2 +- src/storage/models/tokens.js | 2 +- 16 files changed, 196 insertions(+), 55 deletions(-) create mode 100644 src/UpstreamConfiguration.js diff --git a/config/default.yaml b/config/default.yaml index 09969dc..a73e164 100644 --- a/config/default.yaml +++ b/config/default.yaml @@ -22,4 +22,5 @@ demobot: # Upstream configuration. This should almost never change. upstreams: - vector: "https://scalar.vector.im/api" \ No newline at end of file +- name: vector + url: "https://scalar.vector.im/api" \ No newline at end of file diff --git a/src/DimensionApi.js b/src/DimensionApi.js index 0d1fe6f..7c8dc47 100644 --- a/src/DimensionApi.js +++ b/src/DimensionApi.js @@ -32,7 +32,11 @@ class DimensionApi { var factory = IntegrationImpl.getFactory(integrationConfig); if (!factory) throw new Error("Missing config factory for " + integrationConfig.name); - return factory(this._db, integrationConfig, roomId, scalarToken); + try { + return factory(this._db, integrationConfig, roomId, scalarToken); + } catch (err) { + throw new Error("Error using factory for " + integrationConfig.name + ". Please either fix the integration settings or disable the integration.", err); + } } _getIntegrations(req, res) { @@ -49,21 +53,34 @@ class DimensionApi { var integrations = _.map(Integrations.all, i => JSON.parse(JSON.stringify(i))); // clone var promises = []; + var remove = []; _.forEach(integrations, integration => { - promises.push(this._getIntegration(integration, roomId, scalarToken).then(builtIntegration => { - return builtIntegration.getState().then(state => { - var keys = _.keys(state); - for (var key of keys) { - integration[key] = state[key]; - } + try { + promises.push(this._getIntegration(integration, roomId, scalarToken).then(builtIntegration => { + return builtIntegration.getState().then(state => { + var keys = _.keys(state); + for (var key of keys) { + integration[key] = state[key]; + } - return builtIntegration.getUserId(); - }).then(userId => { - integration.userId = userId; - }); - })); + return builtIntegration.getUserId(); + }).then(userId => { + integration.userId = userId; + }); + })); + } catch (err) { + remove.push(integration); + log.error("DimensionApi", err); + } }); + for (var toRemove of remove) { + var idx = integrations.indexOf(toRemove); + if (idx === -1) continue; + log.warn("DimensionApi", "Disabling integration " + toRemove.name +" due to an error encountered in setup"); + integrations.splice(idx, 1); + } + Promise.all(promises).then(() => res.send(_.map(integrations, integration => { // Remove sensitive material integration.upstream = undefined; diff --git a/src/ScalarApi.js b/src/ScalarApi.js index 4c39e2c..606b96b 100644 --- a/src/ScalarApi.js +++ b/src/ScalarApi.js @@ -4,6 +4,7 @@ var ScalarClient = require("./scalar/ScalarClient.js"); var _ = require("lodash"); var log = require("./util/LogService"); var Promise = require("bluebird"); +var UpstreamConfiguration = require("./UpstreamConfiguration"); /** * API handler for the Scalar API, as required by Riot @@ -33,7 +34,11 @@ class ScalarApi { if (!token) res.sendStatus(400); else this._db.checkToken(token).then(() => { res.sendStatus(200); - }).catch(() => res.sendStatus(401)); + }).catch(e => { + res.sendStatus(401); + log.warn("ScalarApi", "Failed to authenticate token"); + log.verbose("ScalarApi", e); + }); } _scalarRegister(req, res) { @@ -51,6 +56,9 @@ class ScalarApi { client.getSelfMxid().then(mxid => { userId = mxid; if (!mxid) throw new Error("Token does not resolve to a matrix user"); + + // TODO: Make this part more generic for other upstreams (#22) + if (!UpstreamConfiguration.hasUpstream("vector")) return Promise.resolve(null); return ScalarClient.register(tokenInfo); }).then(upstreamToken => { return this._db.createToken(userId, tokenInfo, scalarToken, upstreamToken); @@ -64,4 +72,4 @@ class ScalarApi { } } -module.exports = new ScalarApi(); +module.exports = new ScalarApi(); \ No newline at end of file diff --git a/src/UpstreamConfiguration.js b/src/UpstreamConfiguration.js new file mode 100644 index 0000000..de22466 --- /dev/null +++ b/src/UpstreamConfiguration.js @@ -0,0 +1,50 @@ +var LogService = require("./util/LogService"); +var _ = require("lodash"); +var config = require("config"); + +/** + * Handles all upstream configuration information, such as URLs, tokens, and whether or not they are enabled. + */ +class UpstreamConfiguration { + /** + * Creates a new upstream configuration handler + */ + constructor() { + this._upstreams = {}; + this._loadUpstreams(); + } + + _loadUpstreams() { + for (var upstream of config.upstreams) { + var upstreamConfig = upstream; + + if (this._upstreams[upstream.name]) { + LogService.warn("UpstreamConfiguration", "Duplicate upstream " + upstream.name +" - skipping"); + continue; + } + + this._upstreams[upstream.name] = upstreamConfig; + LogService.info("UpstreamConfiguration", "Loaded upstream '" + upstream.name + "' as: " + JSON.stringify(upstreamConfig)); + } + } + + /** + * Checks if a particular upstream exists + * @param {string} name the name of the upstream + * @returns {boolean} true if it is enabled and exists + */ + hasUpstream(name) { + return !!this._upstreams[name]; + } + + /** + * Gets an upstream's configuration + * @param {string} name the upstream name + * @returns {{url:string}} the upstream configuration + */ + getUpstream(name) { + return _.clone(this._upstreams[name]); + } +} + +module.exports = new UpstreamConfiguration(); \ No newline at end of file diff --git a/src/integration/impl/StubbedFactory.js b/src/integration/impl/StubbedFactory.js index c6cf8b3..918e7c0 100644 --- a/src/integration/impl/StubbedFactory.js +++ b/src/integration/impl/StubbedFactory.js @@ -8,6 +8,13 @@ var IntegrationStub = require("../generic_types/IntegrationStub"); * @param {string} scalarToken the scalar token * @returns {Promise<*>} resolves to the configured integration */ -module.exports = (db, integrationConfig, roomId, scalarToken) => { +var factory = (db, integrationConfig, roomId, scalarToken) => { + factory.validateConfig(integrationConfig); return Promise.resolve(new IntegrationStub(integrationConfig)); -}; \ No newline at end of file +}; + +factory.validateConfig = (integrationConfig) => { + // Nothing to do +}; + +module.exports = factory; \ No newline at end of file diff --git a/src/integration/impl/irc/IRCApi.js b/src/integration/impl/irc/IRCApi.js index 71ccbb0..aed963d 100644 --- a/src/integration/impl/irc/IRCApi.js +++ b/src/integration/impl/irc/IRCApi.js @@ -19,7 +19,7 @@ class IRCApi { * @param {DimensionStore} db the store to use */ bootstrap(app, db) { - if (!Integrations.byType["bridge"]["irc"]) { + if (!Integrations.byType["bridge"] || !Integrations.byType["bridge"]["irc"]) { log.info("IRCApi", "IRC Bridge not enabled - not setting up the API"); return; } else log.info("IRCApi", "Setting up IRC API"); diff --git a/src/integration/impl/irc/IRCFactory.js b/src/integration/impl/irc/IRCFactory.js index fe6c660..131d729 100644 --- a/src/integration/impl/irc/IRCFactory.js +++ b/src/integration/impl/irc/IRCFactory.js @@ -1,12 +1,20 @@ var IRCBridge = require("./IRCBridge"); var VectorIrcBackbone = require("./VectorIrcBackbone"); +var UpstreamConfiguration = require("../../../UpstreamConfiguration"); -module.exports = (db, integrationConfig, roomId, scalarToken) => { - if (integrationConfig.upstream) { - if (integrationConfig.upstream.type !== "vector") throw new Error("Unsupported upstream"); - return db.getUpstreamToken(scalarToken).then(upstreamToken => { - var backbone = new VectorIrcBackbone(roomId, upstreamToken); - return new IRCBridge(integrationConfig, backbone); - }); - } else throw new Error("Unsupported config"); -}; \ No newline at end of file +var factory = (db, integrationConfig, roomId, scalarToken) => { + factory.validateConfig(integrationConfig); + + return db.getUpstreamToken(scalarToken).then(upstreamToken => { + var backbone = new VectorIrcBackbone(roomId, upstreamToken); + return new IRCBridge(integrationConfig, backbone); + }); +}; + +factory.validateConfig = (integrationConfig) => { + if (!integrationConfig.upstream) throw new Error("Unsupported configuration"); + if (integrationConfig.upstream.type !== "vector") throw new Error("Unsupported upstream"); + if (!UpstreamConfiguration.hasUpstream("vector")) throw new Error("Vector upstream not specified"); +}; + +module.exports = factory; \ No newline at end of file diff --git a/src/integration/impl/rss/RSSFactory.js b/src/integration/impl/rss/RSSFactory.js index 95f808f..f800a7b 100644 --- a/src/integration/impl/rss/RSSFactory.js +++ b/src/integration/impl/rss/RSSFactory.js @@ -1,12 +1,20 @@ var RSSBot = require("./RSSBot"); var VectorRssBackbone = require("./VectorRssBackbone"); +var UpstreamConfiguration = require("../../../UpstreamConfiguration"); -module.exports = (db, integrationConfig, roomId, scalarToken) => { - if (integrationConfig.upstream) { - if (integrationConfig.upstream.type !== "vector") throw new Error("Unsupported upstream"); - return db.getUpstreamToken(scalarToken).then(upstreamToken => { - var backbone = new VectorRssBackbone(roomId, upstreamToken); - return new RSSBot(integrationConfig, backbone); - }); - } else throw new Error("Unsupported config"); -}; \ No newline at end of file +var factory = (db, integrationConfig, roomId, scalarToken) => { + factory.validateConfig(integrationConfig); + + return db.getUpstreamToken(scalarToken).then(upstreamToken => { + var backbone = new VectorRssBackbone(roomId, upstreamToken); + return new RSSBot(integrationConfig, backbone); + }); +}; + +factory.validateConfig = (integrationConfig) => { + if (!integrationConfig.upstream) throw new Error("Unsupported configuration"); + if (integrationConfig.upstream.type !== "vector") throw new Error("Unsupported upstream"); + if (!UpstreamConfiguration.hasUpstream("vector")) throw new Error("Vector upstream not specified"); +}; + +module.exports = factory; \ No newline at end of file diff --git a/src/integration/impl/simple_bot/SimpleBotFactory.js b/src/integration/impl/simple_bot/SimpleBotFactory.js index 65d9678..1c4c1cc 100644 --- a/src/integration/impl/simple_bot/SimpleBotFactory.js +++ b/src/integration/impl/simple_bot/SimpleBotFactory.js @@ -1,10 +1,12 @@ var SimpleBot = require("./SimpleBot"); var VectorSimpleBackbone = require("./VectorSimpleBackbone"); var HostedSimpleBackbone = require("./HostedSimpleBackbone"); +var UpstreamConfiguration = require("../../../UpstreamConfiguration"); + +var factory = (db, integrationConfig, roomId, scalarToken) => { + factory.validateConfig(integrationConfig); -module.exports = (db, integrationConfig, roomId, scalarToken) => { if (integrationConfig.upstream) { - if (integrationConfig.upstream.type !== "vector") throw new Error("Unsupported upstream"); return db.getUpstreamToken(scalarToken).then(upstreamToken => { var backbone = new VectorSimpleBackbone(integrationConfig, upstreamToken); return new SimpleBot(integrationConfig, backbone); @@ -12,5 +14,14 @@ module.exports = (db, integrationConfig, roomId, scalarToken) => { } else if (integrationConfig.hosted) { var backbone = new HostedSimpleBackbone(integrationConfig); return Promise.resolve(new SimpleBot(integrationConfig, backbone)); - } else throw new Error("Unsupported config"); -}; \ No newline at end of file + } +}; + +factory.validateConfig = (integrationConfig) => { + if (integrationConfig.upstream) { + if (integrationConfig.upstream.type !== "vector") throw new Error("Unsupported upstream"); + if (!UpstreamConfiguration.hasUpstream("vector")) throw new Error("Vector upstream not specified"); + } else if (!integrationConfig.hosted) throw new Error("Unsupported configuration"); +}; + +module.exports = factory; \ No newline at end of file diff --git a/src/integration/impl/simple_widget/SimpleWidgetFactory.js b/src/integration/impl/simple_widget/SimpleWidgetFactory.js index c9c6583..34782e0 100644 --- a/src/integration/impl/simple_widget/SimpleWidgetFactory.js +++ b/src/integration/impl/simple_widget/SimpleWidgetFactory.js @@ -1,6 +1,13 @@ var SimpleWidget = require("./SimpleWidget"); var Promise = require("bluebird"); -module.exports = (db, integrationConfig, roomId, scalarToken) => { +var factory = (db, integrationConfig, roomId, scalarToken) => { + factory.validateConfig(integrationConfig); return Promise.resolve(new SimpleWidget(integrationConfig, roomId)); -}; \ No newline at end of file +}; + +factory.validateConfig = (integrationConfig) => { + // Nothing to do +}; + +module.exports = factory; \ No newline at end of file diff --git a/src/integration/impl/travisci/TravisCiFactory.js b/src/integration/impl/travisci/TravisCiFactory.js index 7ccdfe3..42ac8e9 100644 --- a/src/integration/impl/travisci/TravisCiFactory.js +++ b/src/integration/impl/travisci/TravisCiFactory.js @@ -1,12 +1,20 @@ var TravisCiBot = require("./TravisCiBot"); var VectorTravisCiBackbone = require("./VectorTravisCiBackbone"); +var UpstreamConfiguration = require("../../../UpstreamConfiguration"); -module.exports = (db, integrationConfig, roomId, scalarToken) => { - if (integrationConfig.upstream) { - if (integrationConfig.upstream.type !== "vector") throw new Error("Unsupported upstream"); - return db.getUpstreamToken(scalarToken).then(upstreamToken => { - var backbone = new VectorTravisCiBackbone(roomId, upstreamToken); - return new TravisCiBot(integrationConfig, backbone); - }); - } else throw new Error("Unsupported config"); -}; \ No newline at end of file +var factory = (db, integrationConfig, roomId, scalarToken) => { + factory.validateConfig(integrationConfig); + + return db.getUpstreamToken(scalarToken).then(upstreamToken => { + var backbone = new VectorTravisCiBackbone(roomId, upstreamToken); + return new TravisCiBot(integrationConfig, backbone); + }); +}; + +factory.validateConfig = (integrationConfig) => { + if (!integrationConfig.upstream) throw new Error("Unsupported configuration"); + if (integrationConfig.upstream.type !== "vector") throw new Error("Unsupported upstream"); + if (!UpstreamConfiguration.hasUpstream("vector")) throw new Error("Vector upstream not specified"); +}; + +module.exports = factory; \ No newline at end of file diff --git a/src/integration/index.js b/src/integration/index.js index ef70c12..ca1b2bc 100644 --- a/src/integration/index.js +++ b/src/integration/index.js @@ -3,6 +3,7 @@ var log = require("../util/LogService"); var fs = require("fs"); var path = require("path"); var _ = require("lodash"); +var IntegrationImpl = require("./impl"); log.info("Integrations", "Discovering integrations"); @@ -43,6 +44,19 @@ for (var key of keys) { continue; } + var factory = IntegrationImpl.getFactory(merged); + if (!factory) { + log.warn("Integrations", "Integration " + key + " does not have an associated factory - skipping"); + continue; + } + try { + factory.validateConfig(merged); + } catch (err) { + log.error("Integrations", "Error while validating integration " + key + " - skipping"); + log.error("Integrations", err); + continue; + } + linear.push(merged); if (merged['userId']) byUserId[merged['userId']] = merged; diff --git a/src/scalar/ScalarClient.js b/src/scalar/ScalarClient.js index 9138db3..c6586b2 100644 --- a/src/scalar/ScalarClient.js +++ b/src/scalar/ScalarClient.js @@ -1,6 +1,7 @@ var request = require('request'); var log = require("../util/LogService"); var config = require("config"); +var UpstreamConfiguration = require("../UpstreamConfiguration"); /** * Represents a scalar client @@ -32,7 +33,7 @@ class ScalarClient { // TODO: Merge this, VectorScalarClient, and MatrixLiteClient into a base class _do(method, endpoint, qs = null, body = null) { // TODO: Generify URL - var url = config.get("upstreams.vector") + endpoint; + var url = UpstreamConfiguration.getUpstream("vector").url + endpoint; log.verbose("ScalarClient", "Performing request: " + url); diff --git a/src/scalar/VectorScalarClient.js b/src/scalar/VectorScalarClient.js index 0b932ce..5129070 100644 --- a/src/scalar/VectorScalarClient.js +++ b/src/scalar/VectorScalarClient.js @@ -1,6 +1,7 @@ var request = require('request'); var log = require("../util/LogService"); var config = require("config"); +var UpstreamConfiguration = require("../UpstreamConfiguration"); /** * Represents a scalar client for vector.im @@ -234,7 +235,7 @@ class VectorScalarClient { } _do(method, endpoint, qs = null, body = null) { - var url = config.get("upstreams.vector") + endpoint; + var url = UpstreamConfiguration.getUpstream("vector").url + endpoint; log.verbose("VectorScalarClient", "Performing request: " + url); diff --git a/src/storage/DimensionStore.js b/src/storage/DimensionStore.js index 4221902..a8f0172 100644 --- a/src/storage/DimensionStore.js +++ b/src/storage/DimensionStore.js @@ -65,7 +65,7 @@ class DimensionStore { * @param {string} mxid the matrix user id * @param {OpenID} openId the open ID * @param {string} scalarToken the token associated with the user - * @param {string} upstreamToken the upstream scalar token + * @param {String?} upstreamToken the upstream scalar token (optional) * @returns {Promise<>} resolves when complete */ createToken(mxid, openId, scalarToken, upstreamToken) { diff --git a/src/storage/models/tokens.js b/src/storage/models/tokens.js index d7cc3f5..75c9167 100644 --- a/src/storage/models/tokens.js +++ b/src/storage/models/tokens.js @@ -29,7 +29,7 @@ module.exports = function (sequelize, DataTypes) { }, upstreamToken: { type: DataTypes.STRING, - allowNull: false, + allowNull: true, field: 'upstreamToken' }, expires: {