diff --git a/src/matrixeventprocessor.ts b/src/matrixeventprocessor.ts index f0b5f8112206ae32efb845c20e0e700659b080c8..2ad0107368de6ebf72a54f592d7917a9a968eca2 100644 --- a/src/matrixeventprocessor.ts +++ b/src/matrixeventprocessor.ts @@ -8,7 +8,7 @@ import * as mime from "mime"; import { MatrixUser, Bridge } from "matrix-appservice-bridge"; import { Client as MatrixClient } from "matrix-js-sdk"; import { IMatrixEvent, IMatrixEventContent, IMatrixMessage } from "./matrixtypes"; -import { MatrixMessageProcessor } from "./matrixmessageprocessor"; +import { MatrixMessageProcessor, IMatrixMessageProcessorParams } from "./matrixmessageprocessor"; import { Log } from "./log"; const log = new Log("MatrixEventProcessor"); @@ -102,9 +102,18 @@ export class MatrixEventProcessor { log.warn(`Trying to fetch member state or profile for ${event.sender} failed`, err); } + const params = { + mxClient, + roomId: event.room_id, + userId: event.sender, + } as IMatrixMessageProcessorParams; + if (profile) { + params.displayname = profile.displayname; + } + let body: string = ""; if (event.type !== "m.sticker") { - body = await this.matrixMsgProcessor.FormatMessage(event.content as IMatrixMessage, channel.guild, profile); + body = await this.matrixMsgProcessor.FormatMessage(event.content as IMatrixMessage, channel.guild, params); } const messageEmbed = new Discord.RichEmbed(); diff --git a/src/matrixmessageprocessor.ts b/src/matrixmessageprocessor.ts index 1829609e1d7eaec76c3512085e8daeaa6c78f15b..fbb81d3729a63ef5df2727281b95a686dc773097 100644 --- a/src/matrixmessageprocessor.ts +++ b/src/matrixmessageprocessor.ts @@ -3,23 +3,33 @@ import { IMatrixMessage, IMatrixEvent } from "./matrixtypes"; import * as Parser from "node-html-parser"; import { Util } from "./util"; import { DiscordBot } from "./bot"; +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/#/"; +export interface IMatrixMessageProcessorParams { + displayname?: string; + mxClient?: MatrixClient; + roomId?: string; + userId?: string; +} + export class MatrixMessageProcessor { private guild: Discord.Guild; private listDepth: number = 0; private listBulletPoints: string[] = ["●", "○", "■", "‣"]; + private params?: IMatrixMessageProcessorParams; constructor(public bot: DiscordBot) { } public async FormatMessage( msg: IMatrixMessage, guild: Discord.Guild, - profile?: IMatrixEvent | null, + params?: IMatrixMessageProcessorParams, ): Promise<string> { this.guild = guild; this.listDepth = 0; + this.params = params; let reply = ""; if (msg.formatted_body) { // parser needs everything wrapped in html elements @@ -33,15 +43,15 @@ export class MatrixMessageProcessor { reply = await this.walkNode(parsed); reply = reply.replace(/\s*$/, ""); // trim off whitespace at end } else { - reply = this.escapeDiscord(msg.body); + reply = await this.escapeDiscord(msg.body); } if (msg.msgtype === "m.emote") { - if (profile && - profile.displayname && - profile.displayname.length >= MIN_NAME_LENGTH && - profile.displayname.length <= MAX_NAME_LENGTH) { - reply = `_${profile.displayname} ${reply}_`; + if (params && + params.displayname && + params.displayname.length >= MIN_NAME_LENGTH && + params.displayname.length <= MAX_NAME_LENGTH) { + reply = `_${params.displayname} ${reply}_`; } else { reply = `_${reply}_`; } @@ -49,12 +59,24 @@ export class MatrixMessageProcessor { return reply; } - private escapeDiscord(msg: string): string { + 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"); - msg = msg.replace(/@room/g, "@here"); + 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"); + } + } const escapeChars = ["\\", "*", "_", "~", "`"]; escapeChars.forEach((char) => { msg = msg.replace(new RegExp("\\" + char, "g"), "\\" + char); @@ -152,7 +174,7 @@ export class MatrixMessageProcessor { } if (!emoji) { - return this.escapeDiscord(name); + return await this.escapeDiscord(name); } return `<${emoji.animated ? "a" : ""}:${emoji.name}:${emoji.id}>`; } @@ -232,7 +254,7 @@ export class MatrixMessageProcessor { if ((node as Parser.TextNode).text === "\n") { return ""; } - return this.escapeDiscord((node as Parser.TextNode).text); + return await this.escapeDiscord((node as Parser.TextNode).text); } else if (node.nodeType === Parser.NodeType.ELEMENT_NODE) { const nodeHtml = node as Parser.HTMLElement; switch (nodeHtml.tagName) { diff --git a/src/matrixtypes.ts b/src/matrixtypes.ts index 22a876945404b4499cbae7b7c78a3eba65e85760..9e72460f93ae884d79ca4f4e37fd196813939661 100644 --- a/src/matrixtypes.ts +++ b/src/matrixtypes.ts @@ -23,6 +23,8 @@ export interface IMatrixEvent { content?: IMatrixEventContent; unsigned?: any; // tslint:disable-line no-any origin_server_ts?: number; + users?: any; // tslint:disable-line no-any + notifications?: any; // tslint:disable-line no-any } export interface IMatrixMessage { diff --git a/test/test_matrixmessageprocessor.ts b/test/test_matrixmessageprocessor.ts index 3434e628eb31fdd79658f070dca53027017fc5bb..2a48834be6978b87c0b5ca614a4f66213585f808 100644 --- a/test/test_matrixmessageprocessor.ts +++ b/test/test_matrixmessageprocessor.ts @@ -13,6 +13,23 @@ 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") { @@ -65,13 +82,6 @@ describe("MatrixMessageProcessor", () => { const result = await mp.FormatMessage(msg, guild as any); expect(result).is.equal("wow \\\\\\*this\\\\\\* is cool"); }); - it("Converts @room to @here", async () => { - const mp = new MatrixMessageProcessor(bot); - const guild = new MockGuild("1234"); - const msg = getPlainMessage("hey @room"); - const result = await mp.FormatMessage(msg, guild as any); - expect(result).is.equal("hey @here"); - }); }); describe("FormatMessage / formatted_body / simple", () => { it("leaves blank stuff untouched", async () => { @@ -259,13 +269,6 @@ code const result = await mp.FormatMessage(msg, guild as any); expect(result).is.equal("*yay?*"); }); - it("Converts @room to @here", async () => { - const mp = new MatrixMessageProcessor(bot); - const guild = new MockGuild("1234"); - const msg = getHtmlMessage("hey @room"); - const result = await mp.FormatMessage(msg, guild as any); - expect(result).is.equal("hey @here"); - }); }); describe("FormatMessage / formatted_body / emoji", () => { it("Inserts emoji by name", async () => { @@ -305,6 +308,76 @@ code expect(result).is.equal(""); }); }); + describe("FormatMessage / formatted_body / matrix", () => { + it("escapes @everyone", async () => { + const mp = new MatrixMessageProcessor(bot); + const guild = new MockGuild("1234"); + const msg = getPlainMessage("hey @everyone"); + const result = await mp.FormatMessage(msg, guild as any); + expect(result).is.equal("hey @\u200Beveryone"); + }); + it("escapes @here", async () => { + const mp = new MatrixMessageProcessor(bot); + const guild = new MockGuild("1234"); + const msg = getPlainMessage("hey @here"); + const result = await mp.FormatMessage(msg, guild as any); + expect(result).is.equal("hey @\u200Bhere"); + }); + it("converts @room to @here, if sufficient power", async () => { + const mp = new MatrixMessageProcessor(bot); + const guild = new MockGuild("1234"); + const msg = getPlainMessage("hey @room"); + const params = { + mxClient, + roomId: "!123456:localhost", + userId: "@power:localhost", + }; + const 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, + roomId: "!123456:localhost", + userId: "@nopower:localhost", + }; + const result = await mp.FormatMessage(msg, guild as any, params as any); + expect(result).is.equal("hey @room"); + }); + it("handles /me for normal names", async () => { + const mp = new MatrixMessageProcessor(bot); + const guild = new MockGuild("1234"); + const msg = getPlainMessage("floofs", "m.emote"); + const params = { + displayname: "fox", + }; + const result = await mp.FormatMessage(msg, guild as any, params as any); + expect(result).is.equal("_fox floofs_"); + }); + it("handles /me for short names", async () => { + const mp = new MatrixMessageProcessor(bot); + const guild = new MockGuild("1234"); + const msg = getPlainMessage("floofs", "m.emote"); + const params = { + displayname: "f", + }; + const result = await mp.FormatMessage(msg, guild as any, params as any); + expect(result).is.equal("_floofs_"); + }); + it("handles /me for long names", async () => { + const mp = new MatrixMessageProcessor(bot); + const guild = new MockGuild("1234"); + const msg = getPlainMessage("floofs", "m.emote"); + const params = { + displayname: "foxfoxfoxfoxfoxfoxfoxfoxfoxfoxfoxfox", + }; + const result = await mp.FormatMessage(msg, guild as any, params as any); + expect(result).is.equal("_floofs_"); + }); + }); describe("FormatMessage / formatted_body / blockquotes", () => { it("parses single blockquotes", async () => { const mp = new MatrixMessageProcessor(bot);