From 61ca805b193f662f3fbe4f186491c6ae533afe5b Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 30 Mar 2018 23:12:31 -0600 Subject: [PATCH] Initial admin APIs for managing IRC bridges Missing functionality: * Toggle networks * Add self-hosted --- src/MemoryCache.ts | 3 +- src/api/admin/AdminIrcService.ts | 102 +++++++++ src/bridges/IrcBridge.ts | 197 +++++++++++++++++- src/bridges/models/provision_responses.ts | 10 + src/db/BridgeStore.ts | 13 +- src/db/DimensionStore.ts | 4 + .../migrations/20180330211845-AddIrcBridge.ts | 36 ++++ src/db/models/IrcBridgeNetwork.ts | 34 +++ src/db/models/IrcBridgeRecord.ts | 26 +++ src/integrations/Bridge.ts | 8 +- src/models/ModularResponses.ts | 9 + web/app/admin/bridges/irc/irc.component.html | 4 +- web/app/admin/bridges/irc/irc.component.scss | 3 + web/app/admin/bridges/irc/irc.component.ts | 78 ++++++- web/app/admin/neb/neb.component.ts | 2 +- web/app/app.module.ts | 2 + web/app/shared/models/irc.ts | 16 ++ .../services/admin/admin-irc-api.service.ts | 28 +++ 18 files changed, 546 insertions(+), 29 deletions(-) create mode 100644 src/api/admin/AdminIrcService.ts create mode 100644 src/bridges/models/provision_responses.ts create mode 100644 src/db/migrations/20180330211845-AddIrcBridge.ts create mode 100644 src/db/models/IrcBridgeNetwork.ts create mode 100644 src/db/models/IrcBridgeRecord.ts create mode 100644 web/app/shared/models/irc.ts create mode 100644 web/app/shared/services/admin/admin-irc-api.service.ts diff --git a/src/MemoryCache.ts b/src/MemoryCache.ts index a0b81f7..e66881a 100644 --- a/src/MemoryCache.ts +++ b/src/MemoryCache.ts @@ -46,4 +46,5 @@ export const CACHE_NEB = "neb"; export const CACHE_UPSTREAM = "upstream"; export const CACHE_SCALAR_ACCOUNTS = "scalar-accounts"; export const CACHE_WIDGET_TITLES = "widget-titles"; -export const CACHE_FEDERATION = "federation"; \ No newline at end of file +export const CACHE_FEDERATION = "federation"; +export const CACHE_IRC_BRIDGE = "irc-bridge"; \ No newline at end of file diff --git a/src/api/admin/AdminIrcService.ts b/src/api/admin/AdminIrcService.ts new file mode 100644 index 0000000..2e17cca --- /dev/null +++ b/src/api/admin/AdminIrcService.ts @@ -0,0 +1,102 @@ +import { GET, Path, PathParam, POST, QueryParam } from "typescript-rest"; +import { AdminService } from "./AdminService"; +import { Cache, CACHE_INTEGRATIONS, CACHE_IRC_BRIDGE } from "../../MemoryCache"; +import { LogService } from "matrix-js-snippets"; +import { ApiError } from "../ApiError"; +import IrcBridgeRecord from "../../db/models/IrcBridgeRecord"; +import { AvailableNetworks, IrcBridge } from "../../bridges/IrcBridge"; +import Upstream from "../../db/models/Upstream"; + +interface CreateWithUpstream { + upstreamId: number; +} + +interface CreateSelfhosted { + provisionUrl: string; +} + +interface BridgeResponse { + id: number; + upstreamId?: number; + provisionUrl?: string; + isEnabled: boolean; + availableNetworks: AvailableNetworks; +} + +/** + * Administrative API for configuring IRC bridge instances. + */ +@Path("/api/v1/dimension/admin/irc") +export class AdminIrcService { + + @GET + @Path("all") + public async getBridges(@QueryParam("scalar_token") scalarToken: string): Promise { + const userId = await AdminService.validateAndGetAdminTokenOwner(scalarToken); + + const bridges = await IrcBridgeRecord.findAll(); + const client = new IrcBridge(userId); + return Promise.all(bridges.map(async b => { + return { + id: b.id, + upstreamId: b.upstreamId, + provisionUrl: b.provisionUrl, + isEnabled: b.isEnabled, + availableNetworks: await client.getNetworks(b), + }; + })); + } + + @GET + @Path(":bridgeId") + public async getBridge(@QueryParam("scalar_token") scalarToken: string, @PathParam("bridgeId") bridgeId: number): Promise { + const userId = await AdminService.validateAndGetAdminTokenOwner(scalarToken); + + const ircBridge = await IrcBridgeRecord.findByPrimary(bridgeId); + if (!ircBridge) throw new ApiError(404, "IRC Bridge not found"); + + const client = new IrcBridge(userId); + return { + id: ircBridge.id, + upstreamId: ircBridge.upstreamId, + provisionUrl: ircBridge.provisionUrl, + isEnabled: ircBridge.isEnabled, + availableNetworks: await client.getNetworks(ircBridge), + }; + } + + @POST + @Path("new/upstream") + public async newConfigForUpstream(@QueryParam("scalar_token") scalarToken: string, request: CreateWithUpstream): Promise { + const userId = await AdminService.validateAndGetAdminTokenOwner(scalarToken); + + const upstream = await Upstream.findByPrimary(request.upstreamId); + if (!upstream) throw new ApiError(400, "Upstream not found"); + + const bridge = await IrcBridgeRecord.create({ + upstreamId: request.upstreamId, + isEnabled: true, + }); + LogService.info("AdminIrcService", userId + " created a new IRC Bridge from upstream " + request.upstreamId); + + Cache.for(CACHE_IRC_BRIDGE).clear(); + Cache.for(CACHE_INTEGRATIONS).clear(); + return this.getBridge(scalarToken, bridge.id); + } + + @POST + @Path("new/selfhosted") + public async newSelfhosted(@QueryParam("scalar_token") scalarToken: string, request: CreateSelfhosted): Promise { + const userId = await AdminService.validateAndGetAdminTokenOwner(scalarToken); + + const bridge = await IrcBridgeRecord.create({ + provisionUrl: request.provisionUrl, + isEnabled: true, + }); + LogService.info("AdminIrcService", userId + " created a new IRC Bridge with provisioning URL " + request.provisionUrl); + + Cache.for(CACHE_IRC_BRIDGE).clear(); + Cache.for(CACHE_INTEGRATIONS).clear(); + return this.getBridge(scalarToken, bridge.id); + } +} \ No newline at end of file diff --git a/src/bridges/IrcBridge.ts b/src/bridges/IrcBridge.ts index fa69a2d..700c839 100644 --- a/src/bridges/IrcBridge.ts +++ b/src/bridges/IrcBridge.ts @@ -1,12 +1,64 @@ -import BridgeRecord from "../db/models/BridgeRecord"; import { IrcBridgeConfiguration } from "../integrations/Bridge"; +import { Cache, CACHE_IRC_BRIDGE } from "../MemoryCache"; +import IrcBridgeRecord from "../db/models/IrcBridgeRecord"; +import Upstream from "../db/models/Upstream"; +import UserScalarToken from "../db/models/UserScalarToken"; +import { LogService } from "matrix-js-snippets"; +import * as request from "request"; +import { QueryNetworksResponse } from "./models/provision_responses"; +import { ModularIrcQueryNetworksResponse } from "../models/ModularResponses"; +import IrcBridgeNetwork from "../db/models/IrcBridgeNetwork"; + +interface CachedNetwork { + ircBridgeId: number; + bridgeNetworkId: string; + bridgeUserId: string; + displayName: string; + domain: string; + isEnabled: boolean; +} + +export interface AvailableNetworks { + [networkId: string]: { + name: string; + domain: string; + bridgeUserId: string; + isEnabled: boolean; + }; +} export class IrcBridge { - constructor(private bridgeRecord: BridgeRecord) { + private static getNetworkId(network: CachedNetwork): string { + return network.ircBridgeId + "-" + network.bridgeNetworkId; + } + + public static parseNetworkId(networkId: string): { bridgeId: string, bridgeNetworkId: string } { + const parts = networkId.split("-"); + const bridgeId = parts.splice(1, 1)[0]; + const bridgeNetworkId = parts.join("-"); + return {bridgeId, bridgeNetworkId}; + } + + constructor(private requestingUserId: string) { } public async hasNetworks(): Promise { - return !!this.bridgeRecord; + const allNetworks = (await this.getAllNetworks()).filter(n => n.isEnabled); + return allNetworks.length > 0; + } + + public async getNetworks(bridge?: IrcBridgeRecord): Promise { + let networks = await this.getAllNetworks(); + if (bridge) networks = networks.filter(n => n.ircBridgeId === bridge.id); + + const available: AvailableNetworks = {}; + networks.forEach(n => available[IrcBridge.getNetworkId(n)] = { + name: n.displayName, + domain: n.domain, + bridgeUserId: n.bridgeUserId, + isEnabled: n.isEnabled, + }); + return available; } public async getRoomConfiguration(requestingUserId: string, inRoomId: string): Promise { @@ -16,4 +68,143 @@ export class IrcBridge { public async setRoomConfiguration(requestingUserId: string, inRoomId: string, newConfig: IrcBridgeConfiguration): Promise { return {requestingUserId, inRoomId, newConfig}; } + + private async getAllNetworks(): Promise { + const cached = Cache.for(CACHE_IRC_BRIDGE).get("networks"); + if (cached) return cached; + + const bridges = await IrcBridgeRecord.findAll(); + if (!bridges) return []; + + const networks: CachedNetwork[] = []; + for (const bridge of bridges) { + const bridgeNetworks = await this.fetchNetworks(bridge); + bridgeNetworks.forEach(n => networks.push(n)); + } + + Cache.for(CACHE_IRC_BRIDGE).put("networks", networks, 60 * 60 * 1000); // 1 hour + return networks; + } + + private async fetchNetworks(bridge: IrcBridgeRecord): Promise { + let responses: QueryNetworksResponse[] = []; + if (bridge.upstreamId) { + const result = await this.doUpstreamRequest(bridge, "GET", "/bridges/irc/_matrix/provision/querynetworks"); + if (result && result.replies) responses = result.replies.map(r => r.response); + } else { + const result = await this.doProvisionRequest(bridge, "GET", "/_matrix/provision/querynetworks"); + if (result) responses = [result]; + } + + const networks: CachedNetwork[] = []; + for (const response of responses) { + if (!response || !response.servers) continue; + + for (const server of response.servers) { + if (!server) continue; + + let existingNetwork = await IrcBridgeNetwork.findOne({ + where: { + bridgeId: bridge.id, + bridgeNetworkId: server.network_id, + }, + }); + if (!existingNetwork) { + LogService.info("IrcBridge", "Discovered new network for bridge " + bridge.id + ": " + server.network_id); + existingNetwork = await IrcBridgeNetwork.create({ + bridgeId: bridge.id, + isEnabled: false, + bridgeNetworkId: server.network_id, + bridgeUserId: server.bot_user_id, + displayName: server.desc, + domain: server.fields.domain, + }); + } else { + existingNetwork.displayName = server.desc; + existingNetwork.bridgeUserId = server.bot_user_id; + existingNetwork.domain = server.fields.domain; + await existingNetwork.save(); + } + + networks.push({ + ircBridgeId: bridge.id, + bridgeNetworkId: existingNetwork.bridgeNetworkId, + bridgeUserId: existingNetwork.bridgeUserId, + displayName: existingNetwork.displayName, + domain: existingNetwork.domain, + isEnabled: existingNetwork.isEnabled, + }); + } + } + + return networks; + } + + private async doUpstreamRequest(bridge: IrcBridgeRecord, method: string, endpoint: string, qs?: any, body?: any): Promise { + const upstream = await Upstream.findByPrimary(bridge.upstreamId); + const token = await UserScalarToken.findOne({ + where: { + upstreamId: upstream.id, + isDimensionToken: false, + userId: this.requestingUserId, + }, + }); + + if (!qs) qs = {}; + qs["scalar_token"] = token.scalarToken; + + const apiUrl = upstream.apiUrl.endsWith("/") ? upstream.apiUrl.substring(0, upstream.apiUrl.length - 1) : upstream.apiUrl; + const url = apiUrl + (endpoint.startsWith("/") ? endpoint : "/" + endpoint); + LogService.info("IrcBridge", "Doing upstream IRC Bridge request: " + url); + + return new Promise((resolve, reject) => { + request({ + method: method, + url: url, + qs: qs, + json: body, + }, (err, res, _body) => { + if (err) { + LogService.error("IrcBridge", "Error calling" + url); + LogService.error("IrcBridge", err); + reject(err); + } else if (res.statusCode !== 200) { + LogService.error("IrcBridge", "Got status code " + res.statusCode + " when calling " + url); + LogService.error("IrcBridge", res.body); + reject(new Error("Request failed")); + } else { + if (typeof(res.body) === "string") res.body = JSON.parse(res.body); + resolve(res.body); + } + }); + }); + } + + private async doProvisionRequest(bridge: IrcBridgeRecord, method: string, endpoint: string, qs?: any, body?: any): Promise { + const provisionUrl = bridge.provisionUrl; + const apiUrl = provisionUrl.endsWith("/") ? provisionUrl.substring(0, provisionUrl.length - 1) : provisionUrl; + const url = apiUrl + (endpoint.startsWith("/") ? endpoint : "/" + endpoint); + LogService.info("IrcBridge", "Doing provision IRC Bridge request: " + url); + + return new Promise((resolve, reject) => { + request({ + method: method, + url: url, + qs: qs, + json: body, + }, (err, res, _body) => { + if (err) { + LogService.error("IrcBridge", "Error calling" + url); + LogService.error("IrcBridge", err); + reject(err); + LogService.error("IrcBridge", "Got status code " + res.statusCode + " when calling " + url); + LogService.error("IrcBridge", res.body); + reject(new Error("Request failed")); + } else { + if (typeof(res.body) === "string") res.body = JSON.parse(res.body); + resolve(res.body); + } + }); + }); + } } \ No newline at end of file diff --git a/src/bridges/models/provision_responses.ts b/src/bridges/models/provision_responses.ts new file mode 100644 index 0000000..0c92c9a --- /dev/null +++ b/src/bridges/models/provision_responses.ts @@ -0,0 +1,10 @@ +export interface QueryNetworksResponse { + servers: { + network_id: string; + bot_user_id: string; + desc: string; + fields: { + domain: string; + }; + }[]; +} \ No newline at end of file diff --git a/src/db/BridgeStore.ts b/src/db/BridgeStore.ts index ed3dff3..99faa01 100644 --- a/src/db/BridgeStore.ts +++ b/src/db/BridgeStore.ts @@ -13,7 +13,7 @@ export class BridgeStore { for (const bridgeRecord of allRecords) { if (isEnabled === true || isEnabled === false) { - const isLogicallyEnabled = await BridgeStore.isLogicallyEnabled(bridgeRecord); + const isLogicallyEnabled = await BridgeStore.isLogicallyEnabled(bridgeRecord, requestingUserId); if (isLogicallyEnabled !== isEnabled) continue; } @@ -33,21 +33,18 @@ export class BridgeStore { } 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); + const irc = new IrcBridge(requestingUserId); return irc.setRoomConfiguration(requestingUserId, inRoomId, newConfig); } else throw new Error("Unsupported bridge"); } - private static async isLogicallyEnabled(record: BridgeRecord): Promise { + private static async isLogicallyEnabled(record: BridgeRecord, requestingUserId: string): Promise { if (record.type === "irc") { - const irc = new IrcBridge(record); + const irc = new IrcBridge(requestingUserId); return irc.hasNetworks(); } else return true; } @@ -55,7 +52,7 @@ export class BridgeStore { 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); + const irc = new IrcBridge(requestingUserId); return irc.getRoomConfiguration(requestingUserId, inRoomId); } else return {}; } diff --git a/src/db/DimensionStore.ts b/src/db/DimensionStore.ts index b6c7eae..bab7d0e 100644 --- a/src/db/DimensionStore.ts +++ b/src/db/DimensionStore.ts @@ -16,6 +16,8 @@ import NebNotificationUser from "./models/NebNotificationUser"; import NebIntegrationConfig from "./models/NebIntegrationConfig"; import Webhook from "./models/Webhook"; import BridgeRecord from "./models/BridgeRecord"; +import IrcBridgeRecord from "./models/IrcBridgeRecord"; +import IrcBridgeNetwork from "./models/IrcBridgeNetwork"; class _DimensionStore { private sequelize: Sequelize; @@ -43,6 +45,8 @@ class _DimensionStore { NebIntegrationConfig, Webhook, BridgeRecord, + IrcBridgeRecord, + IrcBridgeNetwork, ]); } diff --git a/src/db/migrations/20180330211845-AddIrcBridge.ts b/src/db/migrations/20180330211845-AddIrcBridge.ts new file mode 100644 index 0000000..3e53d82 --- /dev/null +++ b/src/db/migrations/20180330211845-AddIrcBridge.ts @@ -0,0 +1,36 @@ +import { QueryInterface } from "sequelize"; +import { DataType } from "sequelize-typescript"; + +export default { + up: (queryInterface: QueryInterface) => { + return Promise.resolve() + .then(() => queryInterface.createTable("dimension_irc_bridges", { + "id": {type: DataType.INTEGER, primaryKey: true, autoIncrement: true, allowNull: false}, + "upstreamId": { + type: DataType.INTEGER, allowNull: true, + references: {model: "dimension_upstreams", key: "id"}, + onUpdate: "cascade", onDelete: "cascade", + }, + "provisionUrl": {type: DataType.STRING, allowNull: true}, + "isEnabled": {type: DataType.BOOLEAN, allowNull: false}, + })) + .then(() => queryInterface.createTable("dimension_irc_bridge_networks", { + "id": {type: DataType.INTEGER, primaryKey: true, autoIncrement: true, allowNull: false}, + "bridgeId": { + type: DataType.INTEGER, allowNull: false, + references: {model: "dimension_irc_bridges", key: "id"}, + onUpdate: "cascade", onDelete: "cascade", + }, + "bridgeNetworkId": {type: DataType.STRING, allowNull: false}, + "bridgeUserId": {type: DataType.STRING, allowNull: false}, + "displayName": {type: DataType.STRING, allowNull: false}, + "domain": {type: DataType.STRING, allowNull: false}, + "isEnabled": {type: DataType.BOOLEAN, allowNull: false}, + })); + }, + down: (queryInterface: QueryInterface) => { + return Promise.resolve() + .then(() => queryInterface.dropTable("dimension_irc_bridges")) + .then(() => queryInterface.dropTable("dimension_irc_bridge_networks")); + } +} \ No newline at end of file diff --git a/src/db/models/IrcBridgeNetwork.ts b/src/db/models/IrcBridgeNetwork.ts new file mode 100644 index 0000000..98c272a --- /dev/null +++ b/src/db/models/IrcBridgeNetwork.ts @@ -0,0 +1,34 @@ +import { AllowNull, AutoIncrement, Column, ForeignKey, Model, PrimaryKey, Table } from "sequelize-typescript"; +import IrcBridgeRecord from "./IrcBridgeRecord"; + +@Table({ + tableName: "dimension_irc_bridge_networks", + underscoredAll: false, + timestamps: false, +}) +export default class IrcBridgeNetwork extends Model { + @PrimaryKey + @AutoIncrement + @Column + id: number; + + @AllowNull + @Column + @ForeignKey(() => IrcBridgeRecord) + bridgeId: number; + + @Column + isEnabled: boolean; + + @Column + bridgeNetworkId: string; // the real ID as given by /querynetworks + + @Column + bridgeUserId: string; + + @Column + displayName: string; + + @Column + domain: string; +} \ No newline at end of file diff --git a/src/db/models/IrcBridgeRecord.ts b/src/db/models/IrcBridgeRecord.ts new file mode 100644 index 0000000..167bbba --- /dev/null +++ b/src/db/models/IrcBridgeRecord.ts @@ -0,0 +1,26 @@ +import { AllowNull, AutoIncrement, Column, ForeignKey, Model, PrimaryKey, Table } from "sequelize-typescript"; +import Upstream from "./Upstream"; + +@Table({ + tableName: "dimension_irc_bridges", + underscoredAll: false, + timestamps: false, +}) +export default class IrcBridgeRecord extends Model { + @PrimaryKey + @AutoIncrement + @Column + id: number; + + @AllowNull + @Column + @ForeignKey(() => Upstream) + upstreamId?: number; + + @AllowNull + @Column + provisionUrl?: string; + + @Column + isEnabled: boolean; +} \ No newline at end of file diff --git a/src/integrations/Bridge.ts b/src/integrations/Bridge.ts index 87aeb22..f80dcd3 100644 --- a/src/integrations/Bridge.ts +++ b/src/integrations/Bridge.ts @@ -1,5 +1,6 @@ import { Integration } from "./Integration"; import BridgeRecord from "../db/models/BridgeRecord"; +import { AvailableNetworks } from "../bridges/IrcBridge"; export class Bridge extends Integration { constructor(bridge: BridgeRecord, public config: any) { @@ -17,12 +18,7 @@ export class Bridge extends Integration { } export interface IrcBridgeConfiguration { - availableNetworks: { - [networkId: string]: { - name: string; - bridgeUserId: string; - }; - }; + availableNetworks: AvailableNetworks; links: { [networkId: string]: { channelName: string; diff --git a/src/models/ModularResponses.ts b/src/models/ModularResponses.ts index 8d18f22..31d4e77 100644 --- a/src/models/ModularResponses.ts +++ b/src/models/ModularResponses.ts @@ -1,4 +1,13 @@ +import { QueryNetworksResponse } from "../bridges/models/provision_responses"; + export interface ModularIntegrationInfoResponse { bot_user_id: string; integrations?: any[]; +} + +export interface ModularIrcQueryNetworksResponse { + replies: { + rid: string; + response: QueryNetworksResponse; + }[]; } \ No newline at end of file diff --git a/web/app/admin/bridges/irc/irc.component.html b/web/app/admin/bridges/irc/irc.component.html index a4b61ab..d542354 100644 --- a/web/app/admin/bridges/irc/irc.component.html +++ b/web/app/admin/bridges/irc/irc.component.html @@ -31,9 +31,9 @@ {{ getEnabledNetworksString(bridge) }} - + diff --git a/web/app/admin/bridges/irc/irc.component.scss b/web/app/admin/bridges/irc/irc.component.scss index e69de29..788d7ed 100644 --- a/web/app/admin/bridges/irc/irc.component.scss +++ b/web/app/admin/bridges/irc/irc.component.scss @@ -0,0 +1,3 @@ +.editButton { + cursor: pointer; +} \ No newline at end of file diff --git a/web/app/admin/bridges/irc/irc.component.ts b/web/app/admin/bridges/irc/irc.component.ts index a2df478..ab86c37 100644 --- a/web/app/admin/bridges/irc/irc.component.ts +++ b/web/app/admin/bridges/irc/irc.component.ts @@ -1,31 +1,93 @@ -import { Component } from "@angular/core"; +import { Component, OnInit } from "@angular/core"; +import { ToasterService } from "angular2-toaster"; +import { AdminIrcApiService } from "../../../shared/services/admin/admin-irc-api.service"; +import { FE_Upstream } from "../../../shared/models/admin-responses"; +import { AdminUpstreamApiService } from "../../../shared/services/admin/admin-upstream-api.service"; +import { FE_IrcBridge } from "../../../shared/models/irc"; @Component({ templateUrl: "./irc.component.html", styleUrls: ["./irc.component.scss"], }) -export class AdminIrcBridgeComponent { +export class AdminIrcBridgeComponent implements OnInit { public isLoading = true; + public isUpdating = false; public hasModularBridge = false; - public configurations: any[] = []; + public configurations: FE_IrcBridge[] = []; - constructor() { + private upstreams: FE_Upstream[]; + + constructor(private upstreamApi: AdminUpstreamApiService, + private ircApi: AdminIrcApiService, + private toaster: ToasterService) { } - public getEnabledNetworksString(bridge: any): string { - return "TODO: " + bridge; + public ngOnInit() { + this.reload().then(() => this.isLoading = false); + } + + private async reload(): Promise { + try { + this.upstreams = await this.upstreamApi.getUpstreams(); + this.configurations = await this.ircApi.getBridges(); + + this.hasModularBridge = false; + for (const bridge of this.configurations) { + if (bridge.upstreamId) { + this.hasModularBridge = true; + break; + } + } + } catch (err) { + console.error(err); + this.toaster.pop("error", "Error loading bridges"); + } + } + + public getEnabledNetworksString(bridge: FE_IrcBridge): string { + const networkIds = Object.keys(bridge.availableNetworks); + const result = networkIds.filter(i => bridge.availableNetworks[i].isEnabled) + .map(i => bridge.availableNetworks[i].name) + .join(", "); + if (!result) return "None"; + return result; } public addModularHostedBridge() { + this.isUpdating = true; + const createBridge = (upstream: FE_Upstream) => { + return this.ircApi.newFromUpstream(upstream).then(bridge => { + this.configurations.push(bridge); + this.toaster.pop("success", "matrix.org's IRC bridge added", "Click the pencil icon to enable networks."); + this.isUpdating = false; + this.hasModularBridge = true; + }).catch(err => { + console.error(err); + this.isUpdating = false; + this.toaster.pop("error", "Error adding matrix.org's IRC Bridge"); + }); + }; + const vectorUpstreams = this.upstreams.filter(u => u.type === "vector"); + if (vectorUpstreams.length === 0) { + console.log("Creating default scalar upstream"); + const scalarUrl = "https://scalar.vector.im/api"; + this.upstreamApi.newUpstream("modular", "vector", scalarUrl, scalarUrl).then(upstream => { + this.upstreams.push(upstream); + createBridge(upstream); + }).catch(err => { + console.error(err); + this.toaster.pop("error", "Error creating matrix.org's IRC Bridge"); + }); + } else createBridge(vectorUpstreams[0]); } public addSelfHostedBridge() { - + console.log("TODO: Dialog"); } - public editNetworks(bridge: any) { + public editNetworks(bridge: FE_IrcBridge) { console.log(bridge); } } diff --git a/web/app/admin/neb/neb.component.ts b/web/app/admin/neb/neb.component.ts index a75f625..d2fa73d 100644 --- a/web/app/admin/neb/neb.component.ts +++ b/web/app/admin/neb/neb.component.ts @@ -95,7 +95,7 @@ export class AdminNebComponent { public addModularHostedNeb() { this.isAddingModularNeb = true; const createNeb = (upstream: FE_Upstream) => { - this.nebApi.newUpstreamConfiguration(upstream).then(neb => { + return this.nebApi.newUpstreamConfiguration(upstream).then(neb => { this.configurations.push(neb); this.toaster.pop("success", "matrix.org's go-neb added", "Click the pencil icon to enable the bots."); this.isAddingModularNeb = false; diff --git a/web/app/app.module.ts b/web/app/app.module.ts index 4772a88..73ded73 100644 --- a/web/app/app.module.ts +++ b/web/app/app.module.ts @@ -65,6 +65,7 @@ import { TravisCiComplexBotConfigComponent } from "./configs/complex-bot/travisc 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"; +import { AdminIrcApiService } from "./shared/services/admin/admin-irc-api.service"; @NgModule({ imports: [ @@ -138,6 +139,7 @@ import { AdminIrcBridgeComponent } from "./admin/bridges/irc/irc.component"; AdminAppserviceApiService, AdminNebApiService, AdminUpstreamApiService, + AdminIrcApiService, {provide: Window, useValue: window}, // Vendor diff --git a/web/app/shared/models/irc.ts b/web/app/shared/models/irc.ts new file mode 100644 index 0000000..40839ac --- /dev/null +++ b/web/app/shared/models/irc.ts @@ -0,0 +1,16 @@ +export interface FE_IrcBridge { + id: number; + upstreamId?: number; + provisionUrl?: string; + isEnabled: boolean; + availableNetworks: FE_IrcBridgeAvailableNetworks; +} + +export interface FE_IrcBridgeAvailableNetworks { + [networkId: string]: { + name: string; + domain: string; + bridgeUserId: string; + isEnabled: boolean; + }; +} \ No newline at end of file diff --git a/web/app/shared/services/admin/admin-irc-api.service.ts b/web/app/shared/services/admin/admin-irc-api.service.ts new file mode 100644 index 0000000..cf7df20 --- /dev/null +++ b/web/app/shared/services/admin/admin-irc-api.service.ts @@ -0,0 +1,28 @@ +import { Injectable } from "@angular/core"; +import { Http } from "@angular/http"; +import { AuthedApi } from "../authed-api"; +import { FE_Upstream } from "../../models/admin-responses"; +import { FE_IrcBridge } from "../../models/irc"; + +@Injectable() +export class AdminIrcApiService extends AuthedApi { + constructor(http: Http) { + super(http); + } + + public getBridges(): Promise { + return this.authedGet("/api/v1/dimension/admin/irc/all").map(r => r.json()).toPromise(); + } + + public getBridge(bridgeId: number): Promise { + return this.authedGet("/api/v1/dimension/admin/irc/" + bridgeId).map(r => r.json()).toPromise(); + } + + public newFromUpstream(upstream: FE_Upstream): Promise { + return this.authedPost("/api/v1/dimension/admin/irc/new/upstream", {upstreamId: upstream.id}).map(r => r.json()).toPromise(); + } + + public newSelfhosted(provisionUrl: string): Promise { + return this.authedPost("/api/v1/dimension/admin/irc/new/selfhosted", {provisionUrl: provisionUrl}).map(r => r.json()).toPromise(); + } +}