diff --git a/src/matrixmessageprocessor.ts b/src/matrixmessageprocessor.ts index 3f5aa9f88c9ebfc8dd308e3228e6275dc3e6daca..124978d7689fb22620c6ac60d9bf76e8d7217b70 100644 --- a/src/matrixmessageprocessor.ts +++ b/src/matrixmessageprocessor.ts @@ -24,6 +24,7 @@ import { Client as MatrixClient } from "matrix-js-sdk"; const MIN_NAME_LENGTH = 2; const MAX_NAME_LENGTH = 32; const MATRIX_TO_LINK = "https://matrix.to/#/"; +const DEFAULT_ROOM_NOTIFY_POWER_LEVEL = 50; export interface IMatrixMessageProcessorParams { displayname?: string; @@ -75,24 +76,34 @@ export class MatrixMessageProcessor { return reply; } + private async canNotifyRoom() { + 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"); + + // 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; + } + private async escapeDiscord(msg: string): Promise<string> { // \u200B is the zero-width space --> they still look the same but don't mention msg = msg.replace(/@everyone/g, "@\u200Beveryone"); msg = msg.replace(/@here/g, "@\u200Bhere"); - if (msg.includes("@room") && this.params && this.params.mxClient && this.params.roomId && this.params.userId) { - // let's check for more complex logic if @room should be replaced - const res: IMatrixEvent = await this.params.mxClient.getStateEvent( - this.params.roomId, "m.room.power_levels"); - if ( - res && res.users - && res.users[this.params.userId] !== undefined - && res.notifications - && res.notifications.room !== undefined - && res.users[this.params.userId] >= res.notifications.room - ) { - msg = msg.replace(/@room/g, "@here"); - } + // Check the Matrix permissions to see if this user has the required + // power level to notify with @room; if so, replace it with @here. + if (msg.includes("@room") && await this.canNotifyRoom()) { + msg = msg.replace(/@room/g, "@here"); } const escapeChars = ["\\", "*", "_", "~", "`", "|"]; msg = msg.split(" ").map((s) => { diff --git a/test/test_matrixmessageprocessor.ts b/test/test_matrixmessageprocessor.ts index 66fced8ee3fe2e238c8ea451dd0aa92924981316..99626917d695d263732a2c80b0f32adcb2edddf8 100644 --- a/test/test_matrixmessageprocessor.ts +++ b/test/test_matrixmessageprocessor.ts @@ -29,23 +29,6 @@ import { MatrixMessageProcessor } from "../src/matrixmessageprocessor"; const expect = Chai.expect; -const mxClient = { - getStateEvent: async (roomId, stateType, _) => { - if (stateType === "m.room.power_levels") { - return { - notifications: { - room: 50, - }, - users: { - "@nopower:localhost": 0, - "@power:localhost": 100, - }, - }; - } - return null; - }, -}; - const bot = { GetEmojiByMxc: async (mxc: string): Promise<DbEmoji> => { if (mxc === "mxc://real_emote:localhost") { @@ -377,6 +360,47 @@ code }); }); describe("FormatMessage / formatted_body / matrix", () => { + /** + * Returns a mocked matrix client that mocks the m.room.power_levels + * event to test @room notifications. + * + * @param roomNotificationLevel the power level required to @room + * (if undefined, does not include notifications.room in + * m.room.power_levels) + */ + function getMxClient(roomNotificationLevel?: number) { + return { + getStateEvent: async (roomId, stateType, _) => { + if (stateType === "m.room.power_levels") { + return { + // Only include notifications.room when + // roomNotificationLevel is undefined + ...roomNotificationLevel !== undefined && { + notifications: { + room: roomNotificationLevel, + }, + }, + users: { + "@nopower:localhost": 0, + "@power:localhost": 100, + }, + }; + } + return null; + }, + }; + } + + /** + * Explicit power level required to notify @room. + * + * Essentially, we want to test two code paths - one where the explicit + * power level is set and one where it isn't, to see if the bridge can + * fall back to a default level (of 50). This is the explicit value we + * will set. + */ + const ROOM_NOTIFICATION_LEVEL = 50; + it("escapes @everyone", async () => { const mp = new MatrixMessageProcessor(bot); const guild = new MockGuild("1234"); @@ -395,24 +419,46 @@ code const mp = new MatrixMessageProcessor(bot); const guild = new MockGuild("1234"); const msg = getPlainMessage("hey @room"); - const params = { - mxClient, + let params = { + mxClient: getMxClient(ROOM_NOTIFICATION_LEVEL), roomId: "!123456:localhost", userId: "@power:localhost", }; - const result = await mp.FormatMessage(msg, guild as any, params as any); + let result = await mp.FormatMessage(msg, guild as any, params as any); + expect(result).is.equal("hey @here"); + + // Test again using an unset notifications.room value in + // m.room.power_levels, to ensure it falls back to a default + // requirement. + params = { + mxClient: getMxClient(), + roomId: "!123456:localhost", + userId: "@power:localhost", + }; + result = await mp.FormatMessage(msg, guild as any, params as any); expect(result).is.equal("hey @here"); }); it("ignores @room to @here conversion, if insufficient power", async () => { const mp = new MatrixMessageProcessor(bot); const guild = new MockGuild("1234"); const msg = getPlainMessage("hey @room"); - const params = { - mxClient, + let params = { + mxClient: getMxClient(ROOM_NOTIFICATION_LEVEL), roomId: "!123456:localhost", userId: "@nopower:localhost", }; - const result = await mp.FormatMessage(msg, guild as any, params as any); + let result = await mp.FormatMessage(msg, guild as any, params as any); + expect(result).is.equal("hey @room"); + + // Test again using an unset notifications.room value in + // m.room.power_levels, to ensure it falls back to a default + // requirement. + params = { + mxClient: getMxClient(), + roomId: "!123456:localhost", + userId: "@nopower:localhost", + }; + result = await mp.FormatMessage(msg, guild as any, params as any); expect(result).is.equal("hey @room"); }); it("handles /me for normal names", async () => {