From 2df7c665cb67a40f8b5573f53fd6729a762a9543 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Thu, 30 Nov 2023 16:27:40 +1300 Subject: [PATCH] m->d: Message links are now guessed when unknown --- d2m/converters/message-to-event.js | 9 +- m2d/converters/event-to-message.js | 59 ++++++++++--- m2d/converters/event-to-message.test.js | 108 +++++++++++++++++++++++- test/test.js | 7 +- 4 files changed, 164 insertions(+), 19 deletions(-) diff --git a/d2m/converters/message-to-event.js b/d2m/converters/message-to-event.js index 92bc241..2026e07 100644 --- a/d2m/converters/message-to-event.js +++ b/d2m/converters/message-to-event.js @@ -269,12 +269,12 @@ async function messageToEvent(message, guild, options = {}, di) { */ async function transformContentMessageLinks(content) { let offset = 0 - for (const match of [...content.matchAll(/https:\/\/(?:ptb\.|canary\.|www\.)?discord(?:app)?\.com\/channels\/([0-9]+)\/([0-9]+)\/([0-9]+)/g)]) { + for (const match of [...content.matchAll(/https:\/\/(?:ptb\.|canary\.|www\.)?discord(?:app)?\.com\/channels\/[0-9]+\/([0-9]+)\/([0-9]+)/g)]) { assert(typeof match.index === "number") - const channelID = match[2] - const messageID = match[3] - const roomID = select("channel_room", "room_id", {channel_id: channelID}).pluck().get() + const [_, channelID, messageID] = match let result + + const roomID = select("channel_room", "room_id", {channel_id: channelID}).pluck().get() if (roomID) { const eventID = select("event_message", "event_id", {message_id: messageID}).pluck().get() if (eventID && roomID) { @@ -287,6 +287,7 @@ async function messageToEvent(message, guild, options = {}, di) { } else { result = `${match[0]} [event is from another server]` } + content = content.slice(0, match.index + offset) + result + content.slice(match.index + match[0].length + offset) offset += result.length - match[0].length } diff --git a/m2d/converters/event-to-message.js b/m2d/converters/event-to-message.js index ec84e07..f7053cd 100644 --- a/m2d/converters/event-to-message.js +++ b/m2d/converters/event-to-message.js @@ -11,7 +11,9 @@ const entities = require("entities") const passthrough = require("../../passthrough") const {sync, db, discord, select, from} = passthrough /** @type {import("../converters/utils")} */ -const utils = sync.require("../converters/utils") +const mxUtils = sync.require("../converters/utils") +/** @type {import("../../discord/utils")} */ +const dUtils = sync.require("../../discord/utils") /** @type {import("./emoji-sheet")} */ const emojiSheet = sync.require("./emoji-sheet") @@ -102,6 +104,7 @@ turndownService.addRule("inlineLink", { replacement: function (content, node) { if (node.getAttribute("data-user-id")) return `<@${node.getAttribute("data-user-id")}>` + 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") let brackets = ["", ""] @@ -162,7 +165,7 @@ turndownService.addRule("emoji", { return `<::>` } else { // We prefer not to upload this as a sprite sheet because the emoji is not at the end of the message, it is in the middle. - return `[${node.getAttribute("title")}](${utils.getPublicUrlForMxc(mxcUrl)})` + return `[${node.getAttribute("title")}](${mxUtils.getPublicUrlForMxc(mxcUrl)})` } } }) @@ -276,7 +279,7 @@ async function eventToMessage(event, guild, di) { // Try to extract an accurate display name and avatar URL from the member event const member = await getMemberFromCacheOrHomeserver(event.room_id, event.sender, di?.api) if (member.displayname) displayName = member.displayname - if (member.avatar_url) avatarURL = utils.getPublicUrlForMxc(member.avatar_url) || undefined + if (member.avatar_url) avatarURL = mxUtils.getPublicUrlForMxc(member.avatar_url) || undefined // If the display name is too long to be put into the webhook (80 characters is the maximum), // put the excess characters into displayNameRunoff, later to be put at the top of the message let [displayNameShortened, displayNameRunoff] = splitDisplayName(displayName) @@ -390,7 +393,7 @@ async function eventToMessage(event, guild, di) { // Handling mentions of Discord users input = input.replace(/("https:\/\/matrix.to\/#\/(@[^"]+)")>/g, (whole, attributeValue, mxid) => { - if (utils.eventSenderIsFromDiscord(mxid)) { + if (mxUtils.eventSenderIsFromDiscord(mxid)) { // Handle mention of an OOYE sim user by their mxid const userID = select("sim", "user_id", {mxid: mxid}).pluck().get() if (!userID) return whole @@ -405,12 +408,42 @@ async function eventToMessage(event, guild, di) { } }) - // Handling mentions of Discord rooms - input = input.replace(/("https:\/\/matrix.to\/#\/(![^"]+)")>/g, (whole, attributeValue, roomID) => { + // Handling mentions of rooms and room-messages + let offset = 0 + for (const match of [...input.matchAll(/("https:\/\/matrix.to\/#\/(![^"/?]+)(?:\/(\$[^"/?]+))?(?:\?[^"]*)?")>/g)]) { + assert(typeof match.index === "number") + const [_, attributeValue, roomID, eventID] = match + let result + + // Don't process links that are part of the reply fallback, they'll be removed entirely by turndown + if (input.slice(match.index + match[0].length + offset).startsWith("In reply to")) continue + const channelID = select("channel_room", "channel_id", {room_id: roomID}).pluck().get() - if (!channelID) return whole - return `${attributeValue} data-channel-id="${channelID}">` - }) + if (!channelID) continue + if (!eventID) { + // 1: It's a room link, so <#link> to the channel + result = `${attributeValue} data-channel-id="${channelID}">` + } else { + // Linking to a particular event with a discord.com/channels/guildID/channelID/messageID link + // Need to know the guildID and messageID + const guildID = discord.channels.get(channelID)?.["guild_id"] + if (!guildID) continue + const messageID = select("event_message", "message_id", {event_id: eventID}).pluck().get() + if (messageID) { + // 2: Linking to a known event + result = `${attributeValue} data-channel-id="${channelID}" data-guild-id="${guildID}" data-message-id="${messageID}">` + } else { + // 3: Linking to an unknown event that OOYE didn't originally bridge - we can guess messageID from the timestamp + const originalEvent = await di.api.getEvent(roomID, eventID) + if (!originalEvent) continue + const guessedMessageID = dUtils.timestampToSnowflakeInexact(originalEvent.origin_server_ts) + result = `${attributeValue} data-channel-id="${channelID}" data-guild-id="${guildID}" data-message-id="${guessedMessageID}">` + } + } + + input = input.slice(0, match.index + offset) + result + input.slice(match.index + match[0].length + offset) + offset += result.length - match[0].length + } // Stripping colons after mentions input = input.replace(/( data-user-id.*?<\/a>):?/g, "$1") @@ -430,7 +463,7 @@ async function eventToMessage(event, guild, di) { beforeTag = beforeTag || "" afterContext = afterContext || "" afterTag = afterTag || "" - if (!utils.BLOCK_ELEMENTS.includes(beforeTag.toUpperCase()) && !utils.BLOCK_ELEMENTS.includes(afterTag.toUpperCase())) { + if (!mxUtils.BLOCK_ELEMENTS.includes(beforeTag.toUpperCase()) && !mxUtils.BLOCK_ELEMENTS.includes(afterTag.toUpperCase())) { return beforeContext + "
" + afterContext } else { return whole @@ -480,13 +513,13 @@ async function eventToMessage(event, guild, di) { const filename = event.content.body if ("url" in event.content) { // Unencrypted - const url = utils.getPublicUrlForMxc(event.content.url) + const url = mxUtils.getPublicUrlForMxc(event.content.url) assert(url) attachments.push({id: "0", filename}) pendingFiles.push({name: filename, url}) } else { // Encrypted - const url = utils.getPublicUrlForMxc(event.content.file.url) + const url = mxUtils.getPublicUrlForMxc(event.content.file.url) assert(url) assert.equal(event.content.file.key.alg, "A256CTR") attachments.push({id: "0", filename}) @@ -494,7 +527,7 @@ async function eventToMessage(event, guild, di) { } } else if (event.type === "m.sticker") { content = "" - const url = utils.getPublicUrlForMxc(event.content.url) + const url = mxUtils.getPublicUrlForMxc(event.content.url) assert(url) let filename = event.content.body if (event.type === "m.sticker") { diff --git a/m2d/converters/event-to-message.test.js b/m2d/converters/event-to-message.test.js index 6dbe112..9d2c70c 100644 --- a/m2d/converters/event-to-message.test.js +++ b/m2d/converters/event-to-message.test.js @@ -1701,7 +1701,7 @@ test("event2message: mentioning bridged rooms works", async t => { msgtype: "m.text", body: "wrong body", format: "org.matrix.custom.html", - formatted_body: `I'm just worm-form testing channel mentions` + formatted_body: `I'm just worm-farm testing channel mentions` }, event_id: "$g07oYSZFWBkxohNEfywldwgcWj1hbhDzQ1sBAKvqOOU", origin_server_ts: 1688301929913, @@ -1725,6 +1725,112 @@ test("event2message: mentioning bridged rooms works", async t => { ) }) +test("event2message: mentioning known bridged events works", async t => { + t.deepEqual( + await eventToMessage({ + content: { + msgtype: "m.text", + body: "wrong body", + format: "org.matrix.custom.html", + formatted_body: `it was uploaded earlier in amanda-spam` + }, + event_id: "$g07oYSZFWBkxohNEfywldwgcWj1hbhDzQ1sBAKvqOOU", + origin_server_ts: 1688301929913, + room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe", + sender: "@cadence:cadence.moe", + type: "m.room.message", + unsigned: { + age: 405299 + } + }), + { + ensureJoined: [], + messagesToDelete: [], + messagesToEdit: [], + messagesToSend: [{ + username: "cadence [they]", + content: "it was uploaded earlier in https://discord.com/channels/497159726455455754/497161350934560778/1141619794500649020", + avatar_url: undefined + }] + } + ) +}) + +test("event2message: mentioning unknown bridged events works", async t => { + let called = 0 + t.deepEqual( + await eventToMessage({ + content: { + msgtype: "m.text", + body: "wrong body", + format: "org.matrix.custom.html", + formatted_body: `it was uploaded years ago in amanda-spam` + }, + event_id: "$g07oYSZFWBkxohNEfywldwgcWj1hbhDzQ1sBAKvqOOU", + origin_server_ts: 1688301929913, + room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe", + sender: "@cadence:cadence.moe", + type: "m.room.message", + unsigned: { + age: 405299 + } + }, {}, { + api: { + async getEvent(roomID, eventID) { + called++ + t.equal(roomID, "!CzvdIdUQXgUjDVKxeU:cadence.moe") + t.equal(eventID, "$zpzx6ABetMl8BrpsFbdZ7AefVU1Y_-t97bJRJM2JyW0") + return { + origin_server_ts: 1599813121000 + } + } + } + }), + { + ensureJoined: [], + messagesToDelete: [], + messagesToEdit: [], + messagesToSend: [{ + username: "cadence [they]", + content: "it was uploaded years ago in https://discord.com/channels/497159726455455754/497161350934560778/753895613661184000", + avatar_url: undefined + }] + } + ) + t.equal(called, 1, "getEvent should be called once") +}) + +test("event2message: link to event in an unknown room", async t => { + t.deepEqual( + await eventToMessage({ + content: { + msgtype: "m.text", + body: "wrong body", + format: "org.matrix.custom.html", + formatted_body: 'ah yeah, here\'s where the bug was reported: https://matrix.to/#/!QtykxKocfZaZOUrTwp:matrix.org/$1542477546853947KGhZL:matrix.org' + }, + event_id: "$g07oYSZFWBkxohNEfywldwgcWj1hbhDzQ1sBAKvqOOU", + origin_server_ts: 1688301929913, + room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe", + sender: "@cadence:cadence.moe", + type: "m.room.message", + unsigned: { + age: 405299 + } + }), + { + ensureJoined: [], + messagesToDelete: [], + messagesToEdit: [], + messagesToSend: [{ + username: "cadence [they]", + content: "ah yeah, here's where the bug was reported: [https://matrix.to/#/!QtykxKocfZaZOUrTwp:matrix.org/$1542477546853947KGhZL:matrix.org]()", + avatar_url: undefined + }] + } + ) +}) + test("event2message: colon after mentions is stripped", async t => { t.deepEqual( await eventToMessage({ diff --git a/test/test.js b/test/test.js index 553ec44..4663b18 100644 --- a/test/test.js +++ b/test/test.js @@ -24,7 +24,12 @@ const discord = { ]), application: { id: "684280192553844747" - } + }, + channels: new Map([ + ["497161350934560778", { + guild_id: "497159726455455754" + }] + ]) } Object.assign(passthrough, { discord, config, sync, db })