diff --git a/src/m2d/converters/event-to-message.js b/src/m2d/converters/event-to-message.js index 7fd7b8a..3fe8776 100644 --- a/src/m2d/converters/event-to-message.js +++ b/src/m2d/converters/event-to-message.js @@ -136,10 +136,11 @@ turndownService.addRule("inlineLink", { if (node.getAttribute("data-message-id")) return `https://discord.com/channels/${node.getAttribute("data-guild-id")}/${node.getAttribute("data-channel-id")}/${node.getAttribute("data-message-id")}` if (node.getAttribute("data-channel-id")) return `<#${node.getAttribute("data-channel-id")}>` const href = node.getAttribute("href") + const suppressedHref = node.hasAttribute("data-suppress") ? "<" + href + ">" : href content = content.replace(/ @.*/, "") - if (href === content) return href + if (href === content) return suppressedHref if (decodeURIComponent(href).startsWith("https://matrix.to/#/@") && content[0] !== "@") content = "@" + content - return "[" + content + "](" + href + ")" + return "[" + content + "](" + suppressedHref + ")" } }) @@ -860,6 +861,21 @@ async function eventToMessage(event, guild, di) { pendingFiles.push({name: filename, buffer: Buffer.from(content, "utf8")}) } } + // Suppress link embeds + if (node.nodeType === 1 && node.tagName === "A") { + // Suppress if sender tried to add angle brackets + const inBody = event.content.body.indexOf(node.getAttribute("href")) + let shouldSuppress = inBody !== -1 && event.content.body[inBody-1] === "<" + if (!shouldSuppress && guild?.roles) { + // Suppress if regular users don't have permission + const permissions = dUtils.getPermissions([], guild.roles) + const canEmbedLinks = dUtils.hasPermission(permissions, DiscordTypes.PermissionFlagsBits.EmbedLinks) + shouldSuppress = !canEmbedLinks + } + if (shouldSuppress) { + node.setAttribute("data-suppress", "") + } + } await forEachNode(node.firstChild) } } @@ -901,7 +917,29 @@ 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\/[^<>\n )]*/, "<$&>") // Put < > around any surviving matrix.to links to hide the URL previews + + let offset = 0 + for (const match of [...content.matchAll(/\bhttps?:\/\/[^ )>]*/g)]) { + assert(typeof match.index === "number") + + // Respect sender's angle brackets + const alreadySuppressed = content[match.index-1+offset] === "<" && content[match.index+match.length+offset] === ">" + console.error(content, match.index-1+offset, content[match.index-1+offset]) + if (alreadySuppressed) continue + // Put < > around any surviving matrix.to links + let shouldSuppress = !!match[0].match(/^https?:\/\/matrix\.to\//) + if (!shouldSuppress && guild?.roles) { + // Suppress if regular users don't have permission + const permissions = dUtils.getPermissions([], guild.roles) + const canEmbedLinks = dUtils.hasPermission(permissions, DiscordTypes.PermissionFlagsBits.EmbedLinks) + shouldSuppress = !canEmbedLinks + } + + if (shouldSuppress) { + content = content.slice(0, match.index + offset) + "<" + match[0] + ">" + content.slice(match.index + match[0].length + offset) + offset += 2 + } + } const result = await checkWrittenMentions(content, event.sender, event.room_id, guild, di) if (result) { diff --git a/src/m2d/converters/event-to-message.test.js b/src/m2d/converters/event-to-message.test.js index 439e07f..6665e87 100644 --- a/src/m2d/converters/event-to-message.test.js +++ b/src/m2d/converters/event-to-message.test.js @@ -1,6 +1,7 @@ const assert = require("assert").strict const fs = require("fs") const {test} = require("supertape") +const DiscordTypes = require("discord-api-types/v10") const {eventToMessage} = require("./event-to-message") const {convertImageStream} = require("./emoji-sheet") const data = require("../../../test/data") @@ -302,6 +303,140 @@ test("event2message: markdown in link text does not attempt to be escaped becaus ) }) +test("event2message: links are escaped if the guild does not have embed links permission (formatted body)", async t => { + t.deepEqual( + await eventToMessage({ + content: { + body: "posting one of my favourite songs recently (starts at timestamp) https://youtu.be/RhV2X7WQMPA?t=364", + format: "org.matrix.custom.html", + formatted_body: `posting one of my favourite songs recently (starts at timestamp) https://youtu.be/RhV2X7WQMPA?t=364`, + msgtype: "m.text" + }, + event_id: "$g07oYSZFWBkxohNEfywldwgcWj1hbhDzQ1sBAKvqOOU", + origin_server_ts: 1688301929913, + room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe", + sender: "@cadence:cadence.moe", + type: "m.room.message", + }, { + id: "123", + roles: [{ + id: "123", + name: "@everyone", + permissions: DiscordTypes.PermissionFlagsBits.SendMessages + }] + }), + { + ensureJoined: [], + messagesToDelete: [], + messagesToEdit: [], + messagesToSend: [{ + username: "cadence [they]", + content: "posting one of my favourite songs recently (starts at timestamp) ", + avatar_url: undefined, + allowed_mentions: { + parse: ["users", "roles"] + } + }] + } + ) +}) + +test("event2message: links are escaped if the guild does not have embed links permission (plaintext body)", async t => { + t.deepEqual( + await eventToMessage({ + content: { + body: "posting one of my favourite songs recently (starts at timestamp) https://youtu.be/RhV2X7WQMPA?t=364", + msgtype: "m.text" + }, + event_id: "$g07oYSZFWBkxohNEfywldwgcWj1hbhDzQ1sBAKvqOOU", + origin_server_ts: 1688301929913, + room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe", + sender: "@cadence:cadence.moe", + type: "m.room.message", + }, { + id: "123", + roles: [{ + id: "123", + name: "@everyone", + permissions: DiscordTypes.PermissionFlagsBits.SendMessages + }] + }), + { + ensureJoined: [], + messagesToDelete: [], + messagesToEdit: [], + messagesToSend: [{ + username: "cadence [they]", + content: "posting one of my favourite songs recently (starts at timestamp) ", + avatar_url: undefined, + allowed_mentions: { + parse: ["users", "roles"] + } + }] + } + ) +}) + +test("event2message: links retain angle brackets (formatted body)", async t => { + t.deepEqual( + await eventToMessage({ + content: { + body: "posting one of my favourite songs recently (starts at timestamp) ", + format: "org.matrix.custom.html", + formatted_body: `posting one of my favourite songs recently (starts at timestamp) https://youtu.be/RhV2X7WQMPA?t=364`, + msgtype: "m.text" + }, + event_id: "$g07oYSZFWBkxohNEfywldwgcWj1hbhDzQ1sBAKvqOOU", + origin_server_ts: 1688301929913, + room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe", + sender: "@cadence:cadence.moe", + type: "m.room.message", + }), + { + ensureJoined: [], + messagesToDelete: [], + messagesToEdit: [], + messagesToSend: [{ + username: "cadence [they]", + content: "posting one of my favourite songs recently (starts at timestamp) ", + avatar_url: undefined, + allowed_mentions: { + parse: ["users", "roles"] + } + }] + } + ) +}) + +test("event2message: links retain angle brackets (plaintext body)", async t => { + t.deepEqual( + await eventToMessage({ + content: { + body: "posting one of my favourite songs recently (starts at timestamp) ", + msgtype: "m.text" + }, + event_id: "$g07oYSZFWBkxohNEfywldwgcWj1hbhDzQ1sBAKvqOOU", + origin_server_ts: 1688301929913, + room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe", + sender: "@cadence:cadence.moe", + type: "m.room.message", + }), + { + ensureJoined: [], + messagesToDelete: [], + messagesToEdit: [], + messagesToSend: [{ + username: "cadence [they]", + content: "posting one of my favourite songs recently (starts at timestamp) ", + avatar_url: undefined, + allowed_mentions: { + parse: ["users", "roles"] + } + }] + } + ) +}) + test("event2message: basic html is converted to markdown", async t => { t.deepEqual( await eventToMessage({