parent
22304d716c
commit
16e28019bc
27
README.md
27
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)
|
[![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)
|
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
|
|
@ -5,26 +5,31 @@ bots:
|
||||||
name: "Giphy"
|
name: "Giphy"
|
||||||
avatar: "/img/avatars/giphy.png"
|
avatar: "/img/avatars/giphy.png"
|
||||||
about: "Use `!giphy query` to find an animated GIF on demand"
|
about: "Use `!giphy query` to find an animated GIF on demand"
|
||||||
|
upstreamType: "giphy"
|
||||||
# Guggy (from matrix.org)
|
# Guggy (from matrix.org)
|
||||||
- mxid: "@neb_guggy:matrix.org"
|
- mxid: "@_neb_guggy:matrix.org"
|
||||||
name: "Guggy"
|
name: "Guggy"
|
||||||
avatar: "/img/avatars/guggy.png"
|
avatar: "/img/avatars/guggy.png"
|
||||||
about: "Use `!guggy sentence` to create an animated GIF from a sentence"
|
about: "Use `!guggy sentence` to create an animated GIF from a sentence"
|
||||||
|
upstreamType: "guggy"
|
||||||
# Imgur (from matrix.org)
|
# Imgur (from matrix.org)
|
||||||
- mxid: "@_neb_imgur:matrix.org"
|
- mxid: "@_neb_imgur:matrix.org"
|
||||||
name: "Imgur"
|
name: "Imgur"
|
||||||
avatar: "/img/avatars/imgur.png"
|
avatar: "/img/avatars/imgur.png"
|
||||||
about: "Use `!imgur query` to find an image from Imgur"
|
about: "Use `!imgur query` to find an image from Imgur"
|
||||||
|
upstreamType: "imgur"
|
||||||
# Wikipedia (from matrix.org)
|
# Wikipedia (from matrix.org)
|
||||||
- mxid: "@_neb_wikipedia:matrix.org"
|
- mxid: "@_neb_wikipedia:matrix.org"
|
||||||
name: "Wikipedia"
|
name: "Wikipedia"
|
||||||
avatar: "/img/avatars/wikipedia.png"
|
avatar: "/img/avatars/wikipedia.png"
|
||||||
about: "Use `!wikipedia query` to find something from Wikipedia"
|
about: "Use `!wikipedia query` to find something from Wikipedia"
|
||||||
|
upstreamType: "wikipedia"
|
||||||
# Google (from matrix.org)
|
# Google (from matrix.org)
|
||||||
- mxid: "@_neb_google:matrix.org"
|
- mxid: "@_neb_google:matrix.org"
|
||||||
name: "Google"
|
name: "Google"
|
||||||
avatar: "/img/avatars/google.png"
|
avatar: "/img/avatars/google.png"
|
||||||
about: "Use `!google image query` to find an image from Google"
|
about: "Use `!google image query` to find an image from Google"
|
||||||
|
upstreamType: "google"
|
||||||
|
|
||||||
# The web settings for the service (API and UI)
|
# The web settings for the service (API and UI)
|
||||||
web:
|
web:
|
||||||
|
@ -34,6 +39,10 @@ web:
|
||||||
# The address to bind to (0.0.0.0 for all interfaces)
|
# The address to bind to (0.0.0.0 for all interfaces)
|
||||||
address: '0.0.0.0'
|
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
|
# Settings for controlling how logging works
|
||||||
logging:
|
logging:
|
||||||
file: logs/dimension.log
|
file: logs/dimension.log
|
||||||
|
|
|
@ -53,9 +53,11 @@ then we can expect a response object that looks like this:
|
||||||
An error response will always have the following structure under `response`:
|
An error response will always have the following structure under `response`:
|
||||||
```
|
```
|
||||||
{
|
{
|
||||||
|
"error": {
|
||||||
"message": "Something went wrong",
|
"message": "Something went wrong",
|
||||||
"_error": <original Error object>
|
"_error": <original Error object>
|
||||||
}
|
}
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Actions
|
## Actions
|
||||||
|
|
|
@ -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
|
||||||
|
};
|
|
@ -42,6 +42,7 @@
|
||||||
"@ng-bootstrap/ng-bootstrap": "^1.0.0-alpha.22",
|
"@ng-bootstrap/ng-bootstrap": "^1.0.0-alpha.22",
|
||||||
"@types/node": "^7.0.18",
|
"@types/node": "^7.0.18",
|
||||||
"angular2-template-loader": "^0.6.2",
|
"angular2-template-loader": "^0.6.2",
|
||||||
|
"angular2-toaster": "^4.0.0",
|
||||||
"angular2-ui-switch": "^1.2.0",
|
"angular2-ui-switch": "^1.2.0",
|
||||||
"autoprefixer": "^6.7.7",
|
"autoprefixer": "^6.7.7",
|
||||||
"awesome-typescript-loader": "^3.1.2",
|
"awesome-typescript-loader": "^3.1.2",
|
||||||
|
|
|
@ -6,12 +6,17 @@ var bodyParser = require('body-parser');
|
||||||
var path = require("path");
|
var path = require("path");
|
||||||
var MatrixLiteClient = require("./matrix/MatrixLiteClient");
|
var MatrixLiteClient = require("./matrix/MatrixLiteClient");
|
||||||
var randomString = require("random-string");
|
var randomString = require("random-string");
|
||||||
|
var ScalarClient = require("./scalar/ScalarClient.js");
|
||||||
|
var VectorScalarClient = require("./scalar/VectorScalarClient");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Primary entry point for Dimension
|
* Primary entry point for Dimension
|
||||||
*/
|
*/
|
||||||
class Dimension {
|
class Dimension {
|
||||||
|
|
||||||
|
// TODO: Spread the app out into other classes
|
||||||
|
// eg: ScalarApi, DimensionApi, etc
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new Dimension
|
* Creates a new Dimension
|
||||||
* @param {DimensionStore} db the storage
|
* @param {DimensionStore} db the storage
|
||||||
|
@ -41,8 +46,10 @@ class Dimension {
|
||||||
});
|
});
|
||||||
|
|
||||||
this._app.post("/api/v1/scalar/register", this._scalarRegister.bind(this));
|
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/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() {
|
start() {
|
||||||
|
@ -50,6 +57,35 @@ class Dimension {
|
||||||
log.info("Dimension", "API and UI listening on " + config.get("web.address") + ":" + config.get("web.port"));
|
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) {
|
_getBots(req, res) {
|
||||||
res.setHeader("Content-Type", "application/json");
|
res.setHeader("Content-Type", "application/json");
|
||||||
res.send(JSON.stringify(config.bots));
|
res.send(JSON.stringify(config.bots));
|
||||||
|
@ -57,29 +93,36 @@ class Dimension {
|
||||||
|
|
||||||
_checkScalarToken(req, res) {
|
_checkScalarToken(req, res) {
|
||||||
var token = req.query.scalar_token;
|
var token = req.query.scalar_token;
|
||||||
if (!token) res.sendStatus(404);
|
if (!token) res.sendStatus(400);
|
||||||
else this._db.checkToken(token).then(() => {
|
else this._db.checkToken(token).then(() => {
|
||||||
res.sendStatus(200);
|
res.sendStatus(200);
|
||||||
}, () => res.sendStatus(404));
|
}).catch(() => res.sendStatus(401));
|
||||||
}
|
}
|
||||||
|
|
||||||
_scalarRegister(req, res) {
|
_scalarRegister(req, res) {
|
||||||
|
res.setHeader("Content-Type", "application/json");
|
||||||
|
|
||||||
var tokenInfo = req.body;
|
var tokenInfo = req.body;
|
||||||
if (!tokenInfo || !tokenInfo['access_token'] || !tokenInfo['token_type'] || !tokenInfo['matrix_server_name'] || !tokenInfo['expires_in']) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var client = new MatrixLiteClient(tokenInfo);
|
var client = new MatrixLiteClient(tokenInfo);
|
||||||
var scalarToken = randomString({length: 25});
|
var scalarToken = randomString({length: 25});
|
||||||
|
var userId;
|
||||||
client.getSelfMxid().then(mxid => {
|
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(() => {
|
}).then(() => {
|
||||||
res.setHeader("Content-Type", "application/json");
|
res.setHeader("Content-Type", "application/json");
|
||||||
res.send(JSON.stringify({scalar_token: scalarToken}));
|
res.send({scalar_token: scalarToken});
|
||||||
}, err => {
|
}).catch(err => {
|
||||||
throw err;
|
log.error("Dimension", err);
|
||||||
//res.status(500).send(err.message);
|
res.status(500).send({error: err.message});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,6 +43,7 @@ class MatrixLiteClient {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
request(params, (err, response, body) => {
|
request(params, (err, response, body) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
|
log.error("MatrixLiteClient", err);
|
||||||
reject(err);
|
reject(err);
|
||||||
} else resolve(response, body);
|
} else resolve(response, body);
|
||||||
});
|
});
|
||||||
|
|
55
src/scalar/ScalarClient.js
Normal file
55
src/scalar/ScalarClient.js
Normal file
|
@ -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<string>} 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();
|
69
src/scalar/VectorScalarClient.js
Normal file
69
src/scalar/VectorScalarClient.js
Normal file
|
@ -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<string>} 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();
|
|
@ -65,14 +65,16 @@ class DimensionStore {
|
||||||
* @param {string} mxid the matrix user id
|
* @param {string} mxid the matrix user id
|
||||||
* @param {OpenID} openId the open ID
|
* @param {OpenID} openId the open ID
|
||||||
* @param {string} scalarToken the token associated with the user
|
* @param {string} scalarToken the token associated with the user
|
||||||
|
* @param {string} upstreamToken the upstream scalar token
|
||||||
* @returns {Promise<>} resolves when complete
|
* @returns {Promise<>} resolves when complete
|
||||||
*/
|
*/
|
||||||
createToken(mxid, openId, scalarToken) {
|
createToken(mxid, openId, scalarToken, upstreamToken) {
|
||||||
return this.__Tokens.create({
|
return this.__Tokens.create({
|
||||||
matrixUserId: mxid,
|
matrixUserId: mxid,
|
||||||
matrixServerName: openId.matrix_server_name,
|
matrixServerName: openId.matrix_server_name,
|
||||||
matrixAccessToken: openId.access_token,
|
matrixAccessToken: openId.access_token,
|
||||||
scalarToken: scalarToken,
|
scalarToken: scalarToken,
|
||||||
|
upstreamToken: upstreamToken,
|
||||||
expires: moment().add(openId.expires_in, 'seconds').toDate()
|
expires: moment().add(openId.expires_in, 'seconds').toDate()
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -89,6 +91,18 @@ class DimensionStore {
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the upstream token for a given scalar token
|
||||||
|
* @param {string} scalarToken the scalar token to lookup
|
||||||
|
* @returns {Promise<string>} 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;
|
module.exports = DimensionStore;
|
|
@ -27,6 +27,11 @@ module.exports = function (sequelize, DataTypes) {
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
field: 'scalarToken'
|
field: 'scalarToken'
|
||||||
},
|
},
|
||||||
|
upstreamToken: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: false,
|
||||||
|
field: 'upstreamToken'
|
||||||
|
},
|
||||||
expires: {
|
expires: {
|
||||||
type: DataTypes.TIME,
|
type: DataTypes.TIME,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
<header>
|
<header>
|
||||||
</header>
|
</header>
|
||||||
<main>
|
<main>
|
||||||
|
<toaster-container></toaster-container>
|
||||||
<router-outlet></router-outlet>
|
<router-outlet></router-outlet>
|
||||||
</main>
|
</main>
|
||||||
<footer>
|
<footer>
|
||||||
|
|
|
@ -11,6 +11,9 @@ import { RiotComponent } from "./riot/riot.component";
|
||||||
import { ApiService } from "./shared/api.service";
|
import { ApiService } from "./shared/api.service";
|
||||||
import { BotComponent } from "./bot/bot.component";
|
import { BotComponent } from "./bot/bot.component";
|
||||||
import { UiSwitchModule } from "angular2-ui-switch";
|
import { UiSwitchModule } from "angular2-ui-switch";
|
||||||
|
import { ScalarService } from "./shared/scalar.service";
|
||||||
|
import { ToasterModule } from "angular2-toaster";
|
||||||
|
import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
|
@ -20,6 +23,8 @@ import { UiSwitchModule } from "angular2-ui-switch";
|
||||||
routing,
|
routing,
|
||||||
NgbModule.forRoot(),
|
NgbModule.forRoot(),
|
||||||
UiSwitchModule,
|
UiSwitchModule,
|
||||||
|
ToasterModule,
|
||||||
|
BrowserAnimationsModule,
|
||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
AppComponent,
|
AppComponent,
|
||||||
|
@ -31,6 +36,7 @@ import { UiSwitchModule } from "angular2-ui-switch";
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
ApiService,
|
ApiService,
|
||||||
|
ScalarService,
|
||||||
|
|
||||||
// Vendor
|
// Vendor
|
||||||
],
|
],
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
<b>{{ bot.name }}</b>
|
<b>{{ bot.name }}</b>
|
||||||
<div style="display: flex;">
|
<div style="display: flex;">
|
||||||
<div class="switch">
|
<div class="switch">
|
||||||
<ui-switch [checked]="bot.isEnabled" size="small" [disabled]="updating" (change)="update()"></ui-switch>
|
<ui-switch [checked]="bot.isEnabled" size="small" [disabled]="bot.isBroken" (change)="update()"></ui-switch>
|
||||||
</div>
|
</div>
|
||||||
<div class="toolbar">
|
<div class="toolbar">
|
||||||
<i class="fa fa-question-circle text-info" ngbTooltip="{{bot.about}}" *ngIf="bot.about"></i>
|
<i class="fa fa-question-circle text-info" ngbTooltip="{{bot.about}}" *ngIf="bot.about"></i>
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Component, Input } from "@angular/core";
|
import { Component, Input, Output, EventEmitter } from "@angular/core";
|
||||||
import { Bot } from "../shared/models/bot";
|
import { Bot } from "../shared/models/bot";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
|
@ -9,15 +9,13 @@ import { Bot } from "../shared/models/bot";
|
||||||
export class BotComponent {
|
export class BotComponent {
|
||||||
|
|
||||||
@Input() bot: Bot;
|
@Input() bot: Bot;
|
||||||
|
@Output() updated: EventEmitter<any> = new EventEmitter();
|
||||||
public updating = false;
|
|
||||||
public htmlAbout: string;
|
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
}
|
}
|
||||||
|
|
||||||
public update(): void {
|
public update(): void {
|
||||||
this.updating = true;
|
this.bot.isEnabled = !this.bot.isEnabled;
|
||||||
setTimeout(() => this.updating = false, Math.random() * 15000);
|
this.updated.emit();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,13 @@
|
||||||
<div *ngIf="error">
|
<div *ngIf="error">
|
||||||
<p class="text-danger">{{ error }}</p>
|
<p class="text-danger">{{ error }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div *ngIf="!error">
|
<div *ngIf="loading && !error">
|
||||||
|
<p><i class="fa fa-circle-o-notch fa-spin"></i> Loading...</p>
|
||||||
|
</div>
|
||||||
|
<div *ngIf="!error && !loading">
|
||||||
<h3>Manage Integrations</h3>
|
<h3>Manage Integrations</h3>
|
||||||
<p>Turn on anything you like. If an integration has some special configuration, it will have a configuration icon next to it.</p>
|
<p>Turn on anything you like. If an integration has some special configuration, it will have a configuration icon next to it.</p>
|
||||||
<div class="integration-container">
|
<div class="integration-container">
|
||||||
<my-bot *ngFor="let bot of bots" [bot]="bot"></my-bot>
|
<my-bot *ngFor="let bot of bots" [bot]="bot" (updated)="updateBot(bot)"></my-bot>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
|
@ -2,6 +2,8 @@ import { Component } from "@angular/core";
|
||||||
import { ActivatedRoute } from "@angular/router";
|
import { ActivatedRoute } from "@angular/router";
|
||||||
import { ApiService } from "../shared/api.service";
|
import { ApiService } from "../shared/api.service";
|
||||||
import { Bot } from "../shared/models/bot";
|
import { Bot } from "../shared/models/bot";
|
||||||
|
import { ScalarService } from "../shared/scalar.service";
|
||||||
|
import { ToasterService } from "angular2-toaster";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'my-riot',
|
selector: 'my-riot',
|
||||||
|
@ -12,21 +14,71 @@ export class RiotComponent {
|
||||||
|
|
||||||
public error: string;
|
public error: string;
|
||||||
public bots: Bot[] = [];
|
public bots: Bot[] = [];
|
||||||
|
public loading = true;
|
||||||
|
public roomId: string;
|
||||||
|
|
||||||
constructor(private activatedRoute: ActivatedRoute, private api: ApiService) {
|
private scalarToken: string;
|
||||||
|
|
||||||
|
constructor(private activatedRoute: ActivatedRoute, private api: ApiService, private scalar: ScalarService, private toaster:ToasterService) {
|
||||||
let params: any = this.activatedRoute.snapshot.queryParams;
|
let params: any = this.activatedRoute.snapshot.queryParams;
|
||||||
if (!params.scalar_token || !params.room_id) this.error = "Missing scalar token or room ID";
|
if (!params.scalar_token || !params.room_id) this.error = "Missing scalar token or room ID";
|
||||||
else this.api.checkScalarToken(params.scalar_token).then(isValid => {
|
else {
|
||||||
|
this.roomId = params.room_id;
|
||||||
|
this.scalarToken = params.scalar_token;
|
||||||
|
|
||||||
|
this.api.checkScalarToken(params.scalar_token).then(isValid => {
|
||||||
if (isValid) this.init();
|
if (isValid) this.init();
|
||||||
else this.error = "Invalid scalar token";
|
else this.error = "Invalid scalar token";
|
||||||
|
}).catch(err => {
|
||||||
|
this.error = "Unable to communicate with Dimension";
|
||||||
|
console.error(err);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private init() {
|
private init() {
|
||||||
this.api.getBots().then(bots => {
|
this.api.getBots().then(bots => {
|
||||||
this.bots = bots;
|
this.bots = bots;
|
||||||
bots.map(b => b.isEnabled = Math.random() > 0.75);
|
let promises = bots.map(b => this.updateBotState(b));
|
||||||
|
return Promise.all(promises);
|
||||||
|
}).then(() => this.loading = false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateBotState(bot: Bot) {
|
||||||
|
return this.scalar.getMembershipState(this.roomId, bot.mxid).then(payload => {
|
||||||
|
bot.isBroken = false;
|
||||||
|
|
||||||
|
if (!payload.response) {
|
||||||
|
bot.isEnabled = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
bot.isEnabled = (payload.response.membership === 'join' || payload.response.membership === 'invite');
|
||||||
|
}, (error) => {
|
||||||
|
console.error(error);
|
||||||
|
bot.isEnabled = false;
|
||||||
|
bot.isBroken = true;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public updateBot(bot: Bot) {
|
||||||
|
let promise = null;
|
||||||
|
|
||||||
|
if (!bot.isEnabled) {
|
||||||
|
promise = this.api.kickUser(this.roomId, bot.mxid, this.scalarToken);
|
||||||
|
} else promise = this.scalar.inviteUser(this.roomId, bot.mxid);
|
||||||
|
|
||||||
|
promise
|
||||||
|
.then(() => this.toaster.pop("success", bot.name + " invited to the room"))
|
||||||
|
.catch(err => {
|
||||||
|
var errorMessage = "Could not update bot status";
|
||||||
|
|
||||||
|
if (err.json) {
|
||||||
|
errorMessage = err.json().error;
|
||||||
|
} else errorMessage = err.response.error.message;
|
||||||
|
|
||||||
|
bot.isEnabled = !bot.isEnabled;
|
||||||
|
this.toaster.pop("error", errorMessage);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,4 +16,9 @@ export class ApiService {
|
||||||
return this.http.get("/api/v1/dimension/bots")
|
return this.http.get("/api/v1/dimension/bots")
|
||||||
.map(res => res.json()).toPromise();
|
.map(res => res.json()).toPromise();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
kickUser(roomId: string, userId: string, scalarToken: string): Promise<any> {
|
||||||
|
return this.http.post("/api/v1/dimension/kick", {roomId: roomId, userId: userId, scalarToken: scalarToken})
|
||||||
|
.map(res => res.json()).toPromise();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
// Services
|
|
||||||
export * from './api.service';
|
|
||||||
|
|
||||||
// Models
|
|
||||||
export * from './models/bot';
|
|
|
@ -1,7 +1,8 @@
|
||||||
export class Bot {
|
export interface Bot {
|
||||||
mxid: string;
|
mxid: string;
|
||||||
name: string;
|
name: string;
|
||||||
avatar: string;
|
avatar: string;
|
||||||
about: string; // nullable
|
about: string; // nullable
|
||||||
isEnabled: boolean;
|
isEnabled: boolean;
|
||||||
|
isBroken: boolean;
|
||||||
}
|
}
|
||||||
|
|
27
web/app/shared/models/scalar_responses.ts
Normal file
27
web/app/shared/models/scalar_responses.ts
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
export interface ScalarResponse {
|
||||||
|
action: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ScalarRoomResponse extends ScalarResponse {
|
||||||
|
room_id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ScalarUserResponse extends ScalarRoomResponse {
|
||||||
|
user_id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ScalarErrorResponse extends ScalarResponse {
|
||||||
|
response: {error: {message: string, _error: Error}};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ScalarSuccessResponse extends ScalarResponse {
|
||||||
|
response: {success: boolean};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MembershipStateResponse extends ScalarUserResponse {
|
||||||
|
response: {
|
||||||
|
membership: string;
|
||||||
|
avatar_url: string;
|
||||||
|
displayname: string;
|
||||||
|
};
|
||||||
|
}
|
68
web/app/shared/scalar.service.ts
Normal file
68
web/app/shared/scalar.service.ts
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
import { Injectable } from "@angular/core";
|
||||||
|
import * as randomString from "random-string";
|
||||||
|
import { MembershipStateResponse, ScalarSuccessResponse } from "./models/scalar_responses";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ScalarService {
|
||||||
|
|
||||||
|
private static actionMap: {[key: string]: {resolve: (obj: any) => void, reject: (obj: any) => void}} = {};
|
||||||
|
|
||||||
|
public static getAndRemoveActionHandler(requestKey: string): {resolve: (obj: any) => void, reject: (obj: any) => void} {
|
||||||
|
let handler = ScalarService.actionMap[requestKey];
|
||||||
|
ScalarService.actionMap[requestKey] = null;
|
||||||
|
return handler;
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public inviteUser(roomId: string, userId): Promise<ScalarSuccessResponse> {
|
||||||
|
return this.callAction("invite", {
|
||||||
|
room_id: roomId,
|
||||||
|
user_id: userId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public getMembershipState(roomId: string, userId: string): Promise<MembershipStateResponse> {
|
||||||
|
return this.callAction("membership_state", {
|
||||||
|
room_id: roomId,
|
||||||
|
user_id: userId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private callAction(action, payload) {
|
||||||
|
let requestKey = randomString({length: 20});
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (!window.opener) {
|
||||||
|
// Mimic an error response from scalar
|
||||||
|
reject({response: {error: {message: "No window.opener", _error: new Error("No window.opener")}}});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ScalarService.actionMap[requestKey] = {
|
||||||
|
resolve: resolve,
|
||||||
|
reject: reject
|
||||||
|
};
|
||||||
|
|
||||||
|
let request = JSON.parse(JSON.stringify(payload));
|
||||||
|
request["request_id"] = requestKey;
|
||||||
|
request["action"] = action;
|
||||||
|
|
||||||
|
window.opener.postMessage(request, "*");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register the event listener here to ensure it gets created
|
||||||
|
window.addEventListener("message", event => {
|
||||||
|
if (!event.data) return;
|
||||||
|
|
||||||
|
let requestKey = event.data["request_id"];
|
||||||
|
if (!requestKey) return;
|
||||||
|
|
||||||
|
let action = ScalarService.getAndRemoveActionHandler(requestKey);
|
||||||
|
if (!action) return;
|
||||||
|
|
||||||
|
if (event.data.response && event.data.response.error) action.reject(event.data);
|
||||||
|
else action.resolve(event.data);
|
||||||
|
});
|
|
@ -29,3 +29,6 @@ if (document.readyState === 'complete') {
|
||||||
}
|
}
|
||||||
return hash;
|
return hash;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// HACK: Work around .opener not being available
|
||||||
|
if (!window.opener && window.parent) window.opener = window.parent;
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
// styles in src/style directory are applied to the whole page
|
// styles in src/style directory are applied to the whole page
|
||||||
|
@import '../../node_modules/angular2-toaster/toaster';
|
||||||
|
|
||||||
body {
|
body {
|
||||||
background: #ddd !important;
|
background: #ddd !important;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
|
Loading…
Reference in a new issue