diff --git a/README.md b/README.md index 8cc4cad..9495576 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,30 @@ -# matrix-dimension +# Dimension [![Greenkeeper badge](https://badges.greenkeeper.io/turt2live/matrix-dimension.svg)](https://greenkeeper.io/) [![TravisCI badge](https://travis-ci.org/turt2live/matrix-dimension.svg?branch=master)](https://travis-ci.org/turt2live/matrix-dimension) -An alternative to Scalar for Riot. +An alternative integrations manager for [Riot](https://riot.im). Join us on matrix: [#dimension:t2l.io](https://matrix.to/#/#dimension:t2l.io) + +# Configuring Riot to use Dimension + +Change the values in Riot's `config.json` as shown below. If you do not have a `config.json`, copy the `config.sample.json` from Riot. + +``` +"integrations_ui_url": "https://dimension.t2bot.io/riot", +"integrations_rest_url": "https://dimension.t2bot.io/api/v1/scalar", +``` + +The remaining settings should be tailored for your Riot deployment. + +# Running your own + +TODO + +# Building + +TODO + +# Development + +TODO \ No newline at end of file diff --git a/config/default.yaml b/config/default.yaml index edc1782..60a115c 100644 --- a/config/default.yaml +++ b/config/default.yaml @@ -5,26 +5,31 @@ bots: name: "Giphy" avatar: "/img/avatars/giphy.png" about: "Use `!giphy query` to find an animated GIF on demand" + upstreamType: "giphy" # Guggy (from matrix.org) - - mxid: "@neb_guggy:matrix.org" + - mxid: "@_neb_guggy:matrix.org" name: "Guggy" avatar: "/img/avatars/guggy.png" about: "Use `!guggy sentence` to create an animated GIF from a sentence" + upstreamType: "guggy" # Imgur (from matrix.org) - mxid: "@_neb_imgur:matrix.org" name: "Imgur" avatar: "/img/avatars/imgur.png" about: "Use `!imgur query` to find an image from Imgur" + upstreamType: "imgur" # Wikipedia (from matrix.org) - mxid: "@_neb_wikipedia:matrix.org" name: "Wikipedia" avatar: "/img/avatars/wikipedia.png" about: "Use `!wikipedia query` to find something from Wikipedia" + upstreamType: "wikipedia" # Google (from matrix.org) - mxid: "@_neb_google:matrix.org" name: "Google" avatar: "/img/avatars/google.png" about: "Use `!google image query` to find an image from Google" + upstreamType: "google" # The web settings for the service (API and UI) web: @@ -34,6 +39,10 @@ web: # The address to bind to (0.0.0.0 for all interfaces) address: '0.0.0.0' +# Upstream scalar configuration. This should almost never change. +scalar: + upstreamRestUrl: "https://scalar.vector.im/api" + # Settings for controlling how logging works logging: file: logs/dimension.log diff --git a/docs/scalar_client_api.md b/docs/scalar_client_api.md index 987c38b..384bbaa 100644 --- a/docs/scalar_client_api.md +++ b/docs/scalar_client_api.md @@ -53,8 +53,10 @@ then we can expect a response object that looks like this: An error response will always have the following structure under `response`: ``` { - "message": "Something went wrong", - "_error": + "error": { + "message": "Something went wrong", + "_error": + } } ``` diff --git a/migrations/20170527221910-add-upstream-scalar-token-to-tokens.js b/migrations/20170527221910-add-upstream-scalar-token-to-tokens.js new file mode 100644 index 0000000..7d025aa --- /dev/null +++ b/migrations/20170527221910-add-upstream-scalar-token-to-tokens.js @@ -0,0 +1,27 @@ +'use strict'; + +var dbm; +var type; +var seed; + +/** + * We receive the dbmigrate dependency from dbmigrate initially. + * This enables us to not have to rely on NODE_PATH. + */ +exports.setup = function (options, seedLink) { + dbm = options.dbmigrate; + type = dbm.dataType; + seed = seedLink; +}; + +exports.up = function (db) { + return db.addColumn('tokens', 'upstreamToken', {type: 'string', notNull: false}); // has to be nullable, despite our best intentions +}; + +exports.down = function (db) { + return db.removeColumn('tokens', 'upstreamToken'); +}; + +exports._meta = { + "version": 1 +}; diff --git a/package.json b/package.json index fbcaf80..e1fbe5e 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "@ng-bootstrap/ng-bootstrap": "^1.0.0-alpha.22", "@types/node": "^7.0.18", "angular2-template-loader": "^0.6.2", + "angular2-toaster": "^4.0.0", "angular2-ui-switch": "^1.2.0", "autoprefixer": "^6.7.7", "awesome-typescript-loader": "^3.1.2", diff --git a/src/Dimension.js b/src/Dimension.js index f1dab09..5a29644 100644 --- a/src/Dimension.js +++ b/src/Dimension.js @@ -6,12 +6,17 @@ 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"); /** * 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 @@ -41,8 +46,10 @@ class Dimension { }); this._app.post("/api/v1/scalar/register", this._scalarRegister.bind(this)); - this._app.get("/api/v1/dimension/bots", this._getBots.bind(this)); this._app.get("/api/v1/scalar/checkToken", this._checkScalarToken.bind(this)); + + this._app.get("/api/v1/dimension/bots", this._getBots.bind(this)); + this._app.post("/api/v1/dimension/kick", this._kickUser.bind(this)); } start() { @@ -50,6 +57,35 @@ class Dimension { log.info("Dimension", "API and UI listening on " + config.get("web.address") + ":" + config.get("web.port")); } + _kickUser(req, res) { + // {roomId: roomId, userId: userId, scalarToken: scalarToken} + 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 integrationName = null; + this._db.checkToken(scalarToken).then(() => { + for (var bot of config.bots) { + if (bot.mxid == userId) { + integrationName = bot.upstreamType; + break; + } + } + + return this._db.getUpstreamToken(scalarToken); + }).then(upstreamToken => { + if (!upstreamToken || !integrationName) { + res.status(400).send({error: "Missing token or integration name"}); + return Promise.resolve(); + } else return VectorScalarClient.removeIntegration(integrationName, roomId, upstreamToken); + }).then(() => res.status(200).send({success: true})).catch(err => res.status(500).send({error: err.message})); + } + _getBots(req, res) { res.setHeader("Content-Type", "application/json"); res.send(JSON.stringify(config.bots)); @@ -57,29 +93,36 @@ class Dimension { _checkScalarToken(req, res) { var token = req.query.scalar_token; - if (!token) res.sendStatus(404); + if (!token) res.sendStatus(400); else this._db.checkToken(token).then(() => { res.sendStatus(200); - }, () => res.sendStatus(404)); + }).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('Missing OpenID'); + res.status(400).send({error: 'Missing OpenID'}); return; } var client = new MatrixLiteClient(tokenInfo); var scalarToken = randomString({length: 25}); + var userId; client.getSelfMxid().then(mxid => { - return this._db.createToken(mxid, tokenInfo, scalarToken); + 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(JSON.stringify({scalar_token: scalarToken})); - }, err => { - throw err; - //res.status(500).send(err.message); + res.send({scalar_token: scalarToken}); + }).catch(err => { + log.error("Dimension", err); + res.status(500).send({error: err.message}); }); } } diff --git a/src/matrix/MatrixLiteClient.js b/src/matrix/MatrixLiteClient.js index 78d4c9d..9fa986c 100644 --- a/src/matrix/MatrixLiteClient.js +++ b/src/matrix/MatrixLiteClient.js @@ -43,6 +43,7 @@ class MatrixLiteClient { return new Promise((resolve, reject) => { request(params, (err, response, body) => { if (err) { + log.error("MatrixLiteClient", err); reject(err); } else resolve(response, body); }); diff --git a/src/scalar/ScalarClient.js b/src/scalar/ScalarClient.js new file mode 100644 index 0000000..9024980 --- /dev/null +++ b/src/scalar/ScalarClient.js @@ -0,0 +1,55 @@ +var request = require('request'); +var log = require("../util/LogService"); +var config = require("config"); + +/** + * Represents a scalar client + */ +class ScalarClient { + + /** + * Creates a new Scalar client + */ + constructor() { + } + + /** + * Registers for a scalar token + * @param {OpenID} openId the open ID to register + * @returns {Promise} resolves to a scalar token + */ + register(openId) { + return this._do("POST", "/register", null, openId).then((response, body) => { + if (response.statusCode !== 200) { + log.error("ScalarClient", response.body); + return Promise.reject(response.body); + } + + return response.body['scalar_token']; + }); + } + + _do(method, endpoint, qs = null, body = null) { + var url = config.scalar.upstreamRestUrl + endpoint; + + log.verbose("ScalarClient", "Performing request: " + url); + + var params = { + url: url, + method: method, + json: body, + qs: qs + }; + + return new Promise((resolve, reject) => { + request(params, (err, response, body) => { + if (err) { + log.error("ScalarClient", err); + reject(err); + } else resolve(response, body); + }); + }); + } +} + +module.exports = new ScalarClient(); \ No newline at end of file diff --git a/src/scalar/VectorScalarClient.js b/src/scalar/VectorScalarClient.js new file mode 100644 index 0000000..7e3d6db --- /dev/null +++ b/src/scalar/VectorScalarClient.js @@ -0,0 +1,69 @@ +var request = require('request'); +var log = require("../util/LogService"); +var config = require("config"); + +/** + * Represents a scalar client for vector.im + */ +class VectorScalarClient { + + /** + * Creates a new vector.im Scalar client + */ + constructor() { + } + + /** + * Registers for a scalar token + * @param {OpenID} openId the open ID to register + * @returns {Promise} resolves to a scalar token + */ + register(openId) { + return this._do("POST", "/register", null, openId).then((response, body) => { + var json = JSON.parse(response.body); + return json['scalar_token']; + }); + } + + /** + * Removes a scalar integration + * @param {string} type the type of integration to remove + * @param {string} roomId the room ID to remove it from + * @param {string} scalarToken the upstream scalar token + * @return {Promise<>} resolves when complete + */ + removeIntegration(type, roomId, scalarToken) { + return this._do("POST", "/removeIntegration", {scalar_token: scalarToken}, {type: type, room_id: roomId}).then((response, body) => { + if (response.statusCode !== 200) { + log.error("VectorScalarClient", response.body); + return Promise.reject(response.body); + } + + // no success processing + }); + } + + _do(method, endpoint, qs = null, body = null) { + var url = config.scalar.upstreamRestUrl + endpoint; + + log.verbose("VectorScalarClient", "Performing request: " + url); + + var params = { + url: url, + method: method, + json: body, + qs: qs + }; + + return new Promise((resolve, reject) => { + request(params, (err, response, body) => { + if (err) { + log.error("VectorScalarClient", err); + reject(err); + } else resolve(response, body); + }); + }); + } +} + +module.exports = new VectorScalarClient(); \ No newline at end of file diff --git a/src/storage/DimensionStore.js b/src/storage/DimensionStore.js index 8c76fa9..dac969d 100644 --- a/src/storage/DimensionStore.js +++ b/src/storage/DimensionStore.js @@ -65,14 +65,16 @@ 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 * @returns {Promise<>} resolves when complete */ - createToken(mxid, openId, scalarToken) { + createToken(mxid, openId, scalarToken, upstreamToken) { return this.__Tokens.create({ matrixUserId: mxid, matrixServerName: openId.matrix_server_name, matrixAccessToken: openId.access_token, scalarToken: scalarToken, + upstreamToken: upstreamToken, expires: moment().add(openId.expires_in, 'seconds').toDate() }); } @@ -89,6 +91,18 @@ class DimensionStore { return Promise.resolve(); }); } + + /** + * Gets the upstream token for a given scalar token + * @param {string} scalarToken the scalar token to lookup + * @returns {Promise} resolves to the upstream token, or null if not found + */ + getUpstreamToken(scalarToken) { + return this.__Tokens.find({where: {scalarToken: scalarToken}}).then(token => { + if (!token) return null; + return token.upstreamToken; + }); + } } module.exports = DimensionStore; \ No newline at end of file diff --git a/src/storage/models/tokens.js b/src/storage/models/tokens.js index b616583..d7cc3f5 100644 --- a/src/storage/models/tokens.js +++ b/src/storage/models/tokens.js @@ -27,6 +27,11 @@ module.exports = function (sequelize, DataTypes) { allowNull: false, field: 'scalarToken' }, + upstreamToken: { + type: DataTypes.STRING, + allowNull: false, + field: 'upstreamToken' + }, expires: { type: DataTypes.TIME, allowNull: false, diff --git a/web/app/app.component.html b/web/app/app.component.html index 32f783e..27c14e7 100644 --- a/web/app/app.component.html +++ b/web/app/app.component.html @@ -1,6 +1,7 @@
+