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