From 76931819af32a41f3b4e36d3547d4a9a0abbe49a Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 30 Mar 2018 19:22:15 -0600 Subject: [PATCH] Add the shell for configuring IRC bridges --- src/api/admin/AdminIntegrationsService.ts | 7 +- .../dimension/DimensionIntegrationsService.ts | 25 +++++++ src/bridges/IrcBridge.ts | 19 +++++ src/db/BridgeStore.ts | 66 ++++++++++++++++++ src/db/DimensionStore.ts | 2 + .../migrations/20180330172445-AddBridges.ts | 30 ++++++++ src/db/models/BridgeRecord.ts | 32 +++++++++ src/integrations/Bridge.ts | 32 +++++++++ src/temp_todo.txt | 1 - web/app/admin/admin.component.html | 1 + web/app/admin/bridges/bridges.component.html | 37 ++++++++++ web/app/admin/bridges/bridges.component.scss | 8 +++ web/app/admin/bridges/bridges.component.ts | 28 ++++++++ web/app/admin/bridges/irc/irc.component.html | 49 +++++++++++++ web/app/admin/bridges/irc/irc.component.scss | 0 web/app/admin/bridges/irc/irc.component.ts | 31 +++++++++ web/app/admin/widgets/widgets.component.ts | 7 +- web/app/app.module.ts | 6 ++ web/app/app.routing.ts | 26 +++++++ web/app/configs/bridge/bridge.component.ts | 69 +++++++++++++++++++ .../config-screen.bridge.component.html | 6 ++ .../config-screen.bridge.component.scss | 0 .../config-screen.bridge.component.ts | 16 +++++ web/app/riot/riot-home/home.component.ts | 2 + web/app/shared/models/dimension-responses.ts | 3 +- web/app/shared/models/integration.ts | 7 +- .../admin/admin-integrations-api.service.ts | 8 ++- 27 files changed, 509 insertions(+), 9 deletions(-) create mode 100644 src/bridges/IrcBridge.ts create mode 100644 src/db/BridgeStore.ts create mode 100644 src/db/migrations/20180330172445-AddBridges.ts create mode 100644 src/db/models/BridgeRecord.ts create mode 100644 src/integrations/Bridge.ts create mode 100644 web/app/admin/bridges/bridges.component.html create mode 100644 web/app/admin/bridges/bridges.component.scss create mode 100644 web/app/admin/bridges/bridges.component.ts create mode 100644 web/app/admin/bridges/irc/irc.component.html create mode 100644 web/app/admin/bridges/irc/irc.component.scss create mode 100644 web/app/admin/bridges/irc/irc.component.ts create mode 100644 web/app/configs/bridge/bridge.component.ts create mode 100644 web/app/configs/bridge/config-screen/config-screen.bridge.component.html create mode 100644 web/app/configs/bridge/config-screen/config-screen.bridge.component.scss create mode 100644 web/app/configs/bridge/config-screen/config-screen.bridge.component.ts diff --git a/src/api/admin/AdminIntegrationsService.ts b/src/api/admin/AdminIntegrationsService.ts index 9c9174d..e516ff0 100644 --- a/src/api/admin/AdminIntegrationsService.ts +++ b/src/api/admin/AdminIntegrationsService.ts @@ -6,6 +6,7 @@ import { WidgetStore } from "../../db/WidgetStore"; import { Cache, CACHE_INTEGRATIONS } from "../../MemoryCache"; import { Integration } from "../../integrations/Integration"; import { LogService } from "matrix-js-snippets"; +import { BridgeStore } from "../../db/BridgeStore"; interface SetEnabledRequest { enabled: boolean; @@ -42,6 +43,7 @@ export class AdminIntegrationsService { const userId = await AdminService.validateAndGetAdminTokenOwner(scalarToken); if (category === "widget") await WidgetStore.setEnabled(type, body.enabled); + else if (category === "bridge") await BridgeStore.setEnabled(type, body.enabled); else throw new ApiError(400, "Unrecognized category"); LogService.info("AdminIntegrationsService", userId + " set " + category + "/" + type + " to " + (body.enabled ? "enabled" : "disabled")); @@ -51,10 +53,11 @@ export class AdminIntegrationsService { @GET @Path(":category/all") - public async getAllIntegrations(@QueryParam("scalar_token") scalarToken: string, @QueryParam("category") category: string): Promise { - await AdminService.validateAndGetAdminTokenOwner(scalarToken); + public async getAllIntegrations(@QueryParam("scalar_token") scalarToken: string, @PathParam("category") category: string): Promise { + const userId = await AdminService.validateAndGetAdminTokenOwner(scalarToken); if (category === "widget") return await DimensionIntegrationsService.getWidgets(false); + else if (category === "bridge") return await DimensionIntegrationsService.getBridges(false, userId); else throw new ApiError(400, "Unrecongized category"); } } \ No newline at end of file diff --git a/src/api/dimension/DimensionIntegrationsService.ts b/src/api/dimension/DimensionIntegrationsService.ts index 92bb6d2..f651062 100644 --- a/src/api/dimension/DimensionIntegrationsService.ts +++ b/src/api/dimension/DimensionIntegrationsService.ts @@ -8,11 +8,14 @@ import { WidgetStore } from "../../db/WidgetStore"; import { SimpleBot } from "../../integrations/SimpleBot"; import { NebStore } from "../../db/NebStore"; import { ComplexBot } from "../../integrations/ComplexBot"; +import { Bridge } from "../../integrations/Bridge"; +import { BridgeStore } from "../../db/BridgeStore"; export interface IntegrationsResponse { widgets: Widget[], bots: SimpleBot[], complexBots: ComplexBot[], + bridges: Bridge[], } /** @@ -35,6 +38,24 @@ export class DimensionIntegrationsService { return widgets; } + /** + * Gets a list of bridges + * @param {boolean} enabledOnly True to only return the enabled bridges + * @param {string} forUserId The requesting user ID + * @param {string} inRoomId If specified, the room ID to list the bridges in + * @returns {Promise} Resolves to the bridge list + */ + public static async getBridges(enabledOnly: boolean, forUserId: string, inRoomId?: string): Promise { + const cacheKey = inRoomId ? "bridges_" + inRoomId : "bridges"; + + const cached = Cache.for(CACHE_INTEGRATIONS).get(cacheKey); + if (cached) return cached; + + const bridges = await BridgeStore.listAll(forUserId, enabledOnly ? true : null, inRoomId); + Cache.for(CACHE_INTEGRATIONS).put(cacheKey, bridges); + return bridges; + } + /** * Gets a list of simple bots * @param {string} userId The requesting user ID @@ -72,6 +93,7 @@ export class DimensionIntegrationsService { widgets: await DimensionIntegrationsService.getWidgets(true), bots: await DimensionIntegrationsService.getSimpleBots(userId), complexBots: await DimensionIntegrationsService.getComplexBots(userId, roomId), + bridges: await DimensionIntegrationsService.getBridges(true, userId, roomId), }; } @@ -83,6 +105,7 @@ export class DimensionIntegrationsService { if (category === "widget") return roomConfig.widgets.find(i => i.type === integrationType); else if (category === "bot") return roomConfig.bots.find(i => i.type === integrationType); else if (category === "complex-bot") return roomConfig.complexBots.find(i => i.type === integrationType); + else if (category === "bridge") return roomConfig.bridges.find(i => i.type === integrationType); else throw new ApiError(400, "Unrecognized category"); } @@ -92,6 +115,7 @@ export class DimensionIntegrationsService { const userId = await ScalarService.getTokenOwner(scalarToken); if (category === "complex-bot") await NebStore.setComplexBotConfig(userId, integrationType, roomId, newConfig); + else if (category === "bridge") await BridgeStore.setBridgeRoomConfig(userId, integrationType, roomId, newConfig); else throw new ApiError(400, "Unrecognized category"); Cache.for(CACHE_INTEGRATIONS).clear(); // TODO: Improve which cache we invalidate @@ -106,6 +130,7 @@ export class DimensionIntegrationsService { if (category === "widget") throw new ApiError(400, "Widgets should be removed client-side"); else if (category === "bot") await NebStore.removeSimpleBot(integrationType, roomId, userId); else if (category === "complex-bot") throw new ApiError(400, "Complex bots should be removed automatically"); + else if (category === "bridge") throw new ApiError(400, "Bridges should be removed automatically"); else throw new ApiError(400, "Unrecognized category"); Cache.for(CACHE_INTEGRATIONS).clear(); // TODO: Improve which cache we invalidate diff --git a/src/bridges/IrcBridge.ts b/src/bridges/IrcBridge.ts new file mode 100644 index 0000000..fa69a2d --- /dev/null +++ b/src/bridges/IrcBridge.ts @@ -0,0 +1,19 @@ +import BridgeRecord from "../db/models/BridgeRecord"; +import { IrcBridgeConfiguration } from "../integrations/Bridge"; + +export class IrcBridge { + constructor(private bridgeRecord: BridgeRecord) { + } + + public async hasNetworks(): Promise { + return !!this.bridgeRecord; + } + + public async getRoomConfiguration(requestingUserId: string, inRoomId: string): Promise { + return {requestingUserId, inRoomId}; + } + + public async setRoomConfiguration(requestingUserId: string, inRoomId: string, newConfig: IrcBridgeConfiguration): Promise { + return {requestingUserId, inRoomId, newConfig}; + } +} \ No newline at end of file diff --git a/src/db/BridgeStore.ts b/src/db/BridgeStore.ts new file mode 100644 index 0000000..ed3dff3 --- /dev/null +++ b/src/db/BridgeStore.ts @@ -0,0 +1,66 @@ +import { Bridge } from "../integrations/Bridge"; +import BridgeRecord from "./models/BridgeRecord"; +import { IrcBridge } from "../bridges/IrcBridge"; + +export class BridgeStore { + + public static async listAll(requestingUserId: string, isEnabled?: boolean, inRoomId?: string): Promise { + let conditions = {}; + if (isEnabled === true || isEnabled === false) conditions = {where: {isEnabled: isEnabled}}; + + const allRecords = await BridgeRecord.findAll(conditions); + const enabledBridges: Bridge[] = []; + + for (const bridgeRecord of allRecords) { + if (isEnabled === true || isEnabled === false) { + const isLogicallyEnabled = await BridgeStore.isLogicallyEnabled(bridgeRecord); + if (isLogicallyEnabled !== isEnabled) continue; + } + + const bridgeConfig = await BridgeStore.getConfiguration(bridgeRecord, requestingUserId, inRoomId); + enabledBridges.push(new Bridge(bridgeRecord, bridgeConfig)); + } + + return enabledBridges; + } + + public static async setEnabled(type: string, isEnabled: boolean): Promise { + const bridge = await BridgeRecord.findOne({where: {type: type}}); + if (!bridge) throw new Error("Bridge not found"); + + bridge.isEnabled = isEnabled; + return bridge.save(); + } + + public static async setBridgeRoomConfig(requestingUserId: string, integrationType: string, inRoomId: string, newConfig: any): Promise { + console.log(requestingUserId); + console.log(inRoomId); + console.log(newConfig); + const record = await BridgeRecord.findOne({where: {type: integrationType}}); + if (!record) throw new Error("Bridge not found"); + + if (integrationType === "irc") { + const irc = new IrcBridge(record); + return irc.setRoomConfiguration(requestingUserId, inRoomId, newConfig); + } else throw new Error("Unsupported bridge"); + } + + private static async isLogicallyEnabled(record: BridgeRecord): Promise { + if (record.type === "irc") { + const irc = new IrcBridge(record); + return irc.hasNetworks(); + } else return true; + } + + private static async getConfiguration(record: BridgeRecord, requestingUserId: string, inRoomId?: string): Promise { + if (record.type === "irc") { + if (!inRoomId) return {}; // The bridge's admin config is handled by other APIs + const irc = new IrcBridge(record); + return irc.getRoomConfiguration(requestingUserId, inRoomId); + } else return {}; + } + + private constructor() { + } + +} \ No newline at end of file diff --git a/src/db/DimensionStore.ts b/src/db/DimensionStore.ts index a8bf4d6..b6c7eae 100644 --- a/src/db/DimensionStore.ts +++ b/src/db/DimensionStore.ts @@ -15,6 +15,7 @@ import NebBotUser from "./models/NebBotUser"; import NebNotificationUser from "./models/NebNotificationUser"; import NebIntegrationConfig from "./models/NebIntegrationConfig"; import Webhook from "./models/Webhook"; +import BridgeRecord from "./models/BridgeRecord"; class _DimensionStore { private sequelize: Sequelize; @@ -41,6 +42,7 @@ class _DimensionStore { NebNotificationUser, NebIntegrationConfig, Webhook, + BridgeRecord, ]); } diff --git a/src/db/migrations/20180330172445-AddBridges.ts b/src/db/migrations/20180330172445-AddBridges.ts new file mode 100644 index 0000000..eb3ff97 --- /dev/null +++ b/src/db/migrations/20180330172445-AddBridges.ts @@ -0,0 +1,30 @@ +import { QueryInterface } from "sequelize"; +import { DataType } from "sequelize-typescript"; + +export default { + up: (queryInterface: QueryInterface) => { + return Promise.resolve() + .then(() => queryInterface.createTable("dimension_bridges", { + "id": {type: DataType.INTEGER, primaryKey: true, autoIncrement: true, allowNull: false}, + "type": {type: DataType.STRING, allowNull: false}, + "name": {type: DataType.STRING, allowNull: false}, + "avatarUrl": {type: DataType.STRING, allowNull: false}, + "description": {type: DataType.STRING, allowNull: false}, + "isEnabled": {type: DataType.BOOLEAN, allowNull: false}, + "isPublic": {type: DataType.BOOLEAN, allowNull: false}, + })) + .then(() => queryInterface.bulkInsert("dimension_bridges", [ + { + type: "irc", + name: "IRC Bridge", + avatarUrl: "/img/avatars/irc.png", + isEnabled: true, + isPublic: true, + description: "Bridges IRC channels to rooms, supporting multiple networks", + }, + ])); + }, + down: (queryInterface: QueryInterface) => { + return queryInterface.dropTable("dimension_bridges"); + } +} \ No newline at end of file diff --git a/src/db/models/BridgeRecord.ts b/src/db/models/BridgeRecord.ts new file mode 100644 index 0000000..facc328 --- /dev/null +++ b/src/db/models/BridgeRecord.ts @@ -0,0 +1,32 @@ +import { AutoIncrement, Column, Model, PrimaryKey, Table } from "sequelize-typescript"; +import { IntegrationRecord } from "./IntegrationRecord"; + +@Table({ + tableName: "dimension_bridges", + underscoredAll: false, + timestamps: false, +}) +export default class BridgeRecord extends Model implements IntegrationRecord { + @PrimaryKey + @AutoIncrement + @Column + id: number; + + @Column + type: string; + + @Column + name: string; + + @Column + avatarUrl: string; + + @Column + description: string; + + @Column + isEnabled: boolean; + + @Column + isPublic: boolean; +} \ No newline at end of file diff --git a/src/integrations/Bridge.ts b/src/integrations/Bridge.ts new file mode 100644 index 0000000..87aeb22 --- /dev/null +++ b/src/integrations/Bridge.ts @@ -0,0 +1,32 @@ +import { Integration } from "./Integration"; +import BridgeRecord from "../db/models/BridgeRecord"; + +export class Bridge extends Integration { + constructor(bridge: BridgeRecord, public config: any) { + super(bridge); + this.category = "bridge"; + this.requirements = [{ + condition: "publicRoom", + expectedValue: true, + argument: null, // not used + }]; + + // We'll just say we aren't + this.isEncryptionSupported = false; + } +} + +export interface IrcBridgeConfiguration { + availableNetworks: { + [networkId: string]: { + name: string; + bridgeUserId: string; + }; + }; + links: { + [networkId: string]: { + channelName: string; + addedByUserId: string; + }[]; + }; +} \ No newline at end of file diff --git a/src/temp_todo.txt b/src/temp_todo.txt index 42714b4..66e12e0 100644 --- a/src/temp_todo.txt +++ b/src/temp_todo.txt @@ -2,7 +2,6 @@ Release checklist: * IRC Bridge * Update documentation * Configuration migration (if possible) -* Lots of logging * Final testing (widgets, bots, etc) After release: diff --git a/web/app/admin/admin.component.html b/web/app/admin/admin.component.html index 000c411..ca82593 100644 --- a/web/app/admin/admin.component.html +++ b/web/app/admin/admin.component.html @@ -2,6 +2,7 @@
  • Dashboard
  • Widgets
  • go-neb
  • +
  • Bridges
  • {{ version }} diff --git a/web/app/admin/bridges/bridges.component.html b/web/app/admin/bridges/bridges.component.html new file mode 100644 index 0000000..af111eb --- /dev/null +++ b/web/app/admin/bridges/bridges.component.html @@ -0,0 +1,37 @@ +
    + +
    +
    + +
    +

    + Bridges provide a way for rooms to interact with and/or bring in events from a third party network. For + example, an IRC bridge can allow IRC and matrix users to communicate with each other. +

    + + + + + + + + + + + + + + + + + + + +
    NameDescriptionActions
    No bridges.
    {{ bridge.displayName }}{{ bridge.description }} + + + +
    +
    +
    +
    \ No newline at end of file diff --git a/web/app/admin/bridges/bridges.component.scss b/web/app/admin/bridges/bridges.component.scss new file mode 100644 index 0000000..847d207 --- /dev/null +++ b/web/app/admin/bridges/bridges.component.scss @@ -0,0 +1,8 @@ +tr td:last-child { + vertical-align: middle; +} + +.appsvcConfigButton, +.editButton { + cursor: pointer; +} \ No newline at end of file diff --git a/web/app/admin/bridges/bridges.component.ts b/web/app/admin/bridges/bridges.component.ts new file mode 100644 index 0000000..216eebc --- /dev/null +++ b/web/app/admin/bridges/bridges.component.ts @@ -0,0 +1,28 @@ +import { Component, OnInit } from "@angular/core"; +import { ToasterService } from "angular2-toaster"; +import { FE_Bridge } from "../../shared/models/integration"; +import { AdminIntegrationsApiService } from "../../shared/services/admin/admin-integrations-api.service"; + +@Component({ + templateUrl: "./bridges.component.html", + styleUrls: ["./bridges.component.scss"], +}) +export class AdminBridgesComponent implements OnInit { + + public isLoading = true; + public bridges: FE_Bridge[]; + + constructor(private adminIntegrations: AdminIntegrationsApiService, + private toaster: ToasterService) { + } + + public ngOnInit() { + this.adminIntegrations.getAllBridges().then(bridges => { + this.bridges = bridges.filter(b => b.isEnabled); + this.isLoading = false; + }).catch(err => { + console.error(err); + this.toaster.pop("error", "Failed to load bridges"); + }); + } +} diff --git a/web/app/admin/bridges/irc/irc.component.html b/web/app/admin/bridges/irc/irc.component.html new file mode 100644 index 0000000..a4b61ab --- /dev/null +++ b/web/app/admin/bridges/irc/irc.component.html @@ -0,0 +1,49 @@ +
    + +
    +
    + +
    +

    + matrix-appservice-irc + is an IRC bridge that supports multiple IRC networks. Dimension is capable of using multiple IRC + bridges to better distribute the load across multiple networks in large deployments. +

    + + + + + + + + + + + + + + + + + + + +
    NameEnabled NetworksActions
    No bridge configurations.
    + {{ bridge.upstreamId ? "matrix.org's bridge" : "Self-hosted bridge" }} + ({{ bridge.adminUrl }}) + + {{ getEnabledNetworksString(bridge) }} + + +
    + + +
    +
    +
    \ No newline at end of file diff --git a/web/app/admin/bridges/irc/irc.component.scss b/web/app/admin/bridges/irc/irc.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/web/app/admin/bridges/irc/irc.component.ts b/web/app/admin/bridges/irc/irc.component.ts new file mode 100644 index 0000000..a2df478 --- /dev/null +++ b/web/app/admin/bridges/irc/irc.component.ts @@ -0,0 +1,31 @@ +import { Component } from "@angular/core"; + +@Component({ + templateUrl: "./irc.component.html", + styleUrls: ["./irc.component.scss"], +}) +export class AdminIrcBridgeComponent { + + public isLoading = true; + public hasModularBridge = false; + public configurations: any[] = []; + + constructor() { + } + + public getEnabledNetworksString(bridge: any): string { + return "TODO: " + bridge; + } + + public addModularHostedBridge() { + + } + + public addSelfHostedBridge() { + + } + + public editNetworks(bridge: any) { + console.log(bridge); + } +} diff --git a/web/app/admin/widgets/widgets.component.ts b/web/app/admin/widgets/widgets.component.ts index 3a98c00..5debd8e 100644 --- a/web/app/admin/widgets/widgets.component.ts +++ b/web/app/admin/widgets/widgets.component.ts @@ -22,9 +22,12 @@ export class AdminWidgetsComponent { public widgets: FE_Widget[]; constructor(private adminIntegrationsApi: AdminIntegrationsApiService, private toaster: ToasterService, private modal: Modal) { - this.adminIntegrationsApi.getAllWidgets().then(integrations => { + this.adminIntegrationsApi.getAllWidgets().then(widgets => { this.isLoading = false; - this.widgets = integrations.widgets; + this.widgets = widgets; + }).catch(err => { + console.error(err); + this.toaster.pop("error", "Failed to load widgets"); }); } diff --git a/web/app/app.module.ts b/web/app/app.module.ts index 128633e..4772a88 100644 --- a/web/app/app.module.ts +++ b/web/app/app.module.ts @@ -62,6 +62,9 @@ import { ConfigSimpleBotComponent } from "./configs/simple-bot/simple-bot.compon import { ConfigScreenComplexBotComponent } from "./configs/complex-bot/config-screen/config-screen.complex-bot.component"; import { RssComplexBotConfigComponent } from "./configs/complex-bot/rss/rss.complex-bot.component"; import { TravisCiComplexBotConfigComponent } from "./configs/complex-bot/travisci/travisci.complex-bot.component"; +import { ConfigScreenBridgeComponent } from "./configs/bridge/config-screen/config-screen.bridge.component"; +import { AdminBridgesComponent } from "./admin/bridges/bridges.component"; +import { AdminIrcBridgeComponent } from "./admin/bridges/irc/irc.component"; @NgModule({ imports: [ @@ -118,6 +121,9 @@ import { TravisCiComplexBotConfigComponent } from "./configs/complex-bot/travisc ConfigScreenComplexBotComponent, RssComplexBotConfigComponent, TravisCiComplexBotConfigComponent, + ConfigScreenBridgeComponent, + AdminBridgesComponent, + AdminIrcBridgeComponent, // Vendor ], diff --git a/web/app/app.routing.ts b/web/app/app.routing.ts index a47411d..f7fca46 100644 --- a/web/app/app.routing.ts +++ b/web/app/app.routing.ts @@ -21,6 +21,8 @@ import { AdminEditNebComponent } from "./admin/neb/edit/edit.component"; import { AdminAddSelfhostedNebComponent } from "./admin/neb/add-selfhosted/add-selfhosted.component"; import { RssComplexBotConfigComponent } from "./configs/complex-bot/rss/rss.complex-bot.component"; import { TravisCiComplexBotConfigComponent } from "./configs/complex-bot/travisci/travisci.complex-bot.component"; +import { AdminBridgesComponent } from "./admin/bridges/bridges.component"; +import { AdminIrcBridgeComponent } from "./admin/bridges/irc/irc.component"; const routes: Routes = [ {path: "", component: HomeComponent}, @@ -68,6 +70,21 @@ const routes: Routes = [ }, ] }, + { + path: "bridges", + data: {breadcrumb: "Bridges", name: "Bridges"}, + children: [ + { + path: "", + component: AdminBridgesComponent, + }, + { + path: "irc", + component: AdminIrcBridgeComponent, + data: {breadcrumb: "IRC Bridge", name: "IRC Bridge"}, + }, + ], + }, ], }, { @@ -125,6 +142,15 @@ const routes: Routes = [ }, ], }, + // { + // path: "bridge", + // children: [ + // { + // path: "irc", + // + // } + // ] + // } ], }, { diff --git a/web/app/configs/bridge/bridge.component.ts b/web/app/configs/bridge/bridge.component.ts new file mode 100644 index 0000000..b5c100c --- /dev/null +++ b/web/app/configs/bridge/bridge.component.ts @@ -0,0 +1,69 @@ +import { OnDestroy, OnInit } from "@angular/core"; +import { FE_Bridge } from "../../shared/models/integration"; +import { ActivatedRoute } from "@angular/router"; +import { Subscription } from "rxjs/Subscription"; +import { IntegrationsApiService } from "../../shared/services/integrations/integrations-api.service"; +import { ToasterService } from "angular2-toaster"; +import { ServiceLocator } from "../../shared/registry/locator.service"; +import { ScalarClientApiService } from "../../shared/services/scalar/scalar-client-api.service"; + +export class BridgeComponent implements OnInit, OnDestroy { + + public isLoading = true; + public isUpdating = false; + public bridge: FE_Bridge; + public newConfig: T; + public roomId: string; + + private routeQuerySubscription: Subscription; + + protected toaster = ServiceLocator.injector.get(ToasterService); + protected integrationsApi = ServiceLocator.injector.get(IntegrationsApiService); + protected route = ServiceLocator.injector.get(ActivatedRoute); + protected scalarClientApi = ServiceLocator.injector.get(ScalarClientApiService); + + constructor(private integrationType: string) { + this.isLoading = true; + this.isUpdating = false; + } + + public ngOnInit(): void { + this.routeQuerySubscription = this.route.queryParams.subscribe(params => { + this.roomId = params['roomId']; + this.loadBridge(); + }); + } + + public ngOnDestroy(): void { + if (this.routeQuerySubscription) this.routeQuerySubscription.unsubscribe(); + } + + private loadBridge() { + this.isLoading = true; + this.isUpdating = false; + + this.newConfig = {}; + + this.integrationsApi.getIntegrationInRoom("bridge", this.integrationType, this.roomId).then(i => { + this.bridge = >i; + this.newConfig = JSON.parse(JSON.stringify(this.bridge.config)); + this.isLoading = false; + }).catch(err => { + console.error(err); + this.toaster.pop("error", "Failed to load configuration"); + }); + } + + public save(): void { + this.isUpdating = true; + this.integrationsApi.setIntegrationConfiguration("bridge", this.integrationType, this.roomId, this.newConfig).then(() => { + this.toaster.pop("success", "Configuration updated"); + this.bridge.config = this.newConfig; + this.isUpdating = false; + }).catch(err => { + console.error(err); + this.toaster.pop("error", "Error updating configuration"); + this.isUpdating = false; + }); + } +} \ No newline at end of file diff --git a/web/app/configs/bridge/config-screen/config-screen.bridge.component.html b/web/app/configs/bridge/config-screen/config-screen.bridge.component.html new file mode 100644 index 0000000..89aeb0e --- /dev/null +++ b/web/app/configs/bridge/config-screen/config-screen.bridge.component.html @@ -0,0 +1,6 @@ +
    + +
    +
    + +
    \ No newline at end of file diff --git a/web/app/configs/bridge/config-screen/config-screen.bridge.component.scss b/web/app/configs/bridge/config-screen/config-screen.bridge.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/web/app/configs/bridge/config-screen/config-screen.bridge.component.ts b/web/app/configs/bridge/config-screen/config-screen.bridge.component.ts new file mode 100644 index 0000000..01944b5 --- /dev/null +++ b/web/app/configs/bridge/config-screen/config-screen.bridge.component.ts @@ -0,0 +1,16 @@ +import { Component, ContentChild, Input, TemplateRef } from "@angular/core"; +import { BridgeComponent } from "../bridge.component"; + +@Component({ + selector: "my-bridge-config", + templateUrl: "config-screen.bridge.component.html", + styleUrls: ["config-screen.bridge.component.scss"], +}) +export class ConfigScreenBridgeComponent { + + @Input() bridgeComponent: BridgeComponent; + @ContentChild(TemplateRef) bridgeParamsTemplate: TemplateRef; + + constructor() { + } +} \ No newline at end of file diff --git a/web/app/riot/riot-home/home.component.ts b/web/app/riot/riot-home/home.component.ts index 50e9b8c..3cdf886 100644 --- a/web/app/riot/riot-home/home.component.ts +++ b/web/app/riot/riot-home/home.component.ts @@ -259,6 +259,8 @@ export class RiotHomeComponent { console.error(err); if (requirement.expectedValue) return Promise.reject("Expected to be able to send specific event types"); }); + case "userInRoom": + // TODO: Implement default: return Promise.reject("Requirement '" + requirement.condition + "' not found"); } diff --git a/web/app/shared/models/dimension-responses.ts b/web/app/shared/models/dimension-responses.ts index 23c5f1a..a8472f7 100644 --- a/web/app/shared/models/dimension-responses.ts +++ b/web/app/shared/models/dimension-responses.ts @@ -1,5 +1,6 @@ -import { FE_Widget } from "./integration"; +import { FE_Bridge, FE_Widget } from "./integration"; export interface FE_IntegrationsResponse { widgets: FE_Widget[]; + bridges: FE_Bridge[]; } \ No newline at end of file diff --git a/web/app/shared/models/integration.ts b/web/app/shared/models/integration.ts index 0576099..6b064bb 100644 --- a/web/app/shared/models/integration.ts +++ b/web/app/shared/models/integration.ts @@ -26,6 +26,11 @@ export interface FE_ComplexBot extends FE_Integration { config: T; } +export interface FE_Bridge extends FE_Integration { + bridgeUserId: string; + config: T; +} + export interface FE_Widget extends FE_Integration { options: any; } @@ -44,7 +49,7 @@ export interface FE_JitsiWidget extends FE_Widget { } export interface FE_IntegrationRequirement { - condition: "publicRoom" | "canSendEventTypes"; + condition: "publicRoom" | "canSendEventTypes" | "userInRoom"; argument: any; expectedValue: any; } \ No newline at end of file diff --git a/web/app/shared/services/admin/admin-integrations-api.service.ts b/web/app/shared/services/admin/admin-integrations-api.service.ts index 633a368..2a6ce67 100644 --- a/web/app/shared/services/admin/admin-integrations-api.service.ts +++ b/web/app/shared/services/admin/admin-integrations-api.service.ts @@ -1,7 +1,7 @@ import { Injectable } from "@angular/core"; import { Http } from "@angular/http"; import { AuthedApi } from "../authed-api"; -import { FE_IntegrationsResponse } from "../../models/dimension-responses"; +import { FE_Bridge, FE_Widget } from "../../models/integration"; @Injectable() export class AdminIntegrationsApiService extends AuthedApi { @@ -9,10 +9,14 @@ export class AdminIntegrationsApiService extends AuthedApi { super(http); } - public getAllWidgets(): Promise { + public getAllWidgets(): Promise { return this.authedGet("/api/v1/dimension/admin/integrations/widget/all").map(r => r.json()).toPromise(); } + public getAllBridges(): Promise[]> { + return this.authedGet("/api/v1/dimension/admin/integrations/bridge/all").map(r => r.json()).toPromise(); + } + public toggleIntegration(category: string, type: string, enabled: boolean): Promise { return this.authedPost("/api/v1/dimension/admin/integrations/" + category + "/" + type + "/enabled", {enabled: enabled}).map(r => r.json()).toPromise(); }