From 87121150cce343212ce7ea9642a0e4cabea6ac04 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Sun, 25 Mar 2018 16:20:15 -0600 Subject: [PATCH] Support removing simple bots from rooms --- .../dimension/DimensionIntegrationsService.ts | 14 ++++++++- src/db/NebStore.ts | 29 ++++++++++++++----- src/matrix/MatrixAppserviceClient.ts | 9 ++++++ src/neb/NebProxy.ts | 14 +++++++++ src/temp_todo.txt | 1 - web/app/riot/riot-home/home.component.ts | 4 +-- web/app/shared/services/authed-api.ts | 6 ++++ .../integrations/integrations-api.service.ts | 4 +++ 8 files changed, 68 insertions(+), 13 deletions(-) diff --git a/src/api/dimension/DimensionIntegrationsService.ts b/src/api/dimension/DimensionIntegrationsService.ts index 9367c87..1738b31 100644 --- a/src/api/dimension/DimensionIntegrationsService.ts +++ b/src/api/dimension/DimensionIntegrationsService.ts @@ -1,4 +1,4 @@ -import { GET, Path, PathParam, QueryParam } from "typescript-rest"; +import { DELETE, GET, Path, PathParam, QueryParam } from "typescript-rest"; import { ScalarService } from "../scalar/ScalarService"; import { Widget } from "../../integrations/Widget"; import { Cache, CACHE_INTEGRATIONS } from "../../MemoryCache"; @@ -52,6 +52,18 @@ export class DimensionIntegrationsService { return this.getEnabledIntegrations(scalarToken); } + @DELETE + @Path("room/:roomId/integrations/:category/:type") + public async removeIntegrationInRoom(@QueryParam("scalar_token") scalarToken: string, @PathParam("roomId") roomId: string, @PathParam("category") category: string, @PathParam("type") integrationType: string): Promise { + const userId = await ScalarService.getTokenOwner(scalarToken); + + if (category === "widget") throw new ApiError(400, "Widgets should be removed client-side"); + else if (category === "bot") await NebStore.removeSimpleBot(integrationType, roomId, userId); + else throw new ApiError(400, "Unrecognized category"); + + return {}; // 200 OK + } + @GET @Path(":category/:type") public async getIntegration(@PathParam("category") category: string, @PathParam("type") type: string): Promise { diff --git a/src/db/NebStore.ts b/src/db/NebStore.ts index 4075dc3..ebf0289 100644 --- a/src/db/NebStore.ts +++ b/src/db/NebStore.ts @@ -90,9 +90,9 @@ export class NebStore { }, }; - public static async listSimpleBots(requestingUserId: string): Promise { + public static async listEnabledNebSimpleBots(): Promise<{neb: NebConfig, integration: NebIntegration}[]>{ const nebConfigs = await NebStore.getAllConfigs(); - const integrations: { integration: NebIntegration, userId: string }[] = []; + const integrations: {neb: NebConfig, integration: NebIntegration}[] = []; const hasTypes: string[] = []; for (const neb of nebConfigs) { @@ -103,16 +103,29 @@ export class NebStore { if (!metadata || !metadata.simple) continue; if (hasTypes.indexOf(integration.type) !== -1) continue; - const proxy = new NebProxy(neb, requestingUserId); - integrations.push({ - integration: integration, - userId: await proxy.getBotUserId(integration), - }); + integrations.push({neb, integration}); hasTypes.push(integration.type); } } - return integrations.map(i => new SimpleBot(i.integration, i.userId)); + return integrations; + } + + public static async listSimpleBots(requestingUserId: string): Promise { + const rawIntegrations = await NebStore.listEnabledNebSimpleBots(); + return Promise.all(rawIntegrations.map(async i => { + const proxy = new NebProxy(i.neb, requestingUserId); + return new SimpleBot(i.integration, await proxy.getBotUserId(i.integration)); + })); + } + + public static async removeSimpleBot(type: string, roomId: string, requestingUserId: string): Promise { + const rawIntegrations = await NebStore.listEnabledNebSimpleBots(); + const integration = rawIntegrations.find(i => i.integration.type === type); + if (!integration) throw new Error("Integration not found"); + + const proxy = new NebProxy(integration.neb, requestingUserId); + return proxy.removeBotFromRoom(integration.integration, roomId); } public static async getAllConfigs(): Promise { diff --git a/src/matrix/MatrixAppserviceClient.ts b/src/matrix/MatrixAppserviceClient.ts index cf1aa1e..996c9ec 100644 --- a/src/matrix/MatrixAppserviceClient.ts +++ b/src/matrix/MatrixAppserviceClient.ts @@ -30,4 +30,13 @@ export class MatrixAppserviceClient { ); return response['user_id']; } + + public async leaveRoom(virtualUserId: string, roomId: string): Promise { + return doClientApiCall( + "POST", + "/_matrix/client/r0/rooms/" + encodeURIComponent(roomId) + "/leave", + {access_token: this.appservice.asToken, user_id: virtualUserId}, + {}, + ); + } } diff --git a/src/neb/NebProxy.ts b/src/neb/NebProxy.ts index e5e8de9..e274de4 100644 --- a/src/neb/NebProxy.ts +++ b/src/neb/NebProxy.ts @@ -7,6 +7,8 @@ import Upstream from "../db/models/Upstream"; import UserScalarToken from "../db/models/UserScalarToken"; import { NebClient } from "./NebClient"; import { ModularIntegrationInfoResponse } from "../models/ModularResponses"; +import { AppserviceStore } from "../db/AppserviceStore"; +import { MatrixAppserviceClient } from "../matrix/MatrixAppserviceClient"; export class NebProxy { constructor(private neb: NebConfig, private requestingUserId: string) { @@ -29,6 +31,18 @@ export class NebProxy { } } + public async removeBotFromRoom(integration: NebIntegration, roomId: string) { + if (integration.nebId !== this.neb.id) throw new Error("Integration is not for this NEB proxy"); + + if (this.neb.upstreamId) { + await this.doUpstreamRequest("/removeIntegration", {type: integration.type, room_id: roomId}); + } else { + const appservice = await AppserviceStore.getAppservice(this.neb.appserviceId); + const client = new MatrixAppserviceClient(appservice); + await client.leaveRoom(await this.getBotUserId(integration), roomId); + } + } + private async doUpstreamRequest(endpoint: string, body?: any): Promise { const upstream = await Upstream.findByPrimary(this.neb.upstreamId); const token = await UserScalarToken.findOne({ diff --git a/src/temp_todo.txt b/src/temp_todo.txt index 0326ceb..88457e6 100644 --- a/src/temp_todo.txt +++ b/src/temp_todo.txt @@ -1,5 +1,4 @@ Release checklist: -* Remove bots from rooms * Manage custom bots (not sure if anyone actually uses this?) * RSS bot * Travis CI diff --git a/web/app/riot/riot-home/home.component.ts b/web/app/riot/riot-home/home.component.ts index 364f4a3..3e9bdb4 100644 --- a/web/app/riot/riot-home/home.component.ts +++ b/web/app/riot/riot-home/home.component.ts @@ -135,9 +135,7 @@ export class RiotHomeComponent { let promise: Promise = Promise.resolve(); if (!integration._inRoom) { promise = this.scalar.inviteUser(this.roomId, bot.userId); - } - // TODO: Handle removal of bots - // else promise = this.api.removeIntegration(this.roomId, integration.type, integration.integrationType, this.scalarToken); + } else promise = this.integrationsApi.removeIntegration(integration.category, integration.type, this.roomId); // We set this ahead of the promise for debouncing integration._inRoom = !integration._inRoom; diff --git a/web/app/shared/services/authed-api.ts b/web/app/shared/services/authed-api.ts index 8334134..06782f9 100644 --- a/web/app/shared/services/authed-api.ts +++ b/web/app/shared/services/authed-api.ts @@ -17,4 +17,10 @@ export class AuthedApi { const qs = {scalar_token: SessionStorage.scalarToken}; return this.http.post(url, body, {params: qs}); } + + protected authedDelete(url: string, qs?: any): Observable { + if (!qs) qs = {}; + qs["scalar_token"] = SessionStorage.scalarToken; + return this.http.delete(url, {params: qs}); + } } diff --git a/web/app/shared/services/integrations/integrations-api.service.ts b/web/app/shared/services/integrations/integrations-api.service.ts index 0923ef9..ee8a5b1 100644 --- a/web/app/shared/services/integrations/integrations-api.service.ts +++ b/web/app/shared/services/integrations/integrations-api.service.ts @@ -26,4 +26,8 @@ export class IntegrationsApiService extends AuthedApi { return this.http.get("/api/v1/dimension/widgets/embeddable", {params: {url: url}}) .map(r => r.json()).toPromise(); } + + public removeIntegration(category: string, type: string, roomId: string): Promise { + return this.authedDelete("/api/v1/dimension/integrations/room/" + roomId + "/integrations/" + category + "/" + type).map(r => r.json()).toPromise(); + } }