diff --git a/m2d/actions/channel-webhook.js b/m2d/actions/channel-webhook.js index 52b40956..d52f1746 100644 --- a/m2d/actions/channel-webhook.js +++ b/m2d/actions/channel-webhook.js @@ -57,7 +57,7 @@ async function withWebhook(channelID, callback) { */ async function sendMessageWithWebhook(channelID, data, threadID) { const result = await withWebhook(channelID, async webhook => { - return discord.snow.webhook.executeWebhook(webhook.id, webhook.token, data, {wait: true, thread_id: threadID, disableEveryone: true}) + return discord.snow.webhook.executeWebhook(webhook.id, webhook.token, data, {wait: true, thread_id: threadID}) }) return result } diff --git a/m2d/converters/event-to-message.js b/m2d/converters/event-to-message.js index 8da30042..87fadc87 100644 --- a/m2d/converters/event-to-message.js +++ b/m2d/converters/event-to-message.js @@ -384,19 +384,35 @@ async function handleRoomOrMessageLinks(input, di) { /** * @param {string} content + * @param {string} senderMxid + * @param {string} roomID * @param {DiscordTypes.APIGuild} guild * @param {{api: import("../../matrix/api"), snow: import("snowtransfer").SnowTransfer, fetch: import("node-fetch")["default"]}} di */ -async function checkWrittenMentions(content, guild, di) { +async function checkWrittenMentions(content, senderMxid, roomID, guild, di) { let writtenMentionMatch = content.match(/(?:^|[^"[<>/A-Za-z0-9])@([A-Za-z][A-Za-z0-9._\[\]\(\)-]+):?/d) // /d flag for indices requires node.js 16+ if (writtenMentionMatch) { - const results = await di.snow.guild.searchGuildMembers(guild.id, {query: writtenMentionMatch[1]}) - if (results[0]) { - assert(results[0].user) - return { - // @ts-ignore - typescript doesn't know about indices yet - content: content.slice(0, writtenMentionMatch.indices[1][0]-1) + `<@${results[0].user.id}>` + content.slice(writtenMentionMatch.indices[1][1]), - ensureJoined: results[0].user + if (writtenMentionMatch[1] === "room") { // convert @room to @everyone + const powerLevels = await di.api.getStateEvent(roomID, "m.room.power_levels", "") + const userPower = powerLevels.users?.[senderMxid] || 0 + if (userPower >= powerLevels.notifications?.room) { + return { + // @ts-ignore - typescript doesn't know about indices yet + content: content.slice(0, writtenMentionMatch.indices[1][0]-1) + `@everyone` + content.slice(writtenMentionMatch.indices[1][1]), + ensureJoined: [], + allowedMentionsParse: ["everyone"] + } + } + } else { + const results = await di.snow.guild.searchGuildMembers(guild.id, {query: writtenMentionMatch[1]}) + if (results[0]) { + assert(results[0].user) + return { + // @ts-ignore - typescript doesn't know about indices yet + content: content.slice(0, writtenMentionMatch.indices[1][0]-1) + `<@${results[0].user.id}>` + content.slice(writtenMentionMatch.indices[1][1]), + ensureJoined: [results[0].user], + allowedMentionsParse: [] + } } } } @@ -427,6 +443,7 @@ const attachmentEmojis = new Map([ async function eventToMessage(event, guild, di) { let displayName = event.sender let avatarURL = undefined + const allowedMentionsParse = ["users", "roles"] /** @type {string[]} */ let messageIDsToEdit = [] let replyLine = "" @@ -656,10 +673,11 @@ async function eventToMessage(event, guild, di) { for (; node; node = node.nextSibling) { // Check written mentions if (node.nodeType === 3 && node.nodeValue.includes("@") && !nodeIsChildOf(node, ["A", "CODE", "PRE"])) { - const result = await checkWrittenMentions(node.nodeValue, guild, di) + const result = await checkWrittenMentions(node.nodeValue, event.sender, event.room_id, guild, di) if (result) { node.nodeValue = result.content - ensureJoined.push(result.ensureJoined) + ensureJoined.push(...result.ensureJoined) + allowedMentionsParse.push(...result.allowedMentionsParse) } } // Check for incompatible backticks in code blocks @@ -727,10 +745,11 @@ async function eventToMessage(event, guild, di) { content = await handleRoomOrMessageLinks(content, di) // Replace matrix.to links with discord.com equivalents where possible content = content.replace(/\bhttps?:\/\/matrix\.to\/[^ )]*/, "<$&>") // Put < > around any surviving matrix.to links to hide the URL previews - const result = await checkWrittenMentions(content, guild, di) + const result = await checkWrittenMentions(content, event.sender, event.room_id, guild, di) if (result) { content = result.content - ensureJoined.push(result.ensureJoined) + ensureJoined.push(...result.ensureJoined) + allowedMentionsParse.push(...result.allowedMentionsParse) } // Markdown needs to be escaped, though take care not to escape the middle of links @@ -787,7 +806,7 @@ async function eventToMessage(event, guild, di) { const messages = chunks.map(content => ({ content, allowed_mentions: { - parse: ["users", "roles"] + parse: allowedMentionsParse }, username: displayNameShortened, avatar_url: avatarURL diff --git a/m2d/converters/event-to-message.test.js b/m2d/converters/event-to-message.test.js index 3366d18d..90da51c1 100644 --- a/m2d/converters/event-to-message.test.js +++ b/m2d/converters/event-to-message.test.js @@ -3786,6 +3786,174 @@ test("event2message: guessed @mentions work with other matrix bridge old users", ) }) +test("event2message: @room converts to @everyone and is allowed when the room doesn't restrict who can use it (plaintext body)", async t => { + let called = 0 + t.deepEqual( + await eventToMessage({ + type: "m.room.message", + sender: "@cadence:cadence.moe", + content: { + msgtype: "m.text", + body: "@room dinner's ready", + }, + room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe", + event_id: "$SiXetU9h9Dg-M9Frcw_C6ahnoXZ3QPZe3MVJR5tcB9A" + }, data.guild.general, { + api: { + getStateEvent(roomID, type, key) { + called++ + t.equal(roomID, "!kLRqKKUQXcibIMtOpl:cadence.moe") + t.equal(type, "m.room.power_levels") + t.equal(key, "") + return { + users: {}, + notifications: { + room: 0 + } + } + } + } + }), + { + messagesToDelete: [], + messagesToEdit: [], + messagesToSend: [{ + username: "cadence [they]", + content: "@everyone dinner's ready", + avatar_url: undefined, + allowed_mentions: { + parse: ["users", "roles", "everyone"] + } + }], + ensureJoined: [] + } + ) +}) + +test("event2message: @room converts to @everyone but is not allowed when the room restricts who can use it", async t => { + let called = 0 + t.deepEqual( + await eventToMessage({ + type: "m.room.message", + sender: "@cadence:cadence.moe", + content: { + msgtype: "m.text", + body: "wrong body", + format: "org.matrix.custom.html", + formatted_body: "@room dinner's ready" + }, + room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe", + event_id: "$SiXetU9h9Dg-M9Frcw_C6ahnoXZ3QPZe3MVJR5tcB9A" + }, data.guild.general, { + api: { + getStateEvent(roomID, type, key) { + called++ + t.equal(roomID, "!kLRqKKUQXcibIMtOpl:cadence.moe") + t.equal(type, "m.room.power_levels") + t.equal(key, "") + return { + users: {}, + notifications: { + room: 20 + } + } + } + } + }), + { + messagesToDelete: [], + messagesToEdit: [], + messagesToSend: [{ + username: "cadence [they]", + content: "@room dinner's ready", + avatar_url: undefined, + allowed_mentions: { + parse: ["users", "roles"] + } + }], + ensureJoined: [] + } + ) +}) + +test("event2message: @room converts to @everyone and is allowed if the user has sufficient power to use it", async t => { + let called = 0 + t.deepEqual( + await eventToMessage({ + type: "m.room.message", + sender: "@cadence:cadence.moe", + content: { + msgtype: "m.text", + body: "wrong body", + format: "org.matrix.custom.html", + formatted_body: "@room dinner's ready" + }, + room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe", + event_id: "$SiXetU9h9Dg-M9Frcw_C6ahnoXZ3QPZe3MVJR5tcB9A" + }, data.guild.general, { + api: { + getStateEvent(roomID, type, key) { + called++ + t.equal(roomID, "!kLRqKKUQXcibIMtOpl:cadence.moe") + t.equal(type, "m.room.power_levels") + t.equal(key, "") + return { + users: { + "@cadence:cadence.moe": 20 + }, + notifications: { + room: 20 + } + } + } + } + }), + { + messagesToDelete: [], + messagesToEdit: [], + messagesToSend: [{ + username: "cadence [they]", + content: "@everyone dinner's ready", + avatar_url: undefined, + allowed_mentions: { + parse: ["users", "roles", "everyone"] + } + }], + ensureJoined: [] + } + ) +}) + +test("event2message: @room in the middle of a link is not converted", async t => { + t.deepEqual( + await eventToMessage({ + type: "m.room.message", + sender: "@cadence:cadence.moe", + content: { + msgtype: "m.text", + body: "wrong body", + format: "org.matrix.custom.html", + formatted_body: `https://github.com/@room/repositories https://github.com/@room/repositories` + }, + room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe", + event_id: "$SiXetU9h9Dg-M9Frcw_C6ahnoXZ3QPZe3MVJR5tcB9A" + }), + { + messagesToDelete: [], + messagesToEdit: [], + messagesToSend: [{ + username: "cadence [they]", + content: "https://github.com/@room/repositories https://github.com/@room/repositories", + avatar_url: undefined, + allowed_mentions: { + parse: ["users", "roles"] + } + }], + ensureJoined: [] + } + ) +}) + slow()("event2message: unknown emoji at the end is reuploaded as a sprite sheet", async t => { const messages = await eventToMessage({ type: "m.room.message",