From f78c75a6b08666903c2c8509b48a8dcd9298b87f Mon Sep 17 00:00:00 2001 From: Sinharitik589 Date: Sat, 26 Mar 2022 21:19:40 +0530 Subject: [PATCH 1/2] bridging from discord to matrix --- src/bot.ts | 2 +- src/discordcommandhandler.ts | 58 +++++++++++++++++++++++++++++++++ src/matrixcommandhandler.ts | 36 +++++++++++++++++++++ src/provisioner.ts | 62 +++++++++++++++++++++++++++++++++++- 4 files changed, 156 insertions(+), 2 deletions(-) diff --git a/src/bot.ts b/src/bot.ts index 73c0345a..2192efb6 100644 --- a/src/bot.ts +++ b/src/bot.ts @@ -145,7 +145,7 @@ export class DiscordBot { this.mxEventProcessor = new MatrixEventProcessor( new MatrixEventProcessorOpts(config, bridge, this, store), ); - this.discordCommandHandler = new DiscordCommandHandler(bridge, this); + this.discordCommandHandler = new DiscordCommandHandler(bridge, this, store.roomStore, config); // init vars this.sentMessages = []; this.discordMessageQueue = {}; diff --git a/src/discordcommandhandler.ts b/src/discordcommandhandler.ts index f1d5fda0..579bca21 100644 --- a/src/discordcommandhandler.ts +++ b/src/discordcommandhandler.ts @@ -19,6 +19,8 @@ import * as Discord from "better-discord.js"; import { Util, ICommandActions, ICommandParameters, CommandPermissonCheck } from "./util"; import { Log } from "./log"; import { Appservice } from "matrix-bot-sdk"; +import { DbRoomStore } from "./db/roomstore"; +import { DiscordBridgeConfig } from "./config"; const log = new Log("DiscordCommandHandler"); @@ -26,6 +28,8 @@ export class DiscordCommandHandler { constructor( private bridge: Appservice, private discord: DiscordBot, + private roomStore: DbRoomStore, + private config:DiscordBridgeConfig ) { } public async Process(msg: Discord.Message) { @@ -94,6 +98,12 @@ export class DiscordCommandHandler { permission: ["MANAGE_WEBHOOKS", "MANAGE_CHANNELS"], run: async () => this.UnbridgeChannel(chan), }, + bridge: { + description:"Bridges this room to a Matrix channel", + params:["roomid"], + permission:["MANAGE_WEBHOOKS", "MANAGE_CHANNELS"], + run: async ({roomid}) => this.BridgeChannel(roomid,chan), + } }; const parameters: ICommandParameters = { @@ -105,6 +115,12 @@ export class DiscordCommandHandler { return mxUserId; }, }, + roomid: { + description:"The roomid of matrix room", + get: async (roomid) => { + return roomid; + } + } }; const permissionCheck: CommandPermissonCheck = async (permission: string|string[]) => { @@ -164,4 +180,46 @@ export class DiscordCommandHandler { "Please try again later or contact the bridge operator."; } } + + private async BridgeChannel(roomid:string,channel: Discord.TextChannel): Promise { + try { + const roomRes = await this.roomStore.getEntriesByRemoteRoomData({ + discord_channel: channel.id, + discord_guild: channel.guild.id, + plumbed: true, + }); + if(!roomid){ + return "" + } + if(roomRes.length > 0){ + return "This guild has already been bridged to a matrix room"; + } + if (await this.discord.Provisioner.RoomCountLimitReached(this.config.limits.roomCount)) { + log.info(`Room count limit (value: ${this.config.limits.roomCount}) reached: Rejecting command to bridge new matrix room ${roomid} to ${channel.guild.id}/${channel.id}`); + return `This bridge has reached its room limit of ${this.config.limits.roomCount}. Unbridge another room to allow for new connections.`; + } + try { + + log.info(`Bridging discord room ${channel.guild.id}/${channel.id} to ${roomid}`); + await channel.send( + "I'm asking permission from the channel administrators to make this bridge." + ); + + await this.discord.Provisioner.AskMatrixPermission(this.bridge,channel,roomid); + await this.discord.Provisioner.BridgeMatrixRoom(channel, roomid); + return "I have bridged this room to your channel"; + } catch (err) { + if (err.message === "Timed out waiting for a response from the Matrix owners." + || err.message === "The bridge has been declined by the matrix channel.") { + return err.message; + } + + log.error(`Error bridging ${roomid} to ${channel.guild.id}/${channel.id}`); + log.error(err); + return "There was a problem bridging that channel - has the guild owner approved the bridge?"; + } + } catch (err) { + return "" + } + } } diff --git a/src/matrixcommandhandler.ts b/src/matrixcommandhandler.ts index 9d2ae79b..ab9ce95d 100644 --- a/src/matrixcommandhandler.ts +++ b/src/matrixcommandhandler.ts @@ -143,6 +143,42 @@ export class MatrixCommandHandler { } }, }, + approve: { + description: "Deny a pending bridge request", + params: [], + permission: { + cat: "events", + level: PROVISIONING_DEFAULT_POWER_LEVEL, + selfService: true, + subcat: "m.room.power_levels", + }, + run: async () => { + if (await this.discord.Provisioner.MarkApprovedFromMatrix(event.room_id, true)) { + return "Thanks for your response! The matrix bridge has been approved."; + } else { + return "Thanks for your response, however" + + " it has arrived after the deadline - sorry!"; + } + }, + }, + deny: { + description: "Deny a pending bridge request", + params: [], + permission: { + cat: "events", + level: PROVISIONING_DEFAULT_POWER_LEVEL, + selfService: true, + subcat: "m.room.power_levels", + }, + run: async () => { + if (await this.discord.Provisioner.MarkApprovedFromMatrix(event.room_id, false)) { + return "Thanks for your response! The matrix bridge has been declined."; + } else { + return "Thanks for your response, however" + + " it has arrived after the deadline - sorry!"; + } + }, + }, }; /* diff --git a/src/provisioner.ts b/src/provisioner.ts index c1568af9..53fd6e09 100644 --- a/src/provisioner.ts +++ b/src/provisioner.ts @@ -18,6 +18,7 @@ import * as Discord from "better-discord.js"; import { DbRoomStore, RemoteStoreRoom, MatrixStoreRoom } from "./db/roomstore"; import { ChannelSyncroniser } from "./channelsyncroniser"; import { Log } from "./log"; +import { Appservice } from "matrix-bot-sdk"; const PERMISSION_REQUEST_TIMEOUT = 300000; // 5 minutes @@ -26,7 +27,7 @@ const log = new Log("Provisioner"); export class Provisioner { private pendingRequests: Map void> = new Map(); // [channelId]: resolver fn - + private matrixPendingRequests:Map = new Map(); constructor(private roomStore: DbRoomStore, private channelSync: ChannelSyncroniser) { } public async BridgeMatrixRoom(channel: Discord.TextChannel, roomId: string) { @@ -114,6 +115,50 @@ export class Provisioner { } + public async AskMatrixPermission( + bridge: Appservice, + channel: Discord.TextChannel, + roomid:string, + timeout: number = PERMISSION_REQUEST_TIMEOUT): Promise { + const channelId = `${channel.guild.id}/${channel.id}`; + + let responded = false; + let resolve: (msg: string) => void; + let reject: (err: Error) => void; + const deferP: Promise = new Promise((res, rej) => {resolve = res; reject = rej; }); + + const approveFn = (approved: boolean, expired = false) => { + if (responded) { + return; + } + + responded = true; + this.pendingRequests.delete(channelId); + this.matrixPendingRequests.delete(roomid); + if (approved) { + resolve("Approved"); + } else { + if (expired) { + reject(Error("Timed out waiting for a response from the Matrix owners.")); + } else { + reject(Error("The bridge has been declined by the Matrix room.")); + } + } + }; + + this.pendingRequests.set(channelId, approveFn); + this.matrixPendingRequests.set(roomid,channelId); + setTimeout(() => approveFn(false, true), timeout); + await bridge.botIntent.sendText( + roomid, + `${channel.client.user?.username} on discord server ${channel.guild.name} would like to bridge this channel. Someone with permission` + + " to manage webhooks please reply with `!discord approve` or `!discord deny` in the next 5 minutes.", + "m.notice", + ); + return await deferP; + + } + public HasPendingRequest(channel: Discord.TextChannel): boolean { const channelId = `${channel.guild.id}/${channel.id}`; return this.pendingRequests.has(channelId); @@ -138,4 +183,19 @@ export class Provisioner { this.pendingRequests.get(channelId)!(allow); return true; // replied, so true } + + public async MarkApprovedFromMatrix( + roomid: string, + allow: boolean, + ): Promise { + if (!this.matrixPendingRequests.has(roomid)) { + return false; // no change, so false + } + const channelId = this.matrixPendingRequests.get(roomid); + if(!channelId){ + return false; + } + this.pendingRequests.get(channelId)!(allow); + return true; // replied, so true + } } From d119a54f5b5ff83ec5077c17e93b683af1ec6e28 Mon Sep 17 00:00:00 2001 From: Sinharitik589 Date: Sat, 26 Mar 2022 21:32:41 +0530 Subject: [PATCH 2/2] bridging from discord to matrix --- src/bot.ts | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/bot.ts b/src/bot.ts index 2192efb6..c60835fd 100644 --- a/src/bot.ts +++ b/src/bot.ts @@ -188,7 +188,7 @@ export class DiscordBot { } public GetIntentFromDiscordMember(member: Discord.GuildMember | Discord.PartialUser | Discord.User, - webhookID: string|null = null): Intent { + webhookID: string|null = null): Intent { if (webhookID) { // webhookID and user IDs are the same, they are unique, so no need to prefix _webhook_ const name = member instanceof Discord.GuildMember ? member.user.username : member.username; @@ -606,8 +606,8 @@ export class DiscordBot { }); } } catch (err) { - // throw wrapError(err, Unstable.ForeignNetworkError, "Unable to create \"_matrix\" webhook"); - log.warn("Unable to create _matrix webook:", err); + // throw wrapError(err, Unstable.ForeignNetworkError, "Unable to create \"_matrix\" webhook"); + log.warn("Unable to create _matrix webook:", err); } } try { @@ -751,7 +751,7 @@ export class DiscordBot { } public async GetRoomIdsFromGuild( - guild: Discord.Guild, member?: Discord.GuildMember, useCache: boolean = true): Promise { + guild: Discord.Guild, member?: Discord.GuildMember, useCache: boolean = true): Promise { if (useCache) { const res = this.roomIdsForGuildCache.get(`${guild.id}:${member ? member.id : ""}`); @@ -825,7 +825,7 @@ export class DiscordBot { allow: ["SEND_MESSAGES", "VIEW_CHANNEL"], id: kickee.id, }], - `Unbanned.`, + `Unbanned.`, ); this.channelLock.set(botChannel.id); res = await botChannel.send( @@ -843,8 +843,8 @@ export class DiscordBot { const word = `${kickban === "ban" ? "banned" : "kicked"}`; this.channelLock.set(botChannel.id); res = await botChannel.send( - `${kickee} was ${word} from this channel by ${kicker}.` - + (reason ? ` Reason: ${reason}` : ""), + `${kickee} was ${word} from this channel by ${kicker}.${ + reason ? ` Reason: ${reason}` : ""}`, ) as Discord.Message; this.sentMessages.push(res.id); this.channelLock.release(botChannel.id); @@ -855,7 +855,7 @@ export class DiscordBot { deny: ["SEND_MESSAGES", "VIEW_CHANNEL"], id: kickee.id, }], - `Matrix user was ${word} by ${kicker}.`, + `Matrix user was ${word} by ${kicker}.`, ); if (kickban === "leave") { // Kicks will let the user back in after ~30 seconds. @@ -866,7 +866,7 @@ export class DiscordBot { allow: ["SEND_MESSAGES", "VIEW_CHANNEL"], id: kickee.id, }], - `Lifting kick since duration expired.`, + `Lifting kick since duration expired.`, ); }, this.config.room.kickFor); } @@ -885,7 +885,7 @@ export class DiscordBot { let addText = ""; if (embedSet.replyEmbed) { for (const line of embedSet.replyEmbed.description!.split("\n")) { - addText += "\n> " + line; + addText += `\n> ${ line}`; } } return embed.description += addText; @@ -938,8 +938,8 @@ export class DiscordBot { } private async SendMatrixMessage(matrixMsg: IDiscordMessageParserResult, chan: Discord.Channel, - guild: Discord.Guild, author: Discord.User, - msgID: string): Promise { + guild: Discord.Guild, author: Discord.User, + msgID: string): Promise { const rooms = await this.channelSync.GetRoomIdsFromChannel(chan); const intent = this.GetIntentFromDiscordMember(author); @@ -1008,11 +1008,11 @@ export class DiscordBot { // Test for webhooks if (msg.webhookID) { const webhook = (await chan.fetchWebhooks()) - .filter((h) => h.name === "_matrix").first(); + .filter((h) => h.name === "_matrix").first(); if (webhook && msg.webhookID === webhook.id) { // Filter out our own webhook messages. log.verbose("Not reflecting own webhook messages"); - // Filter out our own webhook messages. + // Filter out our own webhook messages. MetricPeg.get.requestOutcome(msg.id, true, "dropped"); return; }