diff --git a/.gitignore b/.gitignore index 273c5ca..07545ca 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,8 @@ config/development.yaml config/production.yaml db/*.db start.sh +config/integrations/*_development.yaml +config/integrations/*_production.yaml # Logs logs diff --git a/app.js b/app.js index 2aa07c8..89d6ab8 100644 --- a/app.js +++ b/app.js @@ -1,10 +1,18 @@ var log = require("./src/util/LogService"); var Dimension = require("./src/Dimension"); var DimensionStore = require("./src/storage/DimensionStore"); +var DemoBot = require("./src/matrix/DemoBot"); +var config = require("config"); log.info("app", "Bootstrapping Dimension..."); var db = new DimensionStore(); db.prepare().then(() => { var app = new Dimension(db); app.start(); + + if (config.get("demobot.enabled")) { + log.info("app", "Demo bot enabled - starting up"); + var bot = new DemoBot(config.get("demobot.homeserverUrl"), config.get("demobot.userId"), config.get("demobot.accessToken")); + bot.start(); + } }, err => log.error("app", err)).catch(err => log.error("app", err)); \ No newline at end of file diff --git a/config/default.yaml b/config/default.yaml index 60a115c..09969dc 100644 --- a/config/default.yaml +++ b/config/default.yaml @@ -1,48 +1,8 @@ -# Configuration for the bots this Dimension supports -bots: - # Giphy (from matrix.org) - - mxid: "@neb_giphy:matrix.org" - 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" - 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: - # The port to run the webserver on port: 8184 - - # 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 @@ -51,4 +11,15 @@ logging: fileLevel: verbose rotate: size: 52428800 # bytes, default is 50mb - count: 5 \ No newline at end of file + count: 5 + +# Demo bot configuration. Used purely to show how to configure a self-hosted bot in Dimension +demobot: + enabled: false + userId: "@dimension:t2bot.io" + homeserverUrl: "https://t2bot.io" + accessToken: "something" + +# Upstream configuration. This should almost never change. +upstreams: + vector: "https://scalar.vector.im/api" \ No newline at end of file diff --git a/config/integrations/dimension_demo_bot.yaml b/config/integrations/dimension_demo_bot.yaml new file mode 100644 index 0000000..f7798d0 --- /dev/null +++ b/config/integrations/dimension_demo_bot.yaml @@ -0,0 +1,9 @@ +type: "bot" +enabled: true +userId: "@dimension:t2bot.io" +name: "Demo Bot" +about: "A bot that has no functionality. This is just a demonstration on the config." +avatar: "/img/avatars/demobot.png" +hosted: + homeserverUrl: "https://t2bot.io" + accessToken: "your_matrix_access_token_here" diff --git a/config/integrations/giphy.yaml b/config/integrations/giphy.yaml new file mode 100644 index 0000000..25d1fb4 --- /dev/null +++ b/config/integrations/giphy.yaml @@ -0,0 +1,9 @@ +type: "bot" +enabled: true +userId: "@neb_giphy:matrix.org" +name: "Giphy" +about: "Use `!giphy query` to find an animated GIF on demand" +avatar: "/img/avatars/giphy.png" +upstream: + type: "vector" + id: "giphy" \ No newline at end of file diff --git a/config/integrations/google.yaml b/config/integrations/google.yaml new file mode 100644 index 0000000..fb37c3c --- /dev/null +++ b/config/integrations/google.yaml @@ -0,0 +1,9 @@ +type: "bot" +enabled: true +userId: "@_neb_google:matrix.org" +name: "Google" +about: "Use `!google image query` to find an image from Google" +avatar: "/img/avatars/google.png" +upstream: + type: "vector" + id: "google" \ No newline at end of file diff --git a/config/integrations/guggy.yaml b/config/integrations/guggy.yaml new file mode 100644 index 0000000..6ea0c43 --- /dev/null +++ b/config/integrations/guggy.yaml @@ -0,0 +1,9 @@ +type: "bot" +enabled: true +userId: "@_neb_guggy:matrix.org" +name: "Guggy" +about: "Use `!guggy sentence` to create an animated GIF from a sentence" +avatar: "/img/avatars/guggy.png" +upstream: + type: "vector" + id: "guggy" \ No newline at end of file diff --git a/config/integrations/imgur.yaml b/config/integrations/imgur.yaml new file mode 100644 index 0000000..a2c77f7 --- /dev/null +++ b/config/integrations/imgur.yaml @@ -0,0 +1,9 @@ +type: "bot" +enabled: true +userId: "@_neb_imgur:matrix.org" +name: "Imgur" +about: "Use `!imgur query` to find an image from Imgur" +avatar: "/img/avatars/imgur.png" +upstream: + type: "vector" + id: "imgur" \ No newline at end of file diff --git a/config/integrations/wikipedia.yaml b/config/integrations/wikipedia.yaml new file mode 100644 index 0000000..bbcd9da --- /dev/null +++ b/config/integrations/wikipedia.yaml @@ -0,0 +1,9 @@ +type: "bot" +enabled: true +userId: "@_neb_wikipedia:matrix.org" +name: "Wikipedia" +about: "Use `!wikipedia query` to find something from Wikipedia" +avatar: "/img/avatars/wikipedia.png" +upstream: + type: "vector" + id: "wikipedia" \ No newline at end of file diff --git a/package.json b/package.json index 0ac8502..901a7c8 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,8 @@ "db-migrate-sqlite3": "^0.2.1", "express": "^4.15.2", "js-yaml": "^3.8.2", + "lodash": "^4.17.4", + "matrix-js-sdk": "^0.7.8", "moment": "^2.18.1", "random-string": "^0.2.0", "request": "^2.81.0", diff --git a/src/Dimension.js b/src/Dimension.js index a43f8c8..e59745b 100644 --- a/src/Dimension.js +++ b/src/Dimension.js @@ -8,6 +8,10 @@ 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"); /** * Primary entry point for Dimension @@ -48,8 +52,8 @@ class Dimension { this._app.post("/api/v1/scalar/register", this._scalarRegister.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)); + this._app.get("/api/v1/dimension/integrations", this._getIntegrations.bind(this)); + this._app.post("/api/v1/dimension/removeIntegration", this._removeIntegration.bind(this)); } start() { @@ -57,8 +61,7 @@ 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} + _removeIntegration(req, res) { var roomId = req.body.roomId; var userId = req.body.userId; var scalarToken = req.body.scalarToken; @@ -68,27 +71,32 @@ class Dimension { return; } - var integrationName = null; - this._db.checkToken(scalarToken).then(() => { - for (var bot of config.bots) { - if (bot.mxid == userId) { - integrationName = bot.upstreamType; - break; - } - } + var integrationConfig = integrations.byUserId[userId]; + if (!integrationConfig) { + res.status(400).send({error: "Unknown integration"}); + return; + } - 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})); + 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})); } - _getBots(req, res) { + _getIntegrations(req, res) { res.setHeader("Content-Type", "application/json"); - res.send(JSON.stringify(config.bots)); + + var results = _.map(integrations.all, i => { + var integration = JSON.parse(JSON.stringify(i)); + integration.upstream = undefined; + integration.hosted = undefined; + return integration; + }); + + res.send(results); } _checkScalarToken(req, res) { diff --git a/src/integration/HostedIntegration.js b/src/integration/HostedIntegration.js new file mode 100644 index 0000000..a51d7c2 --- /dev/null +++ b/src/integration/HostedIntegration.js @@ -0,0 +1,35 @@ +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; \ No newline at end of file diff --git a/src/integration/StubbedIntegration.js b/src/integration/StubbedIntegration.js new file mode 100644 index 0000000..78c399e --- /dev/null +++ b/src/integration/StubbedIntegration.js @@ -0,0 +1,7 @@ +class StubbedIntegration { + leaveRoom(roomId) { + throw new Error("Not implemented"); + } +} + +module.exports = StubbedIntegration; \ No newline at end of file diff --git a/src/integration/UpstreamIntegration.js b/src/integration/UpstreamIntegration.js new file mode 100644 index 0000000..a5dfe1d --- /dev/null +++ b/src/integration/UpstreamIntegration.js @@ -0,0 +1,33 @@ +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; \ No newline at end of file diff --git a/src/integration/index.js b/src/integration/index.js new file mode 100644 index 0000000..fd38e35 --- /dev/null +++ b/src/integration/index.js @@ -0,0 +1,55 @@ +var config = require("config"); +var log = require("../util/LogService"); +var fs = require("fs"); +var path = require("path"); +var _ = require("lodash"); + +log.info("Integrations", "Discovering integrations"); + +var searchPath = path.join(process.cwd(), "config", "integrations"); +var files = _.filter(fs.readdirSync(searchPath), f => !fs.statSync(path.join(searchPath, f)).isDirectory() && f.endsWith(".yaml")); +var currentEnv = config.util.initParam("NODE_ENV", "development"); + +if (currentEnv !== "development" && currentEnv !== "production") + throw new Error("Unknown node environment: " + currentEnv); + +var configs = {}; + +for (var file of files) { + if (file.endsWith("_development.yaml") || file.endsWith("_production.yaml")) { + if (!file.endsWith("_" + currentEnv + ".yaml")) continue; + var fileName = file.replace("_development.yaml", "").replace("_production.yaml", "") + ".yaml"; + + if (!configs[fileName]) configs[fileName] = {}; + configs[fileName]["alt"] = config.util.parseFile(path.join(searchPath, file)); + } else { + if (!configs[file]) configs[file] = {}; + configs[file]["defaults"] = config.util.parseFile(path.join(searchPath, file)); + } +} + +var keys = _.keys(configs); +log.info("Integrations", "Discovered " + keys.length + " integrations. Parsing definitions..."); + +var linear = []; +var byUserId = {}; + +for (var key of keys) { + log.info("Integrations", "Preparing " + key); + var merged = config.util.extendDeep(configs[key].defaults, configs[key].alt); + if (!merged['enabled']) { + log.warn("Integrations", "Integration " + key + " is not enabled - skipping"); + continue; + } + + linear.push(merged); + if (merged['userId']) + byUserId[merged['userId']] = merged; +} + +log.info("Integrations", "Loaded " + linear.length + " integrations"); + +module.exports = { + all: linear, + byUserId: byUserId +}; \ No newline at end of file diff --git a/src/matrix/DemoBot.js b/src/matrix/DemoBot.js new file mode 100644 index 0000000..1b519a9 --- /dev/null +++ b/src/matrix/DemoBot.js @@ -0,0 +1,66 @@ +var sdk = require("matrix-js-sdk"); +var log = require("../util/LogService"); + +/** + * Dimension demo bot. Doesn't do anything except show how to add a self-hosted bot to Dimension + */ +class DemoBot { + + constructor(homeserverUrl, userId, accessToken) { + this._rooms = []; + + log.info("DemoBot", "Constructing bot as " + userId); + this._client = sdk.createClient({ + baseUrl: homeserverUrl, + accessToken: accessToken, + userId: userId, + }); + + this._client.on('event', event => { + if (event.getType() !== "m.room.member") return; + if (event.getStateKey() != this._client.credentials.userId) return; + if (event.getContent().membership === 'invite') { + if (this._rooms.indexOf(event.getRoomId()) !== -1) return; + log.info("DemoBot", "Joining " + event.getRoomId()); + this._client.joinRoom(event.getRoomId()).then(() => { + this._recalculateRooms(); + this._client.sendMessage(event.getRoomId(), { + msgtype: "m.notice", + body: "Hello! I'm a small bot that acts as an example for how to set up your own bot on Dimension." + }); + }); + } else this._recalculateRooms(); + }); + } + + start() { + log.info("DemoBot", "Starting bot"); + this._client.startClient(); + + this._client.on('sync', state => { + if (state == 'PREPARED') this._recalculateRooms(); + }); + } + + _recalculateRooms() { + var rooms = this._client.getRooms(); + this._rooms = []; + for (var room of rooms) { + var me = room.getMember(this._client.credentials.userId); + if (!me) continue; + + if (me.membership == "invite") { + this._client.joinRoom(room.roomId); + continue; + } + + if (me.membership != "join") continue; + this._rooms.push(room.roomId); + } + + log.verbose("DemoBot", "Currently in " + this._rooms.length + " rooms"); + } + +} + +module.exports = DemoBot; \ No newline at end of file diff --git a/src/scalar/ScalarClient.js b/src/scalar/ScalarClient.js index 9024980..9138db3 100644 --- a/src/scalar/ScalarClient.js +++ b/src/scalar/ScalarClient.js @@ -29,8 +29,10 @@ class ScalarClient { }); } + // TODO: Merge this, VectorScalarClient, and MatrixLiteClient into a base class _do(method, endpoint, qs = null, body = null) { - var url = config.scalar.upstreamRestUrl + endpoint; + // TODO: Generify URL + var url = config.get("upstreams.vector") + endpoint; log.verbose("ScalarClient", "Performing request: " + url); diff --git a/src/scalar/VectorScalarClient.js b/src/scalar/VectorScalarClient.js index 7e3d6db..5c1d329 100644 --- a/src/scalar/VectorScalarClient.js +++ b/src/scalar/VectorScalarClient.js @@ -33,7 +33,10 @@ class VectorScalarClient { * @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) => { + 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); @@ -44,7 +47,7 @@ class VectorScalarClient { } _do(method, endpoint, qs = null, body = null) { - var url = config.scalar.upstreamRestUrl + endpoint; + var url = config.get("upstreams.vector") + endpoint; log.verbose("VectorScalarClient", "Performing request: " + url); diff --git a/web/app/app.module.ts b/web/app/app.module.ts index 36d8af4..633126a 100644 --- a/web/app/app.module.ts +++ b/web/app/app.module.ts @@ -9,11 +9,11 @@ import { removeNgStyles, createNewHosts } from "@angularclass/hmr"; import { NgbModule } from "@ng-bootstrap/ng-bootstrap"; import { RiotComponent } from "./riot/riot.component"; import { ApiService } from "./shared/api.service"; -import { BotComponent } from "./bot/bot.component"; import { UiSwitchModule } from "angular2-ui-switch"; import { ScalarService } from "./shared/scalar.service"; import { ToasterModule } from "angular2-toaster"; import { BrowserAnimationsModule } from "@angular/platform-browser/animations"; +import { IntegrationComponent } from "./integration/integration.component"; @NgModule({ imports: [ @@ -30,7 +30,7 @@ import { BrowserAnimationsModule } from "@angular/platform-browser/animations"; AppComponent, HomeComponent, RiotComponent, - BotComponent, + IntegrationComponent, // Vendor ], diff --git a/web/app/bot/bot.component.ts b/web/app/bot/bot.component.ts deleted file mode 100644 index 8282446..0000000 --- a/web/app/bot/bot.component.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { Component, Input, Output, EventEmitter } from "@angular/core"; -import { Bot } from "../shared/models/bot"; - -@Component({ - selector: 'my-bot', - templateUrl: './bot.component.html', - styleUrls: ['./bot.component.scss'], -}) -export class BotComponent { - - @Input() bot: Bot; - @Output() updated: EventEmitter = new EventEmitter(); - - constructor() { - } - - public update(): void { - this.bot.isEnabled = !this.bot.isEnabled; - this.updated.emit(); - } -} diff --git a/web/app/bot/bot.component.html b/web/app/integration/integration.component.html similarity index 52% rename from web/app/bot/bot.component.html rename to web/app/integration/integration.component.html index 7ca933b..9508397 100644 --- a/web/app/bot/bot.component.html +++ b/web/app/integration/integration.component.html @@ -1,13 +1,13 @@
- +
- {{ bot.name }} + {{ integration.name }}
- +
- +
diff --git a/web/app/bot/bot.component.scss b/web/app/integration/integration.component.scss similarity index 100% rename from web/app/bot/bot.component.scss rename to web/app/integration/integration.component.scss diff --git a/web/app/integration/integration.component.ts b/web/app/integration/integration.component.ts new file mode 100644 index 0000000..efd6d6f --- /dev/null +++ b/web/app/integration/integration.component.ts @@ -0,0 +1,21 @@ +import { Component, Input, Output, EventEmitter } from "@angular/core"; +import { Integration } from "../shared/models/integration"; + +@Component({ + selector: 'my-integration', + templateUrl: './integration.component.html', + styleUrls: ['./integration.component.scss'], +}) +export class IntegrationComponent { + + @Input() integration: Integration; + @Output() updated: EventEmitter = new EventEmitter(); + + constructor() { + } + + public update(): void { + this.integration.isEnabled = !this.integration.isEnabled; + this.updated.emit(); + } +} diff --git a/web/app/riot/riot.component.html b/web/app/riot/riot.component.html index ea03095..79aa836 100644 --- a/web/app/riot/riot.component.html +++ b/web/app/riot/riot.component.html @@ -9,7 +9,7 @@

Manage Integrations

Turn on anything you like. If an integration has some special configuration, it will have a configuration icon next to it.

- +
\ No newline at end of file diff --git a/web/app/riot/riot.component.ts b/web/app/riot/riot.component.ts index e52a89f..5c8fa3d 100644 --- a/web/app/riot/riot.component.ts +++ b/web/app/riot/riot.component.ts @@ -1,9 +1,9 @@ import { Component } from "@angular/core"; import { ActivatedRoute } from "@angular/router"; import { ApiService } from "../shared/api.service"; -import { Bot } from "../shared/models/bot"; import { ScalarService } from "../shared/scalar.service"; import { ToasterService } from "angular2-toaster"; +import { Integration } from "../shared/models/integration"; @Component({ selector: 'my-riot', @@ -13,7 +13,7 @@ import { ToasterService } from "angular2-toaster"; export class RiotComponent { public error: string; - public bots: Bot[] = []; + public integrations: Integration[] = []; public loading = true; public roomId: string; @@ -40,47 +40,50 @@ export class RiotComponent { } private init() { - this.api.getBots().then(bots => { - this.bots = bots; - let promises = bots.map(b => this.updateBotState(b)); + this.api.getIntegrations().then(integrations => { + this.integrations = integrations; + let promises = integrations.map(b => this.updateIntegrationState(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; + }).then(() => this.loading = false).catch(err => { + this.error = "Unable to communicate with Dimension"; + console.error(err); }); } - public updateBot(bot: Bot) { + private updateIntegrationState(integration: Integration) { + return this.scalar.getMembershipState(this.roomId, integration.userId).then(payload => { + integration.isBroken = false; + + if (!payload.response) { + integration.isEnabled = false; + return; + } + + integration.isEnabled = (payload.response.membership === 'join' || payload.response.membership === 'invite'); + }, (error) => { + console.error(error); + integration.isEnabled = false; + integration.isBroken = true; + }); + } + + public updateIntegration(integration: Integration) { 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); + if (!integration.isEnabled) { + promise = this.api.removeIntegration(this.roomId, integration.userId, this.scalarToken); + } else promise = this.scalar.inviteUser(this.roomId, integration.userId); promise - .then(() => this.toaster.pop("success", bot.name + " invited to the room")) + .then(() => this.toaster.pop("success", integration.name + " invited to the room")) .catch(err => { - let errorMessage = "Could not update bot status"; + let errorMessage = "Could not update integration status"; if (err.json) { errorMessage = err.json().error; } else errorMessage = err.response.error.message; - bot.isEnabled = !bot.isEnabled; + integration.isEnabled = !integration.isEnabled; this.toaster.pop("error", errorMessage); }); } diff --git a/web/app/shared/api.service.ts b/web/app/shared/api.service.ts index 2f46484..9d82be4 100644 --- a/web/app/shared/api.service.ts +++ b/web/app/shared/api.service.ts @@ -1,6 +1,6 @@ import { Injectable } from "@angular/core"; import { Http } from "@angular/http"; -import { Bot } from "./models/bot"; +import { Integration } from "./models/integration"; @Injectable() export class ApiService { @@ -12,13 +12,13 @@ export class ApiService { .map(res => res.status === 200).toPromise(); } - getBots(): Promise { - return this.http.get("/api/v1/dimension/bots") + getIntegrations(): Promise { + return this.http.get("/api/v1/dimension/integrations") .map(res => res.json()).toPromise(); } - kickUser(roomId: string, userId: string, scalarToken: string): Promise { - return this.http.post("/api/v1/dimension/kick", {roomId: roomId, userId: userId, scalarToken: scalarToken}) + removeIntegration(roomId: string, userId: string, scalarToken: string): Promise { + return this.http.post("/api/v1/dimension/removeIntegration", {roomId: roomId, userId: userId, scalarToken: scalarToken}) .map(res => res.json()).toPromise(); } } diff --git a/web/app/shared/models/bot.ts b/web/app/shared/models/integration.ts similarity index 63% rename from web/app/shared/models/bot.ts rename to web/app/shared/models/integration.ts index ef38228..62e39fd 100644 --- a/web/app/shared/models/bot.ts +++ b/web/app/shared/models/integration.ts @@ -1,5 +1,6 @@ -export interface Bot { - mxid: string; +export interface Integration { + type: string; + userId: string; name: string; avatar: string; about: string; // nullable diff --git a/web/public/img/avatars/demobot.png b/web/public/img/avatars/demobot.png new file mode 100644 index 0000000..a45afca Binary files /dev/null and b/web/public/img/avatars/demobot.png differ