From 094b2a8b6a46763f1be502ccb00a7876a4ca63f6 Mon Sep 17 00:00:00 2001
From: Will Hunt <will@half-shot.uk>
Date: Sat, 11 May 2019 17:45:15 +0100
Subject: [PATCH] Support unbridging rooms from discord

---
 src/bot.ts                         |  2 +-
 src/discordcommandhandler.ts       | 32 +++++++++++++++++++++++--
 src/matrixcommandhandler.ts        | 10 +++++---
 src/provisioner.ts                 | 38 +++++++++++++++++-------------
 src/util.ts                        |  2 +-
 test/test_discordcommandhandler.ts | 22 +++++++++++++++++
 test/test_matrixcommandhandler.ts  | 34 ++++++++++++++++++++++----
 test/test_provisioner.ts           |  6 ++---
 8 files changed, 115 insertions(+), 31 deletions(-)

diff --git a/src/bot.ts b/src/bot.ts
index 563cc31..01cd190 100644
--- a/src/bot.ts
+++ b/src/bot.ts
@@ -90,7 +90,6 @@ export class DiscordBot {
     ) {
 
         // create handlers
-        this.provisioner = new Provisioner(store.roomStore);
         this.clientFactory = new DiscordClientFactory(store, config.auth);
         this.discordMsgProcessor = new DiscordMessageProcessor(
             new DiscordMessageProcessorOpts(config.bridge.domain, this),
@@ -101,6 +100,7 @@ export class DiscordBot {
             new MatrixEventProcessorOpts(config, bridge, this),
         );
         this.channelSync = new ChannelSyncroniser(bridge, config, this, store.roomStore);
+        this.provisioner = new Provisioner(store.roomStore, this.channelSync);
         this.discordCommandHandler = new DiscordCommandHandler(bridge, this);
         // init vars
         this.sentMessages = [];
diff --git a/src/discordcommandhandler.ts b/src/discordcommandhandler.ts
index 8f3804c..99dfc62 100644
--- a/src/discordcommandhandler.ts
+++ b/src/discordcommandhandler.ts
@@ -18,6 +18,10 @@ import { DiscordBot } from "./bot";
 import * as Discord from "discord.js";
 import { Util, ICommandActions, ICommandParameters, CommandPermissonCheck } from "./util";
 import { Bridge } from "matrix-appservice-bridge";
+import { Log } from "./log";
+
+const log = new Log("MatrixCommandHandler");
+
 export class DiscordCommandHandler {
     constructor(
         private bridge: Bridge,
@@ -78,6 +82,12 @@ export class DiscordCommandHandler {
                 permission: "BAN_MEMBERS",
                 run: this.ModerationActionGenerator(chan, "unban"),
             },
+            unbridge: {
+                description: "Unbridge matrix rooms from this channel",
+                params: [],
+                permission: ["MANAGE_WEBHOOKS", "MANAGE_CHANNELS"],
+                run: async () => this.UnbridgeChannel(chan),
+            },
         };
 
         const parameters: ICommandParameters = {
@@ -91,8 +101,11 @@ export class DiscordCommandHandler {
             },
         };
 
-        const permissionCheck: CommandPermissonCheck = async (permission) => {
-            return msg.member.hasPermission(permission as Discord.PermissionResolvable);
+        const permissionCheck: CommandPermissonCheck = async (permission: string|string[]) => {
+            if (!Array.isArray(permission)) {
+                permission = [permission];
+            }
+            return permission.every((p) => msg.member.hasPermission(p as Discord.PermissionResolvable));
         };
 
         const reply = await Util.ParseCommand("!matrix", msg.content, actions, parameters, permissionCheck);
@@ -131,4 +144,19 @@ export class DiscordCommandHandler {
             return `${action} ${name}`;
         };
     }
+
+    private async UnbridgeChannel(channel: Discord.TextChannel): Promise<string> {
+        try {
+            await this.discord.Provisioner.UnbridgeChannel(channel);
+            return "This channel has been unbridged";
+        } catch (err) {
+            if (err.message === "Channel is not bridged") {
+                return "This channel is not bridged to a plubmed matrix room";
+            }
+            log.error("Error while unbridging room " + channel.id);
+            log.error(err);
+            return "There was an error unbridging this room. " +
+                "Please try again later or contact the bridge operator.";
+        }
+    }
 }
diff --git a/src/matrixcommandhandler.ts b/src/matrixcommandhandler.ts
index b9e00a9..c91ccbc 100644
--- a/src/matrixcommandhandler.ts
+++ b/src/matrixcommandhandler.ts
@@ -23,6 +23,7 @@ import { Provisioner } from "./provisioner";
 import { Util, ICommandActions, ICommandParameters, CommandPermissonCheck } from "./util";
 import * as Discord from "discord.js";
 import * as markdown from "discord-markdown";
+import { RemoteStoreRoom } from "./db/roomstore";
 const log = new Log("MatrixCommandHandler");
 
 /* tslint:disable:no-magic-numbers */
@@ -53,7 +54,6 @@ export class MatrixCommandHandler {
     }
 
     public async Process(event: IMatrixEvent, context: BridgeContext) {
-        const intent = this.bridge.getIntent();
         if (!(await this.isBotInRoom(event.room_id))) {
             log.warn(`Bot is not in ${event.room_id}. Ignoring command`);
             return;
@@ -122,15 +122,19 @@ export class MatrixCommandHandler {
                     subcat: "m.room.power_levels",
                 },
                 run: async () => {
-                    const remoteRoom = context.rooms.remote;
+                    const remoteRoom = context.rooms.remote as RemoteStoreRoom;
                     if (!remoteRoom) {
                         return "This room is not bridged.";
                     }
                     if (!remoteRoom.data.plumbed) {
                         return "This room cannot be unbridged.";
                     }
+                    const res = await this.discord.LookupRoom(
+                        remoteRoom.data.discord_guild,
+                        remoteRoom.data.discord_channel,
+                    );
                     try {
-                        await this.provisioner.UnbridgeRoom(remoteRoom);
+                        await this.provisioner.UnbridgeChannel(res.channel);
                         return "This room has been unbridged";
                     } catch (err) {
                         log.error("Error while unbridging room " + event.room_id);
diff --git a/src/provisioner.ts b/src/provisioner.ts
index c5b5306..0b5ee74 100644
--- a/src/provisioner.ts
+++ b/src/provisioner.ts
@@ -14,13 +14,9 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-import {
-    Bridge,
-    RemoteRoom,
-    MatrixRoom,
-} from "matrix-appservice-bridge";
 import * as Discord from "discord.js";
-import { DbRoomStore } from "./db/roomstore";
+import { DbRoomStore, RemoteStoreRoom, MatrixStoreRoom } from "./db/roomstore";
+import { ChannelSyncroniser } from "./channelsyncroniser";
 
 const PERMISSION_REQUEST_TIMEOUT = 300000; // 5 minutes
 
@@ -28,21 +24,31 @@ export class Provisioner {
 
     private pendingRequests: Map<string, (approved: boolean) => void> = new Map(); // [channelId]: resolver fn
 
-    constructor(private roomStore: DbRoomStore) { }
+    constructor(private roomStore: DbRoomStore, private channelSync: ChannelSyncroniser) { }
 
     public async BridgeMatrixRoom(channel: Discord.TextChannel, roomId: string) {
-        const remote = new RemoteRoom(`discord_${channel.guild.id}_${channel.id}_bridged`);
-        remote.set("discord_type", "text");
-        remote.set("discord_guild", channel.guild.id);
-        remote.set("discord_channel", channel.id);
-        remote.set("plumbed", true);
-
-        const local = new MatrixRoom(roomId);
+        const remote = new RemoteStoreRoom(`discord_${channel.guild.id}_${channel.id}_bridged`, {
+            discord_channel: channel.id,
+            discord_guild: channel.guild.id,
+            discord_type: "text",
+            plumbed: true,
+        });
+
+        const local = new MatrixStoreRoom(roomId);
         return this.roomStore.linkRooms(local, remote);
     }
 
-    public async UnbridgeRoom(remoteRoom: RemoteRoom) {
-        return this.roomStore.removeEntriesByRemoteRoomId(remoteRoom.getId());
+    public async UnbridgeChannel(channel: Discord.TextChannel) {
+        const roomsRes = await this.roomStore.getEntriesByRemoteRoomData({
+            discord_channel: channel.id,
+            discord_guild: channel.guild.id,
+            plumbed: true,
+        });
+        if (roomsRes.length === 0) {
+            throw Error("Channel is not bridged");
+        }
+        const remoteRoom = roomsRes[0].remote as RemoteStoreRoom;
+        await this.roomStore.removeEntriesByRemoteRoomId(remoteRoom.getId());
     }
 
     public async AskBridgePermission(
diff --git a/src/util.ts b/src/util.ts
index 6edce06..87555ca 100644
--- a/src/util.ts
+++ b/src/util.ts
@@ -35,7 +35,7 @@ export interface ICommandAction {
     description?: string;
     help?: string;
     params: string[];
-    permission?: PERMISSIONTYPES;
+    permission?: PERMISSIONTYPES | PERMISSIONTYPES[];
     run(params: any): Promise<any>; // tslint:disable-line no-any
 }
 
diff --git a/test/test_discordcommandhandler.ts b/test/test_discordcommandhandler.ts
index f5851f4..a664395 100644
--- a/test/test_discordcommandhandler.ts
+++ b/test/test_discordcommandhandler.ts
@@ -31,6 +31,7 @@ let USERSJOINED = 0;
 let USERSKICKED = 0;
 let USERSBANNED = 0;
 let USERSUNBANNED = 0;
+let ROOMSUNBRIDGED = 0;
 let MESSAGESENT: any = {};
 let MARKED = -1;
 function createCH(opts: any = {}) {
@@ -38,6 +39,7 @@ function createCH(opts: any = {}) {
     USERSKICKED = 0;
     USERSBANNED = 0;
     USERSUNBANNED = 0;
+    ROOMSUNBRIDGED = 0;
     MESSAGESENT = {};
     MARKED = -1;
     const bridge = {
@@ -66,6 +68,9 @@ function createCH(opts: any = {}) {
                 MARKED = approved ? 1 : 0;
                 return approved;
             },
+            UnbridgeChannel: () => {
+                ROOMSUNBRIDGED++;
+            },
         },
     };
     const discordCommandHndlr = (Proxyquire("../src/discordcommandhandler", {
@@ -202,4 +207,21 @@ describe("DiscordCommandHandler", () => {
         await handler.Process(message);
         expect(MARKED).equals(0);
     });
+    it("handles !matrix unbridge", async () => {
+        const handler: any = createCH();
+        const channel = new MockChannel("123");
+        const guild = new MockGuild("456", [channel]);
+        channel.guild = guild;
+        const member: any = new MockMember("123456", "blah");
+        member.hasPermission = () => {
+            return true;
+        };
+        const message = {
+            channel,
+            content: "!matrix unbridge",
+            member,
+        };
+        await handler.Process(message);
+        expect(ROOMSUNBRIDGED).equals(1);
+    });
 });
diff --git a/test/test_matrixcommandhandler.ts b/test/test_matrixcommandhandler.ts
index 70359d7..2ca7aec 100644
--- a/test/test_matrixcommandhandler.ts
+++ b/test/test_matrixcommandhandler.ts
@@ -87,7 +87,7 @@ function createCH(opts: any = {}) {
                 throw new Error("Test failed matrix bridge");
             }
         },
-        UnbridgeRoom: async () => {
+        UnbridgeChannel: async () => {
             if (opts.failUnbridge) {
                 throw new Error("Test failed unbridge");
             }
@@ -202,24 +202,48 @@ describe("MatrixCommandHandler", () => {
         describe("!discord unbridge", () => {
             it("will unbridge", async () => {
                 const handler: any = createCH();
-                await handler.Process(createEvent("!discord unbridge"), createContext({data: {plumbed: true}}));
+                await handler.Process(createEvent("!discord unbridge"), createContext(
+                    {
+                        data: {
+                            discord_channel: "456",
+                            discord_guild: "123",
+                            plumbed: true,
+                        },
+                    },
+                ));
                 expect(MESSAGESENT.body).equals("This room has been unbridged");
             });
             it("will not unbridge if a link does not exist", async () => {
                 const handler: any = createCH();
-                const evt = await handler.Process(createEvent("!discord unbridge"), createContext());
+                await handler.Process(createEvent("!discord unbridge"), createContext());
                 expect(MESSAGESENT.body).equals("This room is not bridged.");
             });
             it("will not unbridge non-plumbed rooms", async () => {
                 const handler: any = createCH();
-                await handler.Process(createEvent("!discord unbridge"), createContext({data: {plumbed: false}}));
+                await handler.Process(createEvent("!discord unbridge"), createContext(
+                    {
+                        data: {
+                            discord_channel: "456",
+                            discord_guild: "123",
+                            plumbed: false,
+                        },
+                    },
+                ));
                 expect(MESSAGESENT.body).equals("This room cannot be unbridged.");
             });
             it("will show error if unbridge fails", async () => {
                 const handler: any = createCH({
                     failUnbridge: true,
                 });
-                await handler.Process(createEvent("!discord unbridge"), createContext({data: {plumbed: true}}));
+                await handler.Process(createEvent("!discord unbridge"), createContext(
+                    {
+                        data: {
+                            discord_channel: "456",
+                            discord_guild: "123",
+                            plumbed: true,
+                        },
+                    },
+                ));
                 expect(MESSAGESENT.body).to.contain("There was an error unbridging this room.");
             });
         });
diff --git a/test/test_provisioner.ts b/test/test_provisioner.ts
index 79f8f71..4f595e4 100644
--- a/test/test_provisioner.ts
+++ b/test/test_provisioner.ts
@@ -29,7 +29,7 @@ const TIMEOUT_MS = 1000;
 describe("Provisioner", () => {
     describe("AskBridgePermission", () => {
         it("should fail to bridge a room that timed out", async () => {
-            const p = new Provisioner({} as any);
+            const p = new Provisioner({} as any, {} as any);
             const startAt = Date.now();
             try {
                 await p.AskBridgePermission(
@@ -47,7 +47,7 @@ describe("Provisioner", () => {
             }
         });
         it("should fail to bridge a room that was declined", async () => {
-            const p = new Provisioner({} as any);
+            const p = new Provisioner({} as any, {} as any);
             const promise = p.AskBridgePermission(
                 new MockChannel("foo", "bar") as any,
                 "Mark",
@@ -63,7 +63,7 @@ describe("Provisioner", () => {
 
         });
         it("should bridge a room that was approved", async () => {
-            const p = new Provisioner({} as any);
+            const p = new Provisioner({} as any, {} as any);
             const promise = p.AskBridgePermission(
                 new MockChannel("foo", "bar") as any,
                 "Mark",
-- 
GitLab