Restructure backend integrations to better support the various types

This commit is contained in:
turt2live 2017-05-28 17:13:03 -06:00
parent 9ebd87bd88
commit 574c24bcd6
18 changed files with 359 additions and 212 deletions

3
app.js
View File

@ -7,8 +7,7 @@ var config = require("config");
log.info("app", "Bootstrapping Dimension...");
var db = new DimensionStore();
db.prepare().then(() => {
var app = new Dimension(db);
app.start();
Dimension.start(db);
if (config.get("demobot.enabled")) {
log.info("app", "Demo bot enabled - starting up");

View File

@ -4,29 +4,27 @@ var log = require("./util/LogService");
var DimensionStore = require("./storage/DimensionStore");
var bodyParser = require('body-parser');
var path = require("path");
var MatrixLiteClient = require("./matrix/MatrixLiteClient");
var randomString = require("random-string");
var ScalarClient = require("./scalar/ScalarClient.js");
var VectorScalarClient = require("./scalar/VectorScalarClient");
var integrations = require("./integration");
var _ = require("lodash");
var UpstreamIntegration = require("./integration/UpstreamIntegration");
var HostedIntegration = require("./integration/HostedIntegration");
var IntegrationImpl = require("./integration/impl");
var DimensionApi = require("./DimensionApi");
var ScalarApi = require("./ScalarApi");
// TODO: Convert backend to typescript? Would avoid stubbing classes all over the place
/**
* Primary entry point for Dimension
*/
class Dimension {
// TODO: Spread the app out into other classes
// eg: ScalarApi, DimensionApi, etc
/**
* Creates a new Dimension
* @param {DimensionStore} db the storage
*/
constructor(db) {
constructor() {
}
/**
* Starts the Dimension service
* @param {DimensionStore} db the store to use
*/
start(db) {
this._db = db;
this._app = express();
this._app.use(express.static('web-dist'));
@ -50,122 +48,12 @@ class Dimension {
next();
});
this._app.post("/api/v1/scalar/register", this._scalarRegister.bind(this));
this._app.get("/api/v1/scalar/checkToken", this._checkScalarToken.bind(this));
DimensionApi.bootstrap(this._app, this._db);
ScalarApi.bootstrap(this._app, this._db);
this._app.get("/api/v1/dimension/integrations/:roomId", this._getIntegrations.bind(this));
this._app.post("/api/v1/dimension/removeIntegration", this._removeIntegration.bind(this));
}
start() {
this._app.listen(config.get('web.port'), config.get('web.address'));
log.info("Dimension", "API and UI listening on " + config.get("web.address") + ":" + config.get("web.port"));
}
_removeIntegration(req, res) {
var roomId = req.body.roomId;
var userId = req.body.userId;
var scalarToken = req.body.scalarToken;
if (!roomId || !userId || !scalarToken) {
res.status(400).send({error: "Missing room, user, or token"});
return;
}
var integrationConfig = integrations.byUserId[userId];
if (!integrationConfig) {
res.status(400).send({error: "Unknown integration"});
return;
}
this._db.checkToken(scalarToken).then(() => {
if (integrationConfig.upstream) {
return this._db.getUpstreamToken(scalarToken).then(upstreamToken => new UpstreamIntegration(integrationConfig, upstreamToken));
} else return new HostedIntegration(integrationConfig);
}).then(integration => integration.leaveRoom(roomId)).then(() => {
res.status(200).send({success: true});
}).catch(err => res.status(500).send({error: err.message}));
}
_getIntegrations(req, res) {
res.setHeader("Content-Type", "application/json");
var scalarToken = req.query.scalar_token;
this._db.checkToken(scalarToken).then(() => {
var roomId = req.params.roomId;
if (!roomId) {
res.status(400).send({error: 'Missing room ID'});
return;
}
var results = _.map(integrations.all, i => JSON.parse(JSON.stringify(i)));
var promises = [];
_.forEach(results, i => {
if (IntegrationImpl[i.type]) {
var confs = IntegrationImpl[i.type];
if (confs[i.integrationType]) {
log.info("Dimension", "Using special configuration for " + i.type + " (" + i.integrationType + ")");
promises.push(confs[i.integrationType](this._db, i, roomId, scalarToken).then(integration => {
return integration.getUserId().then(userId=> {
i.userId = userId;
return integration.getState();
}).then(state=> {
for (var key in state) {
i[key] = state[key];
}
});
}))
} else log.verbose("Dimension", "No special configuration needs for " + i.type + " (" + i.integrationType + ")");
} else log.verbose("Dimension", "No special implementation type for " + i.type);
});
Promise.all(promises).then(() => res.send(_.map(results, integration => {
integration.upstream = undefined;
integration.hosted = undefined;
return integration;
})));
}).catch(err => {
log.error("Dimension", err);
res.status(500).send({error: err});
});
}
_checkScalarToken(req, res) {
var token = req.query.scalar_token;
if (!token) res.sendStatus(400);
else this._db.checkToken(token).then(() => {
res.sendStatus(200);
}).catch(() => res.sendStatus(401));
}
_scalarRegister(req, res) {
res.setHeader("Content-Type", "application/json");
var tokenInfo = req.body;
if (!tokenInfo || !tokenInfo['access_token'] || !tokenInfo['token_type'] || !tokenInfo['matrix_server_name'] || !tokenInfo['expires_in']) {
res.status(400).send({error: 'Missing OpenID'});
return;
}
var client = new MatrixLiteClient(tokenInfo);
var scalarToken = randomString({length: 25});
var userId;
client.getSelfMxid().then(mxid => {
userId = mxid;
if (!mxid) throw new Error("Token does not resolve to a matrix user");
return ScalarClient.register(tokenInfo);
}).then(upstreamToken => {
return this._db.createToken(userId, tokenInfo, scalarToken, upstreamToken);
}).then(() => {
res.setHeader("Content-Type", "application/json");
res.send({scalar_token: scalarToken});
}).catch(err => {
log.error("Dimension", err);
res.status(500).send({error: err.message});
});
}
}
module.exports = Dimension;
module.exports = new Dimension();

107
src/DimensionApi.js Normal file
View File

@ -0,0 +1,107 @@
var IntegrationImpl = require("./integration/impl/index");
var Integrations = require("./integration/index");
var _ = require("lodash");
var log = require("./util/LogService");
/**
* API handler for the Dimension API
*/
class DimensionApi {
/**
* Creates a new Dimension API
*/
constructor() {
}
/**
* Bootstraps the Dimension API
* @param {*} app the Express application
* @param {DimensionStore} db the store to use
*/
bootstrap(app, db) {
this._db = db;
app.get("/api/v1/dimension/integrations/:roomId", this._getIntegrations.bind(this));
app.post("/api/v1/dimension/removeIntegration", this._removeIntegration.bind(this));
}
_getIntegration(integrationConfig, roomId, scalarToken) {
var factory = IntegrationImpl.getFactory(integrationConfig);
if (!factory) throw new Error("Missing config factory for " + integrationConfig.name);
return factory(this._db, integrationConfig, roomId, scalarToken);
}
_getIntegrations(req, res) {
res.setHeader("Content-Type", "application/json");
var roomId = req.params.roomId;
if (!roomId) {
res.status(400).send({error: 'Missing room ID'});
return;
}
var scalarToken = req.query.scalar_token;
this._db.checkToken(scalarToken).then(() => {
var integrations = _.map(Integrations.all, i => JSON.parse(JSON.stringify(i))); // clone
var promises = [];
_.forEach(integrations, integration => {
promises.push(this._getIntegration(integration, roomId, scalarToken).then(builtIntegration => {
return builtIntegration.getUserId().then(userId => {
integration.userId = userId;
return builtIntegration.getState();
}).then(state => {
var keys = _.keys(state);
for (var key of keys) {
integration[key] = state[key];
}
});
}));
});
Promise.all(promises).then(() => res.send(_.map(integrations, integration => {
// Remove sensitive material
integration.upstream = undefined;
integration.hosted = undefined;
return integration;
})));
}).catch(err => {
log.error("DimensionApi", err);
console.error(err);
res.status(500).send({error: err});
});
}
_removeIntegration(req, res) {
var roomId = req.body.roomId;
var userId = req.body.userId;
var scalarToken = req.body.scalarToken;
if (!roomId || !userId || !scalarToken) {
res.status(400).send({error: "Missing room, user, or token"});
return;
}
var integrationConfig = Integrations.byUserId[userId];
if (!integrationConfig) {
res.status(400).send({error: "Unknown integration"});
return;
}
log.info("DimensionApi", "Remove requested for " + userId + " in room " + roomId);
this._db.checkToken(scalarToken).then(() => {
return this._getIntegration(integrationConfig, roomId, scalarToken);
}).then(integration => integration.removeFromRoom(roomId)).then(() => {
res.status(200).send({success: true});
}).catch(err => {
log.error("DimensionApi", err);
console.error(err);
res.status(500).send({error: err.message});
});
}
}
module.exports = new DimensionApi();

66
src/ScalarApi.js Normal file
View File

@ -0,0 +1,66 @@
var MatrixLiteClient = require("./matrix/MatrixLiteClient");
var randomString = require("random-string");
var ScalarClient = require("./scalar/ScalarClient.js");
var _ = require("lodash");
var log = require("./util/LogService");
/**
* API handler for the Scalar API, as required by Riot
*/
class ScalarApi {
/**
* Creates a new Scalar API
*/
constructor() {
}
/**
* Bootstraps the Scalar API
* @param {*} app the Express application
* @param {DimensionStore} db the store to use
*/
bootstrap(app, db) {
this._db = db;
app.post("/api/v1/scalar/register", this._scalarRegister.bind(this));
app.get("/api/v1/scalar/checkToken", this._checkScalarToken.bind(this));
}
_checkScalarToken(req, res) {
var token = req.query.scalar_token;
if (!token) res.sendStatus(400);
else this._db.checkToken(token).then(() => {
res.sendStatus(200);
}).catch(() => res.sendStatus(401));
}
_scalarRegister(req, res) {
res.setHeader("Content-Type", "application/json");
var tokenInfo = req.body;
if (!tokenInfo || !tokenInfo['access_token'] || !tokenInfo['token_type'] || !tokenInfo['matrix_server_name'] || !tokenInfo['expires_in']) {
res.status(400).send({error: 'Missing OpenID'});
return;
}
var client = new MatrixLiteClient(tokenInfo);
var scalarToken = randomString({length: 25});
var userId;
client.getSelfMxid().then(mxid => {
userId = mxid;
if (!mxid) throw new Error("Token does not resolve to a matrix user");
return ScalarClient.register(tokenInfo);
}).then(upstreamToken => {
return this._db.createToken(userId, tokenInfo, scalarToken, upstreamToken);
}).then(() => {
res.setHeader("Content-Type", "application/json");
res.send({scalar_token: scalarToken});
}).catch(err => {
log.error("ScalarApi", err);
res.status(500).send({error: err.message});
});
}
}
module.exports = new ScalarApi();

View File

@ -1,35 +0,0 @@
var sdk = require("matrix-js-sdk");
var log = require("../util/LogService");
var StubbedIntegration = require("./StubbedIntegration");
/**
* Represents an integration hosted on a known homeserver
*/
class HostedIntegration extends StubbedIntegration {
/**
* Creates a new hosted integration
* @param integrationSettings the integration settings
*/
constructor(integrationSettings) {
super();
this._settings = integrationSettings;
this._client = sdk.createClient({
baseUrl: this._settings.hosted.homeserverUrl,
accessToken: this._settings.hosted.accessToken,
userId: this._settings.userId,
});
}
/**
* Leaves a given Matrix room
* @param {string} roomId the room to leave
* @returns {Promise<>} resolves when completed
*/
leaveRoom(roomId) {
log.info("HostedIntegration", "Removing " + this._settings.userId + " from " + roomId);
return this._client.leave(roomId);
}
}
module.exports = HostedIntegration;

View File

@ -1,7 +0,0 @@
class StubbedIntegration {
leaveRoom(roomId) {
throw new Error("Not implemented");
}
}
module.exports = StubbedIntegration;

View File

@ -1,33 +0,0 @@
var VectorScalarClient = require("../scalar/VectorScalarClient");
var log = require("../util/LogService");
var StubbedIntegration = require("./StubbedIntegration");
/**
* An integration that is handled by an upstream Scalar instance
*/
class UpstreamIntegration extends StubbedIntegration {
/**
* Creates a new hosted integration
* @param integrationSettings the integration settings
* @param {string} upstreamToken the upstream scalar token
*/
constructor(integrationSettings, upstreamToken) {
super();
this._settings = integrationSettings;
this._upstreamToken = upstreamToken;
if (this._settings.upstream.type !== "vector") throw new Error("Unknown upstream type: " + this._settings.upstream.type);
}
/**
* Leaves a given Matrix room
* @param {string} roomId the room to leave
* @returns {Promise<>} resolves when completed
*/
leaveRoom(roomId) {
log.info("UpstreamIntegration", "Removing " + this._settings.userId + " from " + roomId);
return VectorScalarClient.removeIntegration(this._settings.upstream.id, roomId, this._upstreamToken);
}
}
module.exports = UpstreamIntegration;

View File

@ -1,3 +1,5 @@
var IntegrationStub = require("../type/IntegrationStub");
/**
* Creates an integration using the given
* @param {DimensionStore} db the database
@ -7,5 +9,5 @@
* @returns {Promise<*>} resolves to the configured integration
*/
module.exports = (db, integrationConfig, roomId, scalarToken) => {
throw new Error("Not implemented");
return Promise.resolve(new IntegrationStub(integrationConfig));
};

View File

@ -1,7 +1,35 @@
var log = require("../../util/LogService");
var StubbedFactory = require("./StubbedFactory");
var SimpleBotFactory = require("./simple_bot/SimpleBotFactory");
var RSSFactory = require("./rss/RSSFactory");
module.exports = {
var mapping = {
"complex-bot": {
"rss": RSSFactory
}
};
var defaultFactories = {
"complex-bot": null,
"bot": SimpleBotFactory
};
module.exports = {
getFactory: (integrationConfig) => {
var opts = mapping[integrationConfig.type];
if (!opts) {
log.verbose("IntegrationImpl", "No option set available for " + integrationConfig.type + " - will attempt defaults");
}
var factory = null;
if (!opts) factory = defaultFactories[integrationConfig.type];
else factory = opts[integrationConfig.integrationType];
if (!factory) {
log.verbose("IntegrationImpl", "No factory available for " + integrationConfig.type + " (" + integrationConfig.integrationType + ") - using stub");
factory = StubbedFactory;
}
return factory;
}
};

View File

@ -20,15 +20,16 @@ class RSSBot extends ComplexBot {
return this._backbone.getUserId();
}
getFeeds() {
return this._backbone.getFeeds();
/*override*/
getState() {
return this._backbone.getFeeds().then(feeds => {
return {feeds: feeds};
});
}
/*override*/
getState() {
return this.getFeeds().then(feeds => {
return {feeds: feeds};
});
removeFromRoom(roomId) {
return this._backbone.removeFromRoom(roomId);
}
}

View File

@ -24,6 +24,15 @@ class StubbedRssBackbone {
getFeeds() {
throw new Error("Not implemented");
}
/**
* Removes the bot from the given room
* @param {string} roomId the room ID to remove the bot from
* @returns {Promise<>} resolves when completed
*/
removeFromRoom(roomId) {
throw new Error("Not implemented");
}
}
module.exports = StubbedRssBackbone;

View File

@ -1,6 +1,7 @@
var StubbedRssBackbone = require("./StubbedRssBackbone");
var VectorScalarClient = require("../../../scalar/VectorScalarClient");
var _ = require("lodash");
var log = require("../../../util/LogService");
/**
* Backbone for RSS bots running on vector.im through scalar
@ -40,6 +41,10 @@ class VectorRssBackbone extends StubbedRssBackbone {
});
}
/*override*/
removeFromRoom(roomId) {
return VectorScalarClient.removeIntegration(this._config.upstream.id, roomId, this._upstreamToken);
}
}
module.exports = VectorRssBackbone;

View File

@ -0,0 +1,36 @@
var sdk = require("matrix-js-sdk");
var log = require("../../../util/LogService");
var IntegrationStub = require("../../type/IntegrationStub");
/**
* Standalone (matrix) backbone for simple bots
*/
class HostedSimpleBackbone extends IntegrationStub {
/**
* Creates a new standalone bot backbone
* @param {*} botConfig the configuration for the bot
*/
constructor(botConfig) {
super(botConfig);
this._config = botConfig;
this._client = sdk.createClient({
baseUrl: this._config.hosted.homeserverUrl,
accessToken: this._config.hosted.accessToken,
userId: this._config.userId,
});
}
/**
* Leaves a given Matrix room
* @param {string} roomId the room to leave
* @returns {Promise<>} resolves when completed
*/
/*override*/
removeFromRoom(roomId) {
log.info("HostedSimpleBackbone", "Removing " + this._settings.userId + " from " + roomId);
return this._client.leave(roomId);
}
}
module.exports = HostedSimpleBackbone;

View File

@ -0,0 +1,35 @@
var ComplexBot = require("../../type/ComplexBot");
/**
* Represents an RSS bot
*/
class RSSBot extends ComplexBot {
/**
* Creates a new RSS bot
* @param botConfig the bot configuration
* @param backbone the backbone powering this bot
*/
constructor(botConfig, backbone) {
super(botConfig);
this._backbone = backbone;
}
/*override*/
getUserId() {
return this._backbone.getUserId();
}
getFeeds() {
return this._backbone.getFeeds();
}
/*override*/
getState() {
return this.getFeeds().then(feeds => {
return {feeds: feeds};
});
}
}
module.exports = RSSBot;

View File

@ -0,0 +1,12 @@
var RSSBot = require("./RSSBot");
var VectorRssBackbone = require("./VectorRssBackbone");
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");
};

View File

@ -0,0 +1,3 @@
/**
* Created by Travis on 5/28/2017.
*/

View File

@ -0,0 +1,22 @@
/**
* Stubbed/placeholder simple bot backbone
*/
class StubbedSimpleBackbone {
/**
* Creates a new stubbed RSS backbone
*/
constructor() {
}
/**
* Leaves a given Matrix room
* @param {string} roomId the room to leave
* @returns {Promise<>} resolves when completed
*/
leaveRoom(roomId) {
throw new Error("Not implemented");
}
}
module.exports = StubbedRssBackbone;

View File

@ -21,6 +21,15 @@ class IntegrationStub {
getState() {
return Promise.resolve({});
}
/**
* Removes the integration from the given room
* @param {string} roomId the room ID to remove the integration from
* @returns {Promise<>} resolves when completed
*/
removeFromRoom(roomId) {
throw new Error("Not implemented");
}
}
module.exports = IntegrationStub;