From f33f7e5716c0aaad843a9039c6faa3fcce6ac78f Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Sat, 31 Mar 2018 14:37:36 -0600 Subject: [PATCH] Self-service requests to bridge IRC channels --- .../dimension/DimensionIntegrationsService.ts | 9 +- src/api/dimension/DimensionIrcService.ts | 49 +++++++ src/bridges/IrcBridge.ts | 114 +++++++++++++-- .../models/{provision_responses.ts => irc.ts} | 10 ++ src/db/BridgeStore.ts | 7 +- src/integrations/Bridge.ts | 11 +- src/models/ModularResponses.ts | 6 +- web/app/app.module.ts | 4 + web/app/app.routing.ts | 20 +-- .../bridge/irc/irc.bridge.component.html | 89 ++++++++++++ .../bridge/irc/irc.bridge.component.scss | 4 + .../bridge/irc/irc.bridge.component.ts | 133 ++++++++++++++++++ web/app/shared/models/integration.ts | 1 - .../shared/models/server-client-responses.ts | 6 + .../services/integrations/irc-api.service.ts | 18 +++ .../scalar/scalar-client-api.service.ts | 12 +- 16 files changed, 450 insertions(+), 43 deletions(-) create mode 100644 src/api/dimension/DimensionIrcService.ts rename src/bridges/models/{provision_responses.ts => irc.ts} (50%) create mode 100644 web/app/configs/bridge/irc/irc.bridge.component.html create mode 100644 web/app/configs/bridge/irc/irc.bridge.component.scss create mode 100644 web/app/configs/bridge/irc/irc.bridge.component.ts create mode 100644 web/app/shared/services/integrations/irc-api.service.ts diff --git a/src/api/dimension/DimensionIntegrationsService.ts b/src/api/dimension/DimensionIntegrationsService.ts index f651062..5eae02b 100644 --- a/src/api/dimension/DimensionIntegrationsService.ts +++ b/src/api/dimension/DimensionIntegrationsService.ts @@ -46,14 +46,7 @@ export class DimensionIntegrationsService { * @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; + return BridgeStore.listAll(forUserId, enabledOnly ? true : null, inRoomId); } /** diff --git a/src/api/dimension/DimensionIrcService.ts b/src/api/dimension/DimensionIrcService.ts new file mode 100644 index 0000000..1cd13df --- /dev/null +++ b/src/api/dimension/DimensionIrcService.ts @@ -0,0 +1,49 @@ +import { GET, Path, PathParam, POST, QueryParam } from "typescript-rest"; +import { LogService } from "matrix-js-snippets"; +import { ScalarService } from "../scalar/ScalarService"; +import { IrcBridge } from "../../bridges/IrcBridge"; +import IrcBridgeRecord from "../../db/models/IrcBridgeRecord"; +import { ApiError } from "../ApiError"; + +interface RequestLinkRequest { + op: string; +} + +/** + * API for interacting with the IRC bridge + */ +@Path("/api/v1/dimension/irc") +export class DimensionIrcService { + + @GET + @Path(":networkId/channel/:channel/ops") + public async getOps(@QueryParam("scalar_token") scalarToken: string, @PathParam("networkId") networkId: string, @PathParam("channel") channelNoHash: string): Promise { + const userId = await ScalarService.getTokenOwner(scalarToken); + + const parsed = IrcBridge.parseNetworkId(networkId); + const bridge = await IrcBridgeRecord.findByPrimary(parsed.bridgeId); + if (!bridge) throw new ApiError(404, "Bridge not found"); + + const client = new IrcBridge(userId); + const operators = await client.getOperators(bridge, parsed.bridgeNetworkId, "#" + channelNoHash); + + LogService.info("DimensionIrcService", userId + " listed the operators for #" + channelNoHash + " on " + networkId); + return operators; + } + + @POST + @Path(":networkId/channel/:channel/link/:roomId") + public async requestLink(@QueryParam("scalar_token") scalarToken: string, @PathParam("networkId") networkId: string, @PathParam("channel") channelNoHash: string, @PathParam("roomId") roomId: string, request: RequestLinkRequest): Promise { + const userId = await ScalarService.getTokenOwner(scalarToken); + + const parsed = IrcBridge.parseNetworkId(networkId); + const bridge = await IrcBridgeRecord.findByPrimary(parsed.bridgeId); + if (!bridge) throw new ApiError(404, "Bridge not found"); + + const client = new IrcBridge(userId); + await client.requestLink(bridge, parsed.bridgeNetworkId, "#" + channelNoHash, request.op, roomId); + + LogService.info("DimensionIrcService", userId + " requested #" + channelNoHash + " on " + networkId + " to be linked to " + roomId); + return {}; // 200 OK + } +} \ No newline at end of file diff --git a/src/bridges/IrcBridge.ts b/src/bridges/IrcBridge.ts index ec943cf..922f5c9 100644 --- a/src/bridges/IrcBridge.ts +++ b/src/bridges/IrcBridge.ts @@ -5,9 +5,9 @@ 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 { ListLinksResponseItem, ListOpsResponse, QueryNetworksResponse } from "./models/irc"; import IrcBridgeNetwork from "../db/models/IrcBridgeNetwork"; +import { ModularIrcResponse } from "../models/ModularResponses"; interface CachedNetwork { ircBridgeId: number; @@ -27,6 +27,12 @@ export interface AvailableNetworks { }; } +export interface LinkedChannels { + [networkId: string]: { + channelName: string; + }[]; +} + export class IrcBridge { private static getNetworkId(network: CachedNetwork): string { return network.ircBridgeId + "-" + network.bridgeNetworkId; @@ -47,9 +53,10 @@ export class IrcBridge { return allNetworks.length > 0; } - public async getNetworks(bridge?: IrcBridgeRecord): Promise { + public async getNetworks(bridge?: IrcBridgeRecord, enabledOnly?: boolean): Promise { let networks = await this.getAllNetworks(); if (bridge) networks = networks.filter(n => n.ircBridgeId === bridge.id); + if (enabledOnly) networks = networks.filter(n => n.isEnabled); const available: AvailableNetworks = {}; networks.forEach(n => available[IrcBridge.getNetworkId(n)] = { @@ -61,12 +68,63 @@ export class IrcBridge { return available; } - public async getRoomConfiguration(requestingUserId: string, inRoomId: string): Promise { - return {requestingUserId, inRoomId}; + public async getRoomConfiguration(inRoomId: string): Promise { + const availableNetworks = await this.getNetworks(null, true); + const bridges = await IrcBridgeRecord.findAll({where: {isEnabled: true}}); + const linkedChannels: LinkedChannels = {}; + + for (const bridge of bridges) { + const links = await this.fetchLinks(bridge, inRoomId); + for (const key of Object.keys(links)) { + linkedChannels[key] = links[key]; + } + } + + return {availableNetworks: availableNetworks, links: linkedChannels}; } - public async setRoomConfiguration(requestingUserId: string, inRoomId: string, newConfig: IrcBridgeConfiguration): Promise { - return {requestingUserId, inRoomId, newConfig}; + public async getOperators(bridge: IrcBridgeRecord, networkId: string, channel: string): Promise { + const network = (await this.getAllNetworks()).find(n => n.isEnabled && n.ircBridgeId === bridge.id && n.bridgeNetworkId === networkId); + if (!network) throw new Error("Network not found"); + + const requestBody = {remote_room_server: network.domain, remote_room_channel: channel}; + + let responses: ListOpsResponse[] = []; + if (bridge.upstreamId) { + const result = await this.doUpstreamRequest>(bridge, "POST", "/bridges/irc/_matrix/provision/querylink", null, requestBody); + if (result && result.replies) responses = result.replies.map(r => r.response); + } else { + const result = await this.doProvisionRequest(bridge, "POST", "/_matrix/provision/querylink", null, requestBody); + if (result) responses = [result]; + } + + const ops: string[] = []; + for (const response of responses) { + if (!response || !response.operators) continue; + response.operators.forEach(i => ops.push(i)); + } + + return ops; + } + + public async requestLink(bridge: IrcBridgeRecord, networkId: string, channel: string, op: string, inRoomId: string): Promise { + const network = (await this.getAllNetworks()).find(n => n.isEnabled && n.ircBridgeId === bridge.id && n.bridgeNetworkId === networkId); + if (!network) throw new Error("Network not found"); + + const requestBody = { + remote_room_server: network.domain, + remote_room_channel: channel, + matrix_room_id: inRoomId, + op_nick: op, + user_id: this.requestingUserId, + }; + + if (bridge.upstreamId) { + delete requestBody["user_id"]; + await this.doUpstreamRequest(bridge, "POST", "/bridges/irc/_matrix/provision/link", null, requestBody); + } else { + await this.doProvisionRequest(bridge, "POST", "/_matrix/provision/link", null, requestBody); + } } private async getAllNetworks(): Promise { @@ -86,10 +144,50 @@ export class IrcBridge { return networks; } + private async fetchLinks(bridge: IrcBridgeRecord, inRoomId: string): Promise { + const availableNetworks = await this.getNetworks(bridge, true); + const networksByDomain: { [domain: string]: { id: string, name: string, bridgeUserId: string } } = {}; + for (const key of Object.keys(availableNetworks)) { + const network = availableNetworks[key]; + networksByDomain[network.domain] = { + id: key, + name: network.name, + bridgeUserId: network.bridgeUserId, + }; + } + + let responses: ListLinksResponseItem[] = []; + if (bridge.upstreamId) { + const result = await this.doUpstreamRequest>(bridge, "GET", "/bridges/irc/_matrix/provision/listlinks/" + inRoomId); + if (result && result.replies) { + const replies = result.replies.map(r => r.response); + for (const reply of replies) reply.forEach(r => responses.push(r)); + } + } else { + const result = await this.doProvisionRequest(bridge, "GET", "/_matrix/provision/listlinks/" + inRoomId); + if (result) responses = result; + } + + const linked: LinkedChannels = {}; + for (const response of responses) { + if (!response || !response.remote_room_server) continue; + + const network = networksByDomain[response.remote_room_server]; + if (!network) continue; + + if (!linked[network.id]) linked[network.id] = []; + linked[network.id].push({ + channelName: response.remote_room_channel, + }); + } + + return linked; + } + private async fetchNetworks(bridge: IrcBridgeRecord): Promise { let responses: QueryNetworksResponse[] = []; if (bridge.upstreamId) { - const result = await this.doUpstreamRequest(bridge, "GET", "/bridges/irc/_matrix/provision/querynetworks"); + 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"); diff --git a/src/bridges/models/provision_responses.ts b/src/bridges/models/irc.ts similarity index 50% rename from src/bridges/models/provision_responses.ts rename to src/bridges/models/irc.ts index 0c92c9a..b8dde4b 100644 --- a/src/bridges/models/provision_responses.ts +++ b/src/bridges/models/irc.ts @@ -7,4 +7,14 @@ export interface QueryNetworksResponse { domain: string; }; }[]; +} + +export interface ListLinksResponseItem { + matrix_room_id: string; + remote_room_channel: string; + remote_room_server: string; +} + +export interface ListOpsResponse { + operators: string[]; } \ No newline at end of file diff --git a/src/db/BridgeStore.ts b/src/db/BridgeStore.ts index 99faa01..5c502b9 100644 --- a/src/db/BridgeStore.ts +++ b/src/db/BridgeStore.ts @@ -32,13 +32,12 @@ export class BridgeStore { return bridge.save(); } - public static async setBridgeRoomConfig(requestingUserId: string, integrationType: string, inRoomId: string, newConfig: any): Promise { + public static async setBridgeRoomConfig(_requestingUserId: string, integrationType: string, _inRoomId: string, _newConfig: any): Promise { const record = await BridgeRecord.findOne({where: {type: integrationType}}); if (!record) throw new Error("Bridge not found"); if (integrationType === "irc") { - const irc = new IrcBridge(requestingUserId); - return irc.setRoomConfiguration(requestingUserId, inRoomId, newConfig); + throw new Error("IRC Bridges should be modified with the dedicated API"); } else throw new Error("Unsupported bridge"); } @@ -53,7 +52,7 @@ export class BridgeStore { if (record.type === "irc") { if (!inRoomId) return {}; // The bridge's admin config is handled by other APIs const irc = new IrcBridge(requestingUserId); - return irc.getRoomConfiguration(requestingUserId, inRoomId); + return irc.getRoomConfiguration(inRoomId); } else return {}; } diff --git a/src/integrations/Bridge.ts b/src/integrations/Bridge.ts index f80dcd3..fef2643 100644 --- a/src/integrations/Bridge.ts +++ b/src/integrations/Bridge.ts @@ -1,6 +1,6 @@ import { Integration } from "./Integration"; import BridgeRecord from "../db/models/BridgeRecord"; -import { AvailableNetworks } from "../bridges/IrcBridge"; +import { AvailableNetworks, LinkedChannels } from "../bridges/IrcBridge"; export class Bridge extends Integration { constructor(bridge: BridgeRecord, public config: any) { @@ -12,17 +12,12 @@ export class Bridge extends Integration { argument: null, // not used }]; - // We'll just say we aren't + // We'll just say we don't support encryption this.isEncryptionSupported = false; } } export interface IrcBridgeConfiguration { availableNetworks: AvailableNetworks; - links: { - [networkId: string]: { - channelName: string; - addedByUserId: string; - }[]; - }; + links: LinkedChannels; } \ No newline at end of file diff --git a/src/models/ModularResponses.ts b/src/models/ModularResponses.ts index 31d4e77..f9bac1b 100644 --- a/src/models/ModularResponses.ts +++ b/src/models/ModularResponses.ts @@ -1,13 +1,11 @@ -import { QueryNetworksResponse } from "../bridges/models/provision_responses"; - export interface ModularIntegrationInfoResponse { bot_user_id: string; integrations?: any[]; } -export interface ModularIrcQueryNetworksResponse { +export interface ModularIrcResponse { replies: { rid: string; - response: QueryNetworksResponse; + response: T; }[]; } \ No newline at end of file diff --git a/web/app/app.module.ts b/web/app/app.module.ts index 059d8c5..c27dc03 100644 --- a/web/app/app.module.ts +++ b/web/app/app.module.ts @@ -68,6 +68,8 @@ import { AdminIrcBridgeComponent } from "./admin/bridges/irc/irc.component"; import { AdminIrcApiService } from "./shared/services/admin/admin-irc-api.service"; import { AdminIrcBridgeNetworksComponent } from "./admin/bridges/irc/networks/networks.component"; import { AdminIrcBridgeAddSelfhostedComponent } from "./admin/bridges/irc/add-selfhosted/add-selfhosted.component"; +import { IrcBridgeConfigComponent } from "./configs/bridge/irc/irc.bridge.component"; +import { IrcApiService } from "./shared/services/integrations/irc-api.service"; @NgModule({ imports: [ @@ -129,6 +131,7 @@ import { AdminIrcBridgeAddSelfhostedComponent } from "./admin/bridges/irc/add-se AdminIrcBridgeComponent, AdminIrcBridgeNetworksComponent, AdminIrcBridgeAddSelfhostedComponent, + IrcBridgeConfigComponent, // Vendor ], @@ -144,6 +147,7 @@ import { AdminIrcBridgeAddSelfhostedComponent } from "./admin/bridges/irc/add-se AdminNebApiService, AdminUpstreamApiService, AdminIrcApiService, + IrcApiService, {provide: Window, useValue: window}, // Vendor diff --git a/web/app/app.routing.ts b/web/app/app.routing.ts index f7fca46..8fc8699 100644 --- a/web/app/app.routing.ts +++ b/web/app/app.routing.ts @@ -23,6 +23,7 @@ import { RssComplexBotConfigComponent } from "./configs/complex-bot/rss/rss.comp 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"; +import { IrcBridgeConfigComponent } from "./configs/bridge/irc/irc.bridge.component"; const routes: Routes = [ {path: "", component: HomeComponent}, @@ -142,15 +143,16 @@ const routes: Routes = [ }, ], }, - // { - // path: "bridge", - // children: [ - // { - // path: "irc", - // - // } - // ] - // } + { + path: "bridge", + children: [ + { + path: "irc", + component: IrcBridgeConfigComponent, + data: {breadcrumb: "IRC Bridge Configuration", name: "IRC Bridge Configuration"}, + }, + ], + }, ], }, { diff --git a/web/app/configs/bridge/irc/irc.bridge.component.html b/web/app/configs/bridge/irc/irc.bridge.component.html new file mode 100644 index 0000000..9063f54 --- /dev/null +++ b/web/app/configs/bridge/irc/irc.bridge.component.html @@ -0,0 +1,89 @@ + + + +
+ Add an IRC channel +
+
+
+ Bridging a channel requires authorization from a channel operator. When entering a channel below, a + bot will + join the channel to ensure it exists and has operators available. +
+ +
+ + +
+
#
+ +
+
+ +
+
+ +
+ +
+ +
+
+
+
+ +
+ IRC Networks +
+
+ + + + + + + + + + + + + + + + + + +
ChannelNetworkActions
No bridged channels
+ {{ channel.name }} + (pending) + {{ channel.networkName }} + +
+
+
+
+
\ No newline at end of file diff --git a/web/app/configs/bridge/irc/irc.bridge.component.scss b/web/app/configs/bridge/irc/irc.bridge.component.scss new file mode 100644 index 0000000..7c9eeab --- /dev/null +++ b/web/app/configs/bridge/irc/irc.bridge.component.scss @@ -0,0 +1,4 @@ +.actions-col { + width: 120px; + text-align: center; +} \ No newline at end of file diff --git a/web/app/configs/bridge/irc/irc.bridge.component.ts b/web/app/configs/bridge/irc/irc.bridge.component.ts new file mode 100644 index 0000000..fb31acb --- /dev/null +++ b/web/app/configs/bridge/irc/irc.bridge.component.ts @@ -0,0 +1,133 @@ +import { Component } from "@angular/core"; +import { BridgeComponent } from "../bridge.component"; +import { FE_IrcBridgeAvailableNetworks } from "../../../shared/models/irc"; +import { IrcApiService } from "../../../shared/services/integrations/irc-api.service"; + +interface IrcConfig { + availableNetworks: FE_IrcBridgeAvailableNetworks; + links: { + [networkId: string]: { + channelName: string; + }[]; + }; +} + +interface LocalChannel { + name: string; + networkId: string; + networkName: string; + pending: boolean; +} + +@Component({ + templateUrl: "irc.bridge.component.html", + styleUrls: ["irc.bridge.component.scss"], +}) +export class IrcBridgeConfigComponent extends BridgeComponent { + + public loadingOps = false; + public requestingBridge = false; + public channelStep = 1; + public networkId: string; + public channel = ""; + public ops: string[]; + public op: string; + public pending: LocalChannel[] = []; + + constructor(private irc: IrcApiService) { + super("irc"); + } + + private resetForm() { + this.networkId = this.getNetworks()[0].id; + this.channel = ""; + this.ops = []; + this.channelStep = 1; + } + + public getNetworks(): { id: string, name: string }[] { + const ids = Object.keys(this.bridge.config.availableNetworks); + if (!this.networkId) setTimeout(() => this.networkId = ids[0], 0); + return ids.map(i => { + return {id: i, name: this.bridge.config.availableNetworks[i].name}; + }); + } + + public loadOps() { + if (!this.channel.trim()) { + this.toaster.pop("warning", "Please enter a channel name"); + return; + } + + this.loadingOps = true; + this.irc.getOperators(this.networkId, this.channel).then(ops => { + this.ops = ops; + this.op = ops[0]; + this.loadingOps = false; + this.channelStep = 2; + }).catch(err => { + console.error(err); + this.loadingOps = false; + this.toaster.pop("error", "Error loading channel operators"); + }); + } + + public async requestBridge() { + this.requestingBridge = true; + const bridgeUserId = this.bridge.config.availableNetworks[this.networkId].bridgeUserId; + + const memberEvent = await this.scalarClientApi.getMembershipState(this.roomId, bridgeUserId); + const isJoined = memberEvent && memberEvent.response && ["join", "invite"].indexOf(memberEvent.response.membership) !== -1; + + if (!isJoined) await this.scalarClientApi.inviteUser(this.roomId, bridgeUserId); + + try { + await this.scalarClientApi.setUserPowerLevel(this.roomId, bridgeUserId, 100); + } catch (err) { + console.error(err); + this.requestingBridge = false; + this.toaster.pop("error", "Failed to make the bridge an administrator", "Please ensure you are an 'Admin' for the room"); + return; + } + + this.irc.requestLink(this.roomId, this.networkId, this.channel, this.op).then(() => { + this.requestingBridge = false; + this.pending.push({ + name: this.channel, + networkId: this.networkId, + networkName: this.bridge.config.availableNetworks[this.networkId].name, + pending: true, + }); + this.resetForm(); + this.toaster.pop("success", "Link requested!", "The operator selected will have to approve the bridge for it to work"); + }).catch(err => { + console.error(err); + this.requestingBridge = false; + this.toaster.pop("error", "Failed to request a link"); + }); + } + + public getChannels(): LocalChannel[] { + const channels: LocalChannel[] = []; + this.pending.forEach(p => channels.push(p)); + + for (const networkId of Object.keys(this.bridge.config.links)) { + const network = this.bridge.config.availableNetworks[networkId]; + + for (const channel of this.bridge.config.links[networkId]) { + channels.push({ + networkId: networkId, + networkName: network.name, + name: channel.channelName, + pending: false, + }); + } + } + + return channels; + } + + public removeChannel(channel: any) { + console.log(channel); + } +} \ No newline at end of file diff --git a/web/app/shared/models/integration.ts b/web/app/shared/models/integration.ts index 6b064bb..ea72042 100644 --- a/web/app/shared/models/integration.ts +++ b/web/app/shared/models/integration.ts @@ -27,7 +27,6 @@ export interface FE_ComplexBot extends FE_Integration { } export interface FE_Bridge extends FE_Integration { - bridgeUserId: string; config: T; } diff --git a/web/app/shared/models/server-client-responses.ts b/web/app/shared/models/server-client-responses.ts index 3cc3987..1b5a84f 100644 --- a/web/app/shared/models/server-client-responses.ts +++ b/web/app/shared/models/server-client-responses.ts @@ -55,4 +55,10 @@ export interface CanSendEventResponse extends ScalarRoomResponse { export interface RoomEncryptionStatusResponse extends ScalarRoomResponse { response: boolean; +} + +export interface SetPowerLevelResponse extends ScalarRoomResponse { + response: { + success: boolean; + }; } \ No newline at end of file diff --git a/web/app/shared/services/integrations/irc-api.service.ts b/web/app/shared/services/integrations/irc-api.service.ts new file mode 100644 index 0000000..7932098 --- /dev/null +++ b/web/app/shared/services/integrations/irc-api.service.ts @@ -0,0 +1,18 @@ +import { Injectable } from "@angular/core"; +import { Http } from "@angular/http"; +import { AuthedApi } from "../authed-api"; + +@Injectable() +export class IrcApiService extends AuthedApi { + constructor(http: Http) { + super(http); + } + + public getOperators(networkId: string, channelNoHash: string): Promise { + return this.authedGet("/api/v1/dimension/irc/" + networkId + "/channel/" + channelNoHash + "/ops").map(r => r.json()).toPromise(); + } + + public requestLink(roomId: string, networkId: string, channelNoHash: string, op: string): Promise { + return this.authedPost("/api/v1/dimension/irc/" + networkId + "/channel/" + channelNoHash + "/link/" + roomId, {op: op}).map(r => r.json()).toPromise(); + } +} diff --git a/web/app/shared/services/scalar/scalar-client-api.service.ts b/web/app/shared/services/scalar/scalar-client-api.service.ts index ad111ce..f943cac 100644 --- a/web/app/shared/services/scalar/scalar-client-api.service.ts +++ b/web/app/shared/services/scalar/scalar-client-api.service.ts @@ -3,8 +3,10 @@ import * as randomString from "random-string"; import { CanSendEventResponse, JoinRuleStateResponse, - MembershipStateResponse, RoomEncryptionStatusResponse, + MembershipStateResponse, + RoomEncryptionStatusResponse, ScalarSuccessResponse, + SetPowerLevelResponse, WidgetsResponse } from "../../models/server-client-responses"; import { EditableWidget } from "../../models/widget"; @@ -87,6 +89,14 @@ export class ScalarClientApiService { }); } + public setUserPowerLevel(roomId: string, userId: string, powerLevel: number): Promise { + return this.callAction("set_bot_power", { + room_id: roomId, + user_id: userId, + level: powerLevel, + }); + } + private callAction(action, payload): Promise { let requestKey = randomString({length: 20}); return new Promise((resolve, reject) => {