diff --git a/src/discordcommandhandler.ts b/src/discordcommandhandler.ts index 09d2fa48978fc694066afeb63933a7b990221235..d3c00a319eef1f6c9d90d52e75fb7f5deefee60a 100644 --- a/src/discordcommandhandler.ts +++ b/src/discordcommandhandler.ts @@ -1,6 +1,6 @@ import { DiscordBot } from "./bot"; import * as Discord from "discord.js"; -import { Util, ICommandActions, ICommandParameters } from "./util"; +import { Util, ICommandActions, ICommandParameters, ICommandPermissonCheck } from "./util"; import { Bridge } from "matrix-appservice-bridge"; export class DiscordCommandHandler { constructor( diff --git a/src/matrixcommandhandler.ts b/src/matrixcommandhandler.ts index ea1e8e72912265a53516698450ee5595d7be39e2..66ddabb9fbf6853663b5ec354e1688fcc91e2274 100644 --- a/src/matrixcommandhandler.ts +++ b/src/matrixcommandhandler.ts @@ -47,25 +47,84 @@ export class MatrixCommandHandler { const actions: ICommandActions = { bridge: { description: "Bridges this room to a Discord channel", + // tslint:disable prefer-template + help: "How to bridge a Discord guild:\n" + + "1. Invite the bot to your Discord guild using this link: " + Util.GetBotLink(this.config) + "\n" + + "2. Invite me to the matrix room you'd like to bridge\n" + + "3. Open the Discord channel you'd like to bridge in a web browser\n" + + "4. In the matrix room, send the message `!discord bridge <guild id> <channel id>` " + + "(without the backticks)\n" + + " Note: The Guild ID and Channel ID can be retrieved from the URL in your web browser.\n" + + " The URL is formatted as https://discordapp.com/channels/GUILD_ID/CHANNEL_ID\n" + + "5. Enjoy your new bridge!", + // tslint:enable prefer-template params: ["guildid", "channelId"], permission: { + cat: "events", level: PROVISIONING_DEFAULT_POWER_LEVEL, selfService: true, + subcat: "m.room.power_levels", }, run: async ({guildId, channelId}) => { // TODO: parse guildId/channelId + if (context.rooms.remote) { + return "This room is already bridged to a Discord guild"; + } + if (!guildId || !channelId) { + return "Invalid syntax. For more information try `!discord help bridge`"; + } + try { + const discordResult = await this.discord.LookupRoom(guildId, channelId); + const channel = discordResult.channel as Discord.TextChannel; + + log.info(`Bridging matrix room ${event.room_id} to ${guildId}/${channelId}`); + this.bridge.getIntent().sendMessage(event.room_id, { + body: "I'm asking permission from the guild administrators to make this bridge.", + msgtype: "m.notice", + }); + + await this.provisioner.AskBridgePermission(channel, event.sender); + await this.provisioner.BridgeMatrixRoom(channel, event.room_id); + return "I have bridged this room to your channel"; + } catch (err) { + if (err.message === "Timed out waiting for a response from the Discord owners" + || err.message === "The bridge has been declined by the Discord guild") { + return err.message; + } + + log.error(`Error bridging ${event.room_id} to ${guildId}/${channelId}`); + log.error(err); + return "There was a problem bridging that channel - has the guild owner approved the bridge?"; + } }, }, unbridge: { description: "Unbridges a Discord channel from this room", params: [], permission: { + cat: "events", level: PROVISIONING_DEFAULT_POWER_LEVEL, selfService: true, + subcat: "m.room.power_levels", }, run: async () => { - + const remoteRoom = context.rooms.remote; + if (!remoteRoom) { + return "This room is not bridged."; + } + if (!remoteRoom.data.plumbed) { + return "This room cannot be unbridged."; + } + try { + await this.provisioner.UnbridgeRoom(remoteRoom); + return "This room has been unbridged"; + } catch (err) { + log.error("Error while unbridging room " + event.room_id); + log.error(err); + return "There was an error unbridging this room. " + + "Please try again later or contact the bridge operator."; + } } }, }; @@ -83,21 +142,18 @@ export class MatrixCommandHandler { if (permission.selfService && !this.config.bridge.enableSelfServiceBridging) { return false; } - const plEvent = await this.bridge.getIntent().getClient() - .getStateEvent(event.room_id, "m.room.power_levels", ""); - let userLevel = PROVISIONING_DEFAULT_USER_POWER_LEVEL; - let requiredLevel = permission.level; - if (plEvent && plEvent.state_default) { - requiredLevel = plEvent.state_default; - } - if (plEvent && plEvent.users_default) { - userLevel = plEvent.users_default; - } - if (plEvent && plEvent.users && plEvent.users[event.sender]) { - userLevel = plEvent.users[event.sender]; - } + return await Util.CheckMatrixPermission( + this.bridge.getIntent().getClient(), + event.sender, + event.room_id, + permission.level, + permission.cat, + permission.subcat, + ); }; + + if (!this.config.bridge.enableSelfServiceBridging) { // We can do this here because the only commands we support are self-service bridging return this.bridge.getIntent().sendMessage(event.room_id, { @@ -106,160 +162,12 @@ export class MatrixCommandHandler { }); } - // Check to make sure the user has permission to do anything in the room. We can do this here - // because the only commands we support are self-service commands (which therefore require some - // level of permissions) - const plEvent = await this.bridge.getIntent().getClient() - .getStateEvent(event.room_id, "m.room.power_levels", ""); - let userLevel = PROVISIONING_DEFAULT_USER_POWER_LEVEL; - let requiredLevel = PROVISIONING_DEFAULT_POWER_LEVEL; - if (plEvent && plEvent.state_default) { - requiredLevel = plEvent.state_default; - } - if (plEvent && plEvent.users_default) { - userLevel = plEvent.users_default; - } - if (plEvent && plEvent.users && plEvent.users[event.sender]) { - userLevel = plEvent.users[event.sender]; - } - - if (userLevel < requiredLevel) { - return this.bridge.getIntent().sendMessage(event.room_id, { - body: "You do not have the required power level in this room to create a bridge to a Discord channel.", - msgtype: "m.notice", - }); - } - - const {command, args} = Util.MsgToArgs(event.content!.body as string, "!discord"); - - if (command === "help" && args[0] === "bridge") { - const link = Util.GetBotLink(this.config); - // tslint:disable prefer-template - return this.bridge.getIntent().sendMessage(event.room_id, { - body: "How to bridge a Discord guild:\n" + - "1. Invite the bot to your Discord guild using this link: " + link + "\n" + - "2. Invite me to the matrix room you'd like to bridge\n" + - "3. Open the Discord channel you'd like to bridge in a web browser\n" + - "4. In the matrix room, send the message `!discord bridge <guild id> <channel id>` " + - "(without the backticks)\n" + - " Note: The Guild ID and Channel ID can be retrieved from the URL in your web browser.\n" + - " The URL is formatted as https://discordapp.com/channels/GUILD_ID/CHANNEL_ID\n" + - "5. Enjoy your new bridge!", - msgtype: "m.notice", - }); - // tslint:enable prefer-template - } else if (command === "bridge") { - if (context.rooms.remote) { - return this.bridge.getIntent().sendMessage(event.room_id, { - body: "This room is already bridged to a Discord guild.", - msgtype: "m.notice", - }); - } - - const MAXARGS = 2; - if (args.length > MAXARGS || args.length < 1) { - return this.bridge.getIntent().sendMessage(event.room_id, { - body: "Invalid syntax. For more information try !discord help bridge", - msgtype: "m.notice", - }); - } - - let guildId: string; - let channelId: string; - - const AMOUNT_OF_IDS_DISCORD_IDENTIFIES_ROOMS_BY = 2; - - if (args.length === AMOUNT_OF_IDS_DISCORD_IDENTIFIES_ROOMS_BY) { // "x y" syntax - guildId = args[0]; - channelId = args[1]; - } else if (args.length === 1 && args[0].includes("/")) { // "x/y" syntax - const split = args[0].split("/"); - guildId = split[0]; - channelId = split[1]; - } else { - return this.bridge.getIntent().sendMessage(event.room_id, { - body: "Invalid syntax: See `!discord help`", - formatted_body: "Invalid syntax: See <code>!discord help</code>", - msgtype: "m.notice", - }); - } - - try { - const discordResult = await this.discord.LookupRoom(guildId, channelId); - const channel = discordResult.channel as Discord.TextChannel; - - log.info(`Bridging matrix room ${event.room_id} to ${guildId}/${channelId}`); - this.bridge.getIntent().sendMessage(event.room_id, { - body: "I'm asking permission from the guild administrators to make this bridge.", - msgtype: "m.notice", - }); - - await this.provisioner.AskBridgePermission(channel, event.sender); - await this.provisioner.BridgeMatrixRoom(channel, event.room_id); - return this.bridge.getIntent().sendMessage(event.room_id, { - body: "I have bridged this room to your channel", - msgtype: "m.notice", - }); - } catch (err) { - if (err.message === "Timed out waiting for a response from the Discord owners" - || err.message === "The bridge has been declined by the Discord guild") { - return this.bridge.getIntent().sendMessage(event.room_id, { - body: err.message, - msgtype: "m.notice", - }); - } - - log.error(`Error bridging ${event.room_id} to ${guildId}/${channelId}`); - log.error(err); - return this.bridge.getIntent().sendMessage(event.room_id, { - body: "There was a problem bridging that channel - has the guild owner approved the bridge?", - msgtype: "m.notice", - }); - } - } else if (command === "unbridge") { - const remoteRoom = context.rooms.remote; - - if (!remoteRoom) { - return this.bridge.getIntent().sendMessage(event.room_id, { - body: "This room is not bridged.", - msgtype: "m.notice", - }); - } + const reply = await Util.ParseCommand("!discord", event.content!.body!, actions, parameters, permissionCheck); - if (!remoteRoom.data.plumbed) { - return this.bridge.getIntent().sendMessage(event.room_id, { - body: "This room cannot be unbridged.", - msgtype: "m.notice", - }); - } - - try { - await this.provisioner.UnbridgeRoom(remoteRoom); - return this.bridge.getIntent().sendMessage(event.room_id, { - body: "This room has been unbridged", - msgtype: "m.notice", - }); - } catch (err) { - log.error("Error while unbridging room " + event.room_id); - log.error(err); - return this.bridge.getIntent().sendMessage(event.room_id, { - body: "There was an error unbridging this room. " + - "Please try again later or contact the bridge operator.", - msgtype: "m.notice", - }); - } - } else if (command === "help") { - // Unknown command or no command given to get help on, so we'll just give them the help - // tslint:disable prefer-template - return this.bridge.getIntent().sendMessage(event.room_id, { - body: "Available commands:\n" + - "!discord bridge <guild id> <channel id> - Bridges this room to a Discord channel\n" + - "!discord unbridge - Unbridges a Discord channel from this room\n" + - "!discord help <command> - Help menu for another command. Eg: !discord help bridge\n", - msgtype: "m.notice", - }); - // tslint:enable prefer-template - } + await this.bridge.getIntent().sendMessage(event.room_id, { + body: reply, + msgtype: "m.notice", + }); } private async isBotInRoom(roomId: string): Promise<boolean> { diff --git a/src/matrixmessageprocessor.ts b/src/matrixmessageprocessor.ts index 91d7e5ad543f0b2bd4caa48c94972cc95cecd034..c455aac924af09f6cd5f94f1aecfab1df512202c 100644 --- a/src/matrixmessageprocessor.ts +++ b/src/matrixmessageprocessor.ts @@ -80,20 +80,14 @@ export class MatrixMessageProcessor { if (!this.params || !this.params.mxClient || !this.params.roomId || !this.params.userId) { return false; } - - const res: IMatrixEvent = await this.params.mxClient.getStateEvent( - this.params.roomId, "m.room.power_levels"); - - // TODO: utilize default values correctly - // Some rooms may not have notifications.room set if the value hasn't - // been changed from the default. If so, use our hardcoded power level. - const requiredPowerLevel = res && res.notifications && res.notifications.room - ? res.notifications.room - : DEFAULT_ROOM_NOTIFY_POWER_LEVEL; - - return res && res.users - && res.users[this.params.userId] !== undefined - && res.users[this.params.userId] >= requiredPowerLevel; + return await Util.CheckMatrixPermission( + this.params.mxClient, + this.params.userId, + this.params.roomId, + DEFAULT_ROOM_NOTIFY_POWER_LEVEL, + "notifications", + "room", + ); } private async escapeDiscord(msg: string): Promise<string> { diff --git a/src/util.ts b/src/util.ts index 1d308bb04e26cf368627e01253e5a87a38b88f30..c5ed597bc17ded67814efee8dda3e0cfbc006fc1 100644 --- a/src/util.ts +++ b/src/util.ts @@ -21,6 +21,8 @@ import { Buffer } from "buffer"; import * as mime from "mime"; import { Permissions } from "discord.js"; import { DiscordBridgeConfig } from "./config"; +import { Client as MatrixClient } from "matrix-js-sdk"; +import { IMatrixEvent } from "./matrixtypes"; const HTTP_OK = 200; @@ -43,7 +45,7 @@ export interface ICommandActions { export interface ICommandParameter { description?: string; - get(param: string)?: Promise<any>; // tslint:disable-line no-any + get?(param: string): Promise<any>; // tslint:disable-line no-any } export interface ICommandParameters { @@ -51,7 +53,7 @@ export interface ICommandParameters { } export interface ICommandPermissonCheck { - (permission: PERMISSIONTYPES): Promise<bool>; + (permission: PERMISSIONTYPES): Promise<boolean>; } export interface IPatternMap { @@ -240,8 +242,8 @@ export class Util { actions: ICommandActions, parameters: ICommandParameters, args: string[], - permissionCheck: ICommandPermissonCheck?, - ): string { + permissionCheck?: ICommandPermissonCheck, + ): Promise<string> { let reply = ""; if (args[0]) { const actionKey = args[0]; @@ -285,10 +287,10 @@ export class Util { public static async ParseCommand( prefix: string, msg: string, - actions: ICommandAction[], + actions: ICommandActions, parameters: ICommandParameters, - permissionCheck: ICommandPermissonCheck?, - ): string { + permissionCheck?: ICommandPermissonCheck, + ): Promise<string> { const {command, args} = Util.MsgToArgs(msg, prefix); if (command === "help") { @@ -309,8 +311,8 @@ export class Util { const params = {}; let i = 0; for (const param of action.params) { - if (parameters[param].get) { - params[param] = await parameters[param].get(args[i]); + if (parameters[param].get !== undefined) { + params[param] = await parameters[param].get!(args[i]); } else { params[param] = args[i]; } @@ -366,6 +368,35 @@ export class Util { } return str; } + + public static async CheckMatrixPermission( + mxClient: MatrixClient, + userId: string, + roomId: string, + defaultLevel: number, + cat: string, + subcat?: string, + ) { + const res: IMatrixEvent = await mxClient.getStateEvent(roomId, "m.room.power_levels"); + let requiredLevel = defaultLevel; + if (res && (res[cat] || !subcat)) { + if (subcat) { + if (res[cat][subcat] !== undefined) { + requiredLevel = res[cat][subcat]; + } + } else { + if (res[cat] !== undefined) { + requiredLevel = res[cat]; + } + } + } + + let haveLevel = 0; + if (res && res.users && res.users[userId] !== undefined) { + haveLevel = res.users[userId]; + } + return haveLevel >= requiredLevel; + } } interface IUploadResult {