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({