From 756e8e27ad0299a6bcdd94d693eac203b805265d Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Wed, 21 Jan 2026 01:59:54 +1300 Subject: [PATCH 1/6] Make registration more consistent --- src/d2m/converters/user-to-mxid.test.js | 15 +++++++-------- test/test.js | 1 + 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/d2m/converters/user-to-mxid.test.js b/src/d2m/converters/user-to-mxid.test.js index 2217a93..387d472 100644 --- a/src/d2m/converters/user-to-mxid.test.js +++ b/src/d2m/converters/user-to-mxid.test.js @@ -45,14 +45,6 @@ test("user2name: works on special user", t => { t.equal(userToSimName(data.user.clyde_ai), "clyde_ai") }) -test("user2name: includes ID if requested in config", t => { - const {reg} = require("../../matrix/read-registration") - reg.ooye.include_user_id_in_mxid = true - t.equal(userToSimName({username: "Harry Styles!", discriminator: "0001", id: "123456"}), "123456_harry_styles") - t.equal(userToSimName({username: "f***", discriminator: "0001", id: "123456"}), "123456_f") - reg.ooye.include_user_id_in_mxid = false -}) - test("webhook author: can generate sim names", t => { t.equal(webhookAuthorToSimName({ username: "Cadence, Maid of Creation, Eye of Clarity, Empress of Hope โ˜†", @@ -60,3 +52,10 @@ test("webhook author: can generate sim names", t => { id: "123" }), "webhook_cadence_maid_of_creation_eye_of_clarity_empress_of_hope") }) + +test("user2name: includes ID if requested in config", t => { + const {reg} = require("../../matrix/read-registration") + reg.ooye.include_user_id_in_mxid = true + t.equal(userToSimName({username: "Harry Styles!", discriminator: "0001", id: "123456"}), "123456_harry_styles") + t.equal(userToSimName({username: "f***", discriminator: "0001", id: "123456"}), "123456_f") +}) diff --git a/test/test.js b/test/test.js index be7febf..5ae9f67 100644 --- a/test/test.js +++ b/test/test.js @@ -30,6 +30,7 @@ reg.ooye.bridge_origin = "https://bridge.example.org" reg.ooye.time_zone = "Pacific/Auckland" reg.ooye.max_file_size = 5000000 reg.ooye.web_password = "password123" +reg.ooye.include_user_id_in_mxid = false const sync = new HeatSync({watchFS: false}) From 5e4b99a5523327c5820a3fa57b9f89140b240a48 Mon Sep 17 00:00:00 2001 From: Rory& Date: Tue, 20 Jan 2026 11:21:12 +0100 Subject: [PATCH 2/6] Remove reply fallback for same-room replies (and update tests accordingly) --- src/d2m/converters/edit-to-changes.test.js | 7 +--- src/d2m/converters/message-to-event.js | 12 +++---- .../converters/message-to-event.pk.test.js | 18 ++-------- src/d2m/converters/message-to-event.test.js | 33 ++++--------------- 4 files changed, 15 insertions(+), 55 deletions(-) diff --git a/src/d2m/converters/edit-to-changes.test.js b/src/d2m/converters/edit-to-changes.test.js index b252175..d687702 100644 --- a/src/d2m/converters/edit-to-changes.test.js +++ b/src/d2m/converters/edit-to-changes.test.js @@ -181,12 +181,7 @@ test("edit2changes: edit of reply to skull webp attachment with content", async newContent: { $type: "m.room.message", msgtype: "m.text", - body: "> Extremity: Image\n\n* Edit", - format: "org.matrix.custom.html", - formatted_body: - '
In reply to Extremity' - + '
Image
' - + '* Edit', + body: "* Edit", "m.mentions": {}, "m.new_content": { msgtype: "m.text", diff --git a/src/d2m/converters/message-to-event.js b/src/d2m/converters/message-to-event.js index 1c92123..11e82a5 100644 --- a/src/d2m/converters/message-to-event.js +++ b/src/d2m/converters/message-to-event.js @@ -508,15 +508,13 @@ async function messageToEvent(message, guild, options = {}, di) { // Generate a reply pointing to the Matrix event we found const latestRoomID = select("channel_room", "room_id", {channel_id: repliedToEventRow.channel_id}).pluck().get() // native replies don't work across room upgrades, so make sure the old and new message are in the same room if (latestRoomID !== repliedToEventRow.room_id) repliedToEventInDifferentRoom = true - html = - (latestRoomID === repliedToEventRow.room_id ? "" : "") - + `
In reply to ${repliedToUserHtml}` + html = repliedToEventInDifferentRoom ? + (`
In reply to ${repliedToUserHtml}` + `
${repliedToHtml}
` - + (latestRoomID === repliedToEventRow.room_id ? "" : "") - + html - body = (`${repliedToDisplayName}: ` // scenario 1 part B for mentions + + html) : html + body = repliedToEventInDifferentRoom ? ((`${repliedToDisplayName}: ` // scenario 1 part B for mentions + repliedToBody).split("\n").map(line => "> " + line).join("\n") - + "\n\n" + body + + "\n\n" + body) : body } else { // repliedToUnknownEvent // This reply can't point to the Matrix event because it isn't bridged, we need to indicate this. assert(message.referenced_message) diff --git a/src/d2m/converters/message-to-event.pk.test.js b/src/d2m/converters/message-to-event.pk.test.js index ce83d54..1323280 100644 --- a/src/d2m/converters/message-to-event.pk.test.js +++ b/src/d2m/converters/message-to-event.pk.test.js @@ -50,11 +50,7 @@ test("message2event: pk reply to matrix is converted to native matrix reply", as ] }, msgtype: "m.text", - body: "> cadence [they]: now for my next experiment:\n\nthis is a reply", - format: "org.matrix.custom.html", - formatted_body: '
In reply to cadence [they]
' - + "now for my next experiment:
" - + "this is a reply", + body: "this is a reply", "m.relates_to": { "m.in_reply_to": { event_id: "$NB6nPgO2tfXyIwwDSF0Ga0BUrsgX1S-0Xl-jAvI8ucU" @@ -80,11 +76,7 @@ test("message2event: pk reply to discord is converted to native matrix reply", a $type: "m.room.message", msgtype: "m.text", "m.mentions": {}, - body: "> wing: some text\n\nthis is a reply", - format: "org.matrix.custom.html", - formatted_body: '
In reply to wing
' - + "some text
" - + "this is a reply", + body: "this is a reply", "m.relates_to": { "m.in_reply_to": { event_id: "$mtR8cJqM4fKno1bVsm8F4wUVqSntt2sq6jav1lyavuA" @@ -120,11 +112,7 @@ test("message2event: pk reply to matrix attachment is converted to native matrix "m.mentions": { user_ids: ["@ampflower:matrix.org"] }, - body: "> Ampflower ๐ŸŒบ: [Media]\n\nCat nod", - format: "org.matrix.custom.html", - formatted_body: '
In reply to Ampflower ๐ŸŒบ
' - + "[Media]
" - + "Cat nod", + body: "Cat nod", "m.relates_to": { "m.in_reply_to": { event_id: "$OEEK-Wam2FTh6J-6kVnnJ6KnLA_lLRnLTHatKKL62-Y" diff --git a/src/d2m/converters/message-to-event.test.js b/src/d2m/converters/message-to-event.test.js index f7769d3..a527ad8 100644 --- a/src/d2m/converters/message-to-event.test.js +++ b/src/d2m/converters/message-to-event.test.js @@ -423,12 +423,7 @@ test("message2event: reply to skull webp attachment with content", async t => { }, "m.mentions": {}, msgtype: "m.text", - body: "> Extremity: Image\n\nReply", - format: "org.matrix.custom.html", - formatted_body: - '
In reply to Extremity' - + '
Image
' - + 'Reply' + body: "Reply" }, { $type: "m.room.message", "m.mentions": {}, @@ -472,12 +467,7 @@ test("message2event: simple reply to matrix user", async t => { ] }, msgtype: "m.text", - body: "> cadence: so can you reply to my webhook uwu\n\nReply", - format: "org.matrix.custom.html", - formatted_body: - '
In reply to cadence' - + '
so can you reply to my webhook uwu
' - + 'Reply' + body: "Reply" }]) }) @@ -539,12 +529,7 @@ test("message2event: reply to matrix user with mention", async t => { ] }, msgtype: "m.text", - body: "> okay ๐Ÿค yay ๐Ÿค: @extremity: you owe me $30\n\nkys", - format: "org.matrix.custom.html", - formatted_body: - '
In reply to okay ๐Ÿค yay ๐Ÿค' - + '
@extremity you owe me $30
' - + 'kys' + body: "kys" }]) }) @@ -656,9 +641,7 @@ test("message2event: simple reply in thread to a matrix user's reply", async t = user_ids: ["@cadence:cadence.moe"] }, msgtype: "m.text", - body: "> cadence [they]: What about them?\n\nWell, they don't seem to...", - format: "org.matrix.custom.html", - formatted_body: "
In reply to cadence [they]
What about them?
Well, they don't seem to...", + body: "Well, they don't seem to..." }]) }) @@ -695,9 +678,7 @@ test("message2event: infinidoge's reply to ami's matrix smalltext reply to infin user_ids: ["@ami:the-apothecary.club"] }, msgtype: "m.text", - body: `> Ami (she/her): let me guess they got a lot of bug reports like "empty chest with no loot?"\n\nMost likely`, - format: "org.matrix.custom.html", - formatted_body: `
In reply to Ami (she/her)
let me guess they got a lot of bug reports like "empty chest with no loot?"
Most likely`, + body: `Most likely` }]) }) @@ -734,9 +715,7 @@ test("message2event: infinidoge's reply to ami's matrix smalltext singleline rep user_ids: ["@ami:the-apothecary.club"] }, msgtype: "m.text", - body: `> Ami (she/her): let me guess they got a lot of bug reports like "empty chest with no loot?"\n\nMost likely`, - format: "org.matrix.custom.html", - formatted_body: `
In reply to Ami (she/her)
let me guess they got a lot of bug reports like "empty chest with no loot?"
Most likely`, + body: `Most likely` }]) }) From b5596b2459664019fcadd113438efa40f4de5345 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Wed, 21 Jan 2026 13:50:16 +1300 Subject: [PATCH 3/6] Fetch referenced_message for reply fallback --- src/d2m/converters/message-to-event.js | 96 ++++++++++++++------------ 1 file changed, 53 insertions(+), 43 deletions(-) diff --git a/src/d2m/converters/message-to-event.js b/src/d2m/converters/message-to-event.js index 11e82a5..4e52176 100644 --- a/src/d2m/converters/message-to-event.js +++ b/src/d2m/converters/message-to-event.js @@ -480,51 +480,61 @@ async function messageToEvent(message, guild, options = {}, di) { } // Fallback body/formatted_body for replies + // Generate a fallback if native replies are unsupported, which is in the following situations: + // 1. The replied-to event is in a different room to where the reply will be sent (i.e. a room upgrade occurred between) + // 2. The replied-to message has no corresponding Matrix event (repliedToUnknownEvent is true) // This branch is optional - do NOT change anything apart from the reply fallback, since it may not be run if ((repliedToEventRow || repliedToUnknownEvent) && options.includeReplyFallback !== false) { - let repliedToDisplayName - let repliedToUserHtml - if (repliedToEventRow?.source === 0 && repliedToEventSenderMxid) { - const match = repliedToEventSenderMxid.match(/^@([^:]*)/) - assert(match) - repliedToDisplayName = message.referenced_message?.author.username || match[1] || "a Matrix user" // grab the localpart as the display name, whatever - repliedToUserHtml = `${repliedToDisplayName}` - } else { - repliedToDisplayName = message.referenced_message?.author.global_name || message.referenced_message?.author.username || "a Discord user" - repliedToUserHtml = repliedToDisplayName - } - let repliedToContent = message.referenced_message?.content - if (repliedToContent?.match(/^(-# )?> (-# )?<:L1:/)) { - // If the Discord user is replying to a Matrix user's reply, the fallback is going to contain the emojis and stuff from the bridged rep of the Matrix user's reply quote. - // Need to remove that previous reply rep from this fallback body. The fallbody body should only contain the Matrix user's actual message. - // โ”Œโ”€โ”€โ”€โ”€โ”€โ”€Aโ”€โ”€โ”€โ”€โ”€โ” A reply rep starting with >quote or -#smalltext >quote. Match until the end of the line. - // โ”† โ”†โ”Œโ”€Bโ”€โ” There may be up to 2 reply rep lines in a row if it was created in the old format. Match all lines. - repliedToContent = repliedToContent.replace(/^((-# )?> .*\n){1,2}/, "") - } - if (repliedToContent == "") repliedToContent = "[Media]" - else if (!repliedToContent) repliedToContent = "[Replied-to message content wasn't provided by Discord]" - const {body: repliedToBody, html: repliedToHtml} = await transformContent(repliedToContent) - if (repliedToEventRow) { - // Generate a reply pointing to the Matrix event we found - const latestRoomID = select("channel_room", "room_id", {channel_id: repliedToEventRow.channel_id}).pluck().get() // native replies don't work across room upgrades, so make sure the old and new message are in the same room - if (latestRoomID !== repliedToEventRow.room_id) repliedToEventInDifferentRoom = true - html = repliedToEventInDifferentRoom ? - (`
In reply to ${repliedToUserHtml}` - + `
${repliedToHtml}
` - + html) : html - body = repliedToEventInDifferentRoom ? ((`${repliedToDisplayName}: ` // scenario 1 part B for mentions - + repliedToBody).split("\n").map(line => "> " + line).join("\n") - + "\n\n" + body) : body - } else { // repliedToUnknownEvent - // This reply can't point to the Matrix event because it isn't bridged, we need to indicate this. - assert(message.referenced_message) - const dateDisplay = dUtils.howOldUnbridgedMessage(message.referenced_message.timestamp, message.timestamp) - html = `
In reply to ${dateDisplay} from ${repliedToDisplayName}:` - + `
${repliedToHtml}
` - + html - body = (`In reply to ${dateDisplay}:\n${repliedToDisplayName}: ` - + repliedToBody).split("\n").map(line => "> " + line).join("\n") - + "\n\n" + body + const latestRoomID = repliedToEventRow ? select("channel_room", "room_id", {channel_id: repliedToEventRow.channel_id}).pluck().get() : null + if (latestRoomID !== repliedToEventRow?.room_id) repliedToEventInDifferentRoom = true + + // check that condition 1 or 2 is met + if (repliedToEventInDifferentRoom || repliedToUnknownEvent) { + let referenced = message.referenced_message + if (!referenced) { // backend couldn't be bothered to dereference the message, have to do it ourselves + referenced = await discord.snow.channel.getChannelMessage(message.message_reference.channel_id, message.message_reference.message_id) + } + + // Username + let repliedToDisplayName + let repliedToUserHtml + if (repliedToEventRow?.source === 0 && repliedToEventSenderMxid) { + const match = repliedToEventSenderMxid.match(/^@([^:]*)/) + assert(match) + repliedToDisplayName = referenced.author.username || match[1] || "a Matrix user" // grab the localpart as the display name, whatever + repliedToUserHtml = `${repliedToDisplayName}` + } else { + repliedToDisplayName = referenced.author.global_name || referenced.author.username || "a Discord user" + repliedToUserHtml = repliedToDisplayName + } + + // Content + let repliedToContent = referenced.content + if (repliedToContent?.match(/^(-# )?> (-# )?<:L1:/)) { + // If the Discord user is replying to a Matrix user's reply, the fallback is going to contain the emojis and stuff from the bridged rep of the Matrix user's reply quote. + // Need to remove that previous reply rep from this fallback body. The fallbody body should only contain the Matrix user's actual message. + // โ”Œโ”€โ”€โ”€โ”€โ”€โ”€Aโ”€โ”€โ”€โ”€โ”€โ” A reply rep starting with >quote or -#smalltext >quote. Match until the end of the line. + // โ”† โ”†โ”Œโ”€Bโ”€โ” There may be up to 2 reply rep lines in a row if it was created in the old format. Match all lines. + repliedToContent = repliedToContent.replace(/^((-# )?> .*\n){1,2}/, "") + } + if (repliedToContent == "") repliedToContent = "[Media]" + const {body: repliedToBody, html: repliedToHtml} = await transformContent(repliedToContent) + + // Now branch on condition 1 or 2 for a different kind of fallback + if (repliedToEventRow) { + html = `
In reply to ${repliedToUserHtml}` + + `
${repliedToHtml}
` + + html + body = `${repliedToDisplayName}: ${repliedToBody}`.split("\n").map(line => "> " + line).join("\n") // scenario 1 part B for mentions + + "\n\n" + body + } else { // repliedToUnknownEvent + const dateDisplay = dUtils.howOldUnbridgedMessage(referenced.timestamp, message.timestamp) + html = `
In reply to ${dateDisplay} from ${repliedToDisplayName}:` + + `
${repliedToHtml}
` + + html + body = `In reply to ${dateDisplay}:\n${repliedToDisplayName}: ${repliedToBody}`.split("\n").map(line => "> " + line).join("\n") + + "\n\n" + body + } } } From ddc7387fa0924e6da5029505b6daf84e22f9f063 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Wed, 21 Jan 2026 13:01:36 +1300 Subject: [PATCH 4/6] Do not make forwarded messages m.notice --- src/d2m/converters/message-to-event.js | 5 +- src/d2m/converters/message-to-event.test.js | 89 ++++++++++++++++++++- 2 files changed, 88 insertions(+), 6 deletions(-) diff --git a/src/d2m/converters/message-to-event.js b/src/d2m/converters/message-to-event.js index 4e52176..78829ba 100644 --- a/src/d2m/converters/message-to-event.js +++ b/src/d2m/converters/message-to-event.js @@ -37,8 +37,8 @@ function getDiscordParseCallbacks(message, guild, useHTML, spoilers = []) { const username = message.mentions?.find(ment => ment.id === node.id)?.username || message.referenced_message?.mentions?.find(ment => ment.id === node.id)?.username || (interaction?.user.id === node.id ? interaction.user.username : null) - || (message.author.id === node.id ? message.author.username : null) - || node.id + || (message.author?.id === node.id ? message.author.username : null) + || "unknown-user" if (mxid && useHTML) { return `@${username}` } else { @@ -610,7 +610,6 @@ async function messageToEvent(message, guild, options = {}, di) { // Indent for (const event of forwardedEvents) { if (["m.text", "m.notice"].includes(event.msgtype)) { - event.msgtype = "m.notice" event.body = event.body.split("\n").map(l => "ยป " + l).join("\n") event.formatted_body = `
${event.formatted_body}
` } diff --git a/src/d2m/converters/message-to-event.test.js b/src/d2m/converters/message-to-event.test.js index a527ad8..fa51eae 100644 --- a/src/d2m/converters/message-to-event.test.js +++ b/src/d2m/converters/message-to-event.test.js @@ -1090,7 +1090,7 @@ test("message2event: constructed forwarded message", async t => { formatted_body: `๐Ÿ”€ Forwarded from wonderland [jump to event]` + `
What's cooking, good looking? :hipposcope:
`, "m.mentions": {}, - msgtype: "m.notice", + msgtype: "m.text", }, { $type: "m.room.message", @@ -1149,7 +1149,7 @@ test("message2event: constructed forwarded text", async t => { formatted_body: `๐Ÿ”€ Forwarded from amanda-spam [jump to room]` + `
What's cooking, good looking?
`, "m.mentions": {}, - msgtype: "m.notice", + msgtype: "m.text", }, { $type: "m.room.message", @@ -1172,7 +1172,7 @@ test("message2event: don't scan forwarded messages for mentions", async t => { formatted_body: `๐Ÿ”€ Forwarded message` + `
If some folks have spare bandwidth then helping out ArchiveTeam with archiving soon to be deleted research and government data might be worthwhile https://social.luca.run/@luca/113950834185678114
`, "m.mentions": {}, - msgtype: "m.notice" + msgtype: "m.text" } ]) }) @@ -1429,3 +1429,86 @@ test("message2event: cross-room reply", async t => { } ]) }) + +test("message2event: forwarded message with unreferenced mention", async t => { + const events = await messageToEvent({ + type: 0, + content: "", + attachments: [], + embeds: [], + timestamp: "2026-01-20T14:14:21.281Z", + edited_timestamp: null, + flags: 16384, + components: [], + id: "1463174818823405651", + channel_id: "893634327722721290", + author: { + id: "100031256988766208", + username: "leo60228", + discriminator: "0", + avatar: "8a164f29946f23eb4f45cde71a75e5a6", + avatar_decoration_data: null, + public_flags: 768, + global_name: "leo vriska", + primary_guild: null, + collectibles: null, + display_name_styles: null + }, + bot: false, + pinned: false, + mentions: [], + mention_roles: [], + mention_everyone: false, + tts: false, + message_reference: { + type: 1, + channel_id: "937181373943382036", + message_id: "1032034158261846038", + guild_id: "936370934292549712" + }, + message_snapshots: [ + { + message: { + type: 0, + content: "<@77084495118868480>", + attachments: [ + { + id: "1463174815119704114", + filename: "2022-10-18_16-49-46.mp4", + size: 51238885, + url: "https://cdn.discordapp.com/attachments/893634327722721290/1463174815119704114/2022-10-18_16-49-46.mp4?ex=6970df3c&is=696f8dbc&hm=515d3cbcc8464bdada7f4c3d9ccc8174f671cb75391ce21a46a804fcb1e4befe&", + proxy_url: "https://media.discordapp.net/attachments/893634327722721290/1463174815119704114/2022-10-18_16-49-46.mp4?ex=6970df3c&is=696f8dbc&hm=515d3cbcc8464bdada7f4c3d9ccc8174f671cb75391ce21a46a804fcb1e4befe&", + width: 1920, + height: 1080, + content_type: "video/mp4", + content_scan_version: 3, + spoiler: false + } + ], + embeds: [], + timestamp: "2022-10-18T20:55:17.597Z", + edited_timestamp: null, + flags: 0, + components: [] + } + } + ] + }) + t.deepEqual(events, [ + { + $type: "m.room.message", + msgtype: "m.text", + body: "[๐Ÿ”€ Forwarded message]\nยป @unknown-user:", + format: "org.matrix.custom.html", + formatted_body: `๐Ÿ”€ Forwarded message
@unknown-user:
`, + "m.mentions": {} + }, { + $type: "m.room.message", + msgtype: "m.text", + body: "ยป ๐ŸŽž๏ธ Uploaded file: https://bridge.example.org/download/discordcdn/893634327722721290/1463174815119704114/2022-10-18_16-49-46.mp4 (51 MB)", + format: "org.matrix.custom.html", + formatted_body: "
๐ŸŽž๏ธ Uploaded file: 2022-10-18_16-49-46.mp4 (51 MB)
", + "m.mentions": {} + } + ]) +}) From 345b7d61359eb080d439ab0b04a2354c97e72421 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Wed, 21 Jan 2026 13:25:30 +1300 Subject: [PATCH 5/6] Merge attachments with message when possible --- src/d2m/converters/message-to-event.js | 36 ++++++--- src/d2m/converters/message-to-event.test.js | 81 +++++++++++++++------ test/ooye-test-data.sql | 3 +- 3 files changed, 87 insertions(+), 33 deletions(-) diff --git a/src/d2m/converters/message-to-event.js b/src/d2m/converters/message-to-event.js index 78829ba..449303a 100644 --- a/src/d2m/converters/message-to-event.js +++ b/src/d2m/converters/message-to-event.js @@ -542,16 +542,9 @@ async function messageToEvent(message, guild, options = {}, di) { $type: "m.room.message", "m.mentions": mentions, msgtype, - body: body - } - - const isPlaintext = body === html - - if (!isPlaintext || options.alwaysReturnFormattedBody) { - Object.assign(newTextMessageEvent, { - format: "org.matrix.custom.html", - formatted_body: html - }) + body: body, + format: "org.matrix.custom.html", + formatted_body: html } events.push(newTextMessageEvent) @@ -695,7 +688,18 @@ async function messageToEvent(message, guild, options = {}, di) { // Then attachments if (message.attachments) { const attachmentEvents = await Promise.all(message.attachments.map(attachmentToEvent.bind(null, mentions))) - events.push(...attachmentEvents) + + // Try to merge attachment events with the previous event + // This means that if the attachments ended up as a text link, and especially if there were many of them, the events will be joined together. + let prev = events.at(-1) + for (const atch of attachmentEvents) { + if (atch.msgtype === "m.text" && prev?.body && prev?.formatted_body && ["m.text", "m.notice"].includes(prev?.msgtype)) { + prev.body = prev.body + "\n" + atch.body + prev.formatted_body = prev.formatted_body + "
" + atch.formatted_body + } else { + events.push(atch) + } + } } // Then embeds @@ -829,6 +833,16 @@ async function messageToEvent(message, guild, options = {}, di) { }) } + // Strip formatted_body where equivalent to body + if (!options.alwaysReturnFormattedBody) { + for (const event of events) { + if (["m.text", "m.notice"].includes(event.msgtype) && event.body === event.formatted_body) { + delete event.format + delete event.formatted_body + } + } + } + return events } diff --git a/src/d2m/converters/message-to-event.test.js b/src/d2m/converters/message-to-event.test.js index fa51eae..3c0c5d9 100644 --- a/src/d2m/converters/message-to-event.test.js +++ b/src/d2m/converters/message-to-event.test.js @@ -869,14 +869,62 @@ test("message2event: very large attachment is linked instead of being uploaded", $type: "m.room.message", "m.mentions": {}, msgtype: "m.text", - body: "hey" - }, { + body: "hey\n๐Ÿ“„ Uploaded file: https://bridge.example.org/download/discordcdn/123/456/789.mega (100 MB)", + format: "org.matrix.custom.html", + formatted_body: 'hey
๐Ÿ“„ Uploaded file: hey.jpg (100 MB)' + }]) +}) + +test("message2event: multiple attachments are combined into the same event where possible", async t => { + const events = await messageToEvent({ + content: "hey", + attachments: [{ + filename: "hey.jpg", + url: "https://cdn.discordapp.com/attachments/123/456/789.mega", + content_type: "application/i-made-it-up", + size: 100e6 + }, { + filename: "SPOILER_secret.jpg", + url: "https://cdn.discordapp.com/attachments/123/456/SPOILER_secret.jpg", + content_type: "image/jpeg", + size: 38291 + }, { + filename: "my enemies.txt", + url: "https://cdn.discordapp.com/attachments/123/456/my_enemies.txt", + content_type: "text/plain", + size: 8911 + }, { + filename: "hey.jpg", + url: "https://cdn.discordapp.com/attachments/123/456/789.mega", + content_type: "application/i-made-it-up", + size: 100e6 + }] + }) + t.deepEqual(events, [{ $type: "m.room.message", "m.mentions": {}, msgtype: "m.text", - body: "๐Ÿ“„ Uploaded file: https://bridge.example.org/download/discordcdn/123/456/789.mega (100 MB)", + body: "hey" + + "\n๐Ÿ“„ Uploaded file: https://bridge.example.org/download/discordcdn/123/456/789.mega (100 MB)" + + "\n๐Ÿ“ธ Uploaded SPOILER file: https://bridge.example.org/download/discordcdn/123/456/SPOILER_secret.jpg (38 KB)" + + "\n๐Ÿ“„ Uploaded file: https://bridge.example.org/download/discordcdn/123/456/789.mega (100 MB)", format: "org.matrix.custom.html", - formatted_body: '๐Ÿ“„ Uploaded file: hey.jpg (100 MB)' + formatted_body: "hey" + + `
๐Ÿ“„ Uploaded file: hey.jpg (100 MB)` + + `
๐Ÿ“ธ Uploaded SPOILER file: https://bridge.example.org/download/discordcdn/123/456/SPOILER_secret.jpg (38 KB)
` + + `
๐Ÿ“„ Uploaded file: hey.jpg (100 MB)` + }, { + $type: "m.room.message", + "m.mentions": {}, + msgtype: "m.file", + body: "my enemies.txt", + filename: "my enemies.txt", + external_url: "https://bridge.example.org/download/discordcdn/123/456/my_enemies.txt", + url: "mxc://cadence.moe/y89EOTRp2lbeOkgdsEleGOge", + info: { + mimetype: "text/plain", + size: 8911 + } }]) }) @@ -1494,21 +1542,12 @@ test("message2event: forwarded message with unreferenced mention", async t => { } ] }) - t.deepEqual(events, [ - { - $type: "m.room.message", - msgtype: "m.text", - body: "[๐Ÿ”€ Forwarded message]\nยป @unknown-user:", - format: "org.matrix.custom.html", - formatted_body: `๐Ÿ”€ Forwarded message
@unknown-user:
`, - "m.mentions": {} - }, { - $type: "m.room.message", - msgtype: "m.text", - body: "ยป ๐ŸŽž๏ธ Uploaded file: https://bridge.example.org/download/discordcdn/893634327722721290/1463174815119704114/2022-10-18_16-49-46.mp4 (51 MB)", - format: "org.matrix.custom.html", - formatted_body: "
๐ŸŽž๏ธ Uploaded file: 2022-10-18_16-49-46.mp4 (51 MB)
", - "m.mentions": {} - } - ]) + t.deepEqual(events, [{ + $type: "m.room.message", + msgtype: "m.text", + body: "[๐Ÿ”€ Forwarded message]\nยป @unknown-user:\nยป ๐ŸŽž๏ธ Uploaded file: https://bridge.example.org/download/discordcdn/893634327722721290/1463174815119704114/2022-10-18_16-49-46.mp4 (51 MB)", + format: "org.matrix.custom.html", + formatted_body: "๐Ÿ”€ Forwarded message
@unknown-user:
๐ŸŽž๏ธ Uploaded file: 2022-10-18_16-49-46.mp4 (51 MB)
", + "m.mentions": {} + }]) }) diff --git a/test/ooye-test-data.sql b/test/ooye-test-data.sql index ef3dc5f..04e6b9b 100644 --- a/test/ooye-test-data.sql +++ b/test/ooye-test-data.sql @@ -151,7 +151,8 @@ INSERT INTO file (discord_url, mxc_url) VALUES ('https://cdn.discordapp.com/attachments/112760669178241024/1197621094786531358/Ins_1960637570.mp4', 'mxc://cadence.moe/kMqLycqMURhVpwleWkmASpnU'), ('https://cdn.discordapp.com/attachments/1099031887500034088/1112476845502365786/voice-message.ogg', 'mxc://cadence.moe/MRRPDggXQMYkrUjTpxQbmcxB'), ('https://cdn.discordapp.com/attachments/122155380120748034/1174514575220158545/the.yml', 'mxc://cadence.moe/HnQIYQmmlIKwOQsbFsIGpzPP'), -('https://cdn.discordapp.com/attachments/112760669178241024/1296237494987133070/100km.gif', 'mxc://cadence.moe/qDAotmebTfEIfsAIVCEZptLh'); +('https://cdn.discordapp.com/attachments/112760669178241024/1296237494987133070/100km.gif', 'mxc://cadence.moe/qDAotmebTfEIfsAIVCEZptLh'), +('https://cdn.discordapp.com/attachments/123/456/my_enemies.txt', 'mxc://cadence.moe/y89EOTRp2lbeOkgdsEleGOge'); INSERT INTO emoji (emoji_id, name, animated, mxc_url) VALUES ('230201364309868544', 'hippo', 0, 'mxc://cadence.moe/qWmbXeRspZRLPcjseyLmeyXC'), From 90fcbd0ddc407434cf448ada14cc4d53d5d51b68 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Wed, 21 Jan 2026 14:33:24 +1300 Subject: [PATCH 6/6] Update Discord libraries --- package-lock.json | 64 ++++++++++--------------- package.json | 5 +- src/d2m/actions/update-pins.js | 2 +- src/d2m/converters/pins-to-list.js | 9 ++-- src/d2m/converters/pins-to-list.test.js | 5 +- src/matrix/kstate.js | 16 +++++-- test/data.js | 14 +++--- 7 files changed, 59 insertions(+), 56 deletions(-) diff --git a/package-lock.json b/package-lock.json index bf857a5..432c3f1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,7 +23,7 @@ "ansi-colors": "^4.1.3", "better-sqlite3": "^12.2.0", "chunk-text": "^2.0.1", - "cloudstorm": "^0.14.0", + "cloudstorm": "^0.15.2", "discord-api-types": "^0.38.36", "domino": "^2.1.6", "enquirer": "^2.4.1", @@ -35,7 +35,7 @@ "lru-cache": "^11.0.2", "prettier-bytes": "^1.0.4", "sharp": "^0.34.5", - "snowtransfer": "^0.14.2", + "snowtransfer": "^0.17.0", "stream-mime-type": "^1.0.2", "try-to-catch": "^3.0.1", "uqr": "^0.1.2", @@ -1552,30 +1552,18 @@ } }, "node_modules/cloudstorm": { - "version": "0.14.1", - "resolved": "https://registry.npmjs.org/cloudstorm/-/cloudstorm-0.14.1.tgz", - "integrity": "sha512-x95WCKg818E1rE1Ru45NPD3RoIq0pg3WxwvF0GE7Eq07pAeLcjSRqM1lUmbmfjdOqZrWdSRYA1NETVZ8QhVrIA==", + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/cloudstorm/-/cloudstorm-0.15.2.tgz", + "integrity": "sha512-5y7E0uI39R3d7c+AWksqAQAlZlpx+qNjxjQfNIem2hh68s6QRmOFHTKu34I7pBE6JonpZf8AmoMYArY/4lLVmg==", "license": "MIT", "dependencies": { - "discord-api-types": "^0.38.21", - "snowtransfer": "^0.15.0" + "discord-api-types": "^0.38.37", + "snowtransfer": "^0.17.0" }, "engines": { "node": ">=22.0.0" } }, - "node_modules/cloudstorm/node_modules/snowtransfer": { - "version": "0.15.0", - "resolved": "https://registry.npmjs.org/snowtransfer/-/snowtransfer-0.15.0.tgz", - "integrity": "sha512-kEDGKtFiH5nSkHsDZonEUuDx99lUasJoZ7AGrgvE8HzVG59vjvqc//C+pjWj4DuJqTj4Q+Z1L/M/MYNim8F2VA==", - "license": "MIT", - "dependencies": { - "discord-api-types": "^0.38.21" - }, - "engines": { - "node": ">=16.15.0" - } - }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -1711,9 +1699,9 @@ } }, "node_modules/discord-api-types": { - "version": "0.38.36", - "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.38.36.tgz", - "integrity": "sha512-qrbUbjjwtyeBg5HsAlm1C859epfOyiLjPqAOzkdWlCNsZCWJrertnETF/NwM8H+waMFU58xGSc5eXUfXah+WTQ==", + "version": "0.38.37", + "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.38.37.tgz", + "integrity": "sha512-Cv47jzY1jkGkh5sv0bfHYqGgKOWO1peOrGMkDFM4UmaGMOTgOW8QSexhvixa9sVOiz8MnVOBryWYyw/CEVhj7w==", "license": "MIT", "workspaces": [ "scripts/actions/documentation" @@ -1971,9 +1959,9 @@ } }, "node_modules/h3": { - "version": "1.15.4", - "resolved": "https://registry.npmjs.org/h3/-/h3-1.15.4.tgz", - "integrity": "sha512-z5cFQWDffyOe4vQ9xIqNfCZdV4p//vy6fBnr8Q1AWnVZ0teurKMG66rLj++TKwKPUP3u7iMUvrvKaEUiQw2QWQ==", + "version": "1.15.5", + "resolved": "https://registry.npmjs.org/h3/-/h3-1.15.5.tgz", + "integrity": "sha512-xEyq3rSl+dhGX2Lm0+eFQIAzlDN6Fs0EcC4f7BNUmzaRX/PTzeuM+Tr2lHB8FoXggsQIeXLj8EDVgs5ywxyxmg==", "license": "MIT", "dependencies": { "cookie-es": "^1.2.2", @@ -1981,9 +1969,9 @@ "defu": "^6.1.4", "destr": "^2.0.5", "iron-webcrypto": "^1.2.1", - "node-mock-http": "^1.0.2", + "node-mock-http": "^1.0.4", "radix3": "^1.1.2", - "ufo": "^1.6.1", + "ufo": "^1.6.3", "uncrypto": "^0.1.3" } }, @@ -2321,9 +2309,9 @@ } }, "node_modules/node-mock-http": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/node-mock-http/-/node-mock-http-1.0.2.tgz", - "integrity": "sha512-zWaamgDUdo9SSLw47we78+zYw/bDr5gH8pH7oRRs8V3KmBtu8GLgGIbV2p/gRPd3LWpEOpjQj7X1FOU3VFMJ8g==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/node-mock-http/-/node-mock-http-1.0.4.tgz", + "integrity": "sha512-8DY+kFsDkNXy1sJglUfuODx1/opAGJGyrTuFqEoN90oRc2Vk0ZbD4K2qmKXBBEhZQzdKHIVfEJpDU8Ak2NJEvQ==", "license": "MIT" }, "node_modules/object-assign": { @@ -2821,15 +2809,15 @@ } }, "node_modules/snowtransfer": { - "version": "0.14.2", - "resolved": "https://registry.npmjs.org/snowtransfer/-/snowtransfer-0.14.2.tgz", - "integrity": "sha512-Fi8OdRmaIgeCj58oVej+tQAoY2I+Xp/6PAYV8X93jE/2E6Anc87SbTbDV6WZXCnuzTQz3gty8JOGz02qI7Qs9A==", + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/snowtransfer/-/snowtransfer-0.17.0.tgz", + "integrity": "sha512-H6Avpsco+HlVIkN+MbX34Q7+9g9Wci0wZQwGsvfw20VqEb7jnnk73iUcWytNMYtKZ72Ud58n6cFnQ3apTEamxw==", "license": "MIT", "dependencies": { - "discord-api-types": "^0.38.8" + "discord-api-types": "^0.38.37" }, "engines": { - "node": ">=16.15.0" + "node": ">=22.0.0" } }, "node_modules/source-map": { @@ -3244,9 +3232,9 @@ } }, "node_modules/ufo": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.1.tgz", - "integrity": "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==", + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.3.tgz", + "integrity": "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==", "license": "MIT" }, "node_modules/uncrypto": { diff --git a/package.json b/package.json index 64e6f77..4d0c43a 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,7 @@ "ansi-colors": "^4.1.3", "better-sqlite3": "^12.2.0", "chunk-text": "^2.0.1", - "cloudstorm": "^0.14.0", + "cloudstorm": "^0.15.2", "discord-api-types": "^0.38.36", "domino": "^2.1.6", "enquirer": "^2.4.1", @@ -44,7 +44,7 @@ "lru-cache": "^11.0.2", "prettier-bytes": "^1.0.4", "sharp": "^0.34.5", - "snowtransfer": "^0.14.2", + "snowtransfer": "^0.17.0", "stream-mime-type": "^1.0.2", "try-to-catch": "^3.0.1", "uqr": "^0.1.2", @@ -64,7 +64,6 @@ "scripts": { "start": "node --enable-source-maps start.js", "setup": "node --enable-source-maps scripts/setup.js", - "build": "mkdir -p dist/out-of-your-element && cp -R src dist/out-of-your-element && cp -R docs dist/out-of-your-element && npx tsdown", "addbot": "node addbot.js", "test": "cross-env FORCE_COLOR=true supertape --no-check-assertions-count --format tap --no-worker test/test.js | tap-dot", "test-slow": "cross-env FORCE_COLOR=true supertape --no-check-assertions-count --format tap --no-worker test/test.js -- --slow | tap-dot", diff --git a/src/d2m/actions/update-pins.js b/src/d2m/actions/update-pins.js index 15febaa..56c9642 100644 --- a/src/d2m/actions/update-pins.js +++ b/src/d2m/actions/update-pins.js @@ -34,7 +34,7 @@ async function updatePins(channelID, roomID, convertedTimestamp) { throw e } - const kstate = await ks.roomToKState(roomID) + const kstate = await ks.roomToKState(roomID, [["m.room.pinned_events", ""]]) const pinned = pinsToList.pinsToList(discordPins, kstate) const diff = ks.diffKState(kstate, {"m.room.pinned_events/": {pinned}}) diff --git a/src/d2m/converters/pins-to-list.js b/src/d2m/converters/pins-to-list.js index 3e890ea..5a33c7c 100644 --- a/src/d2m/converters/pins-to-list.js +++ b/src/d2m/converters/pins-to-list.js @@ -3,10 +3,11 @@ const {select} = require("../../passthrough") /** - * @param {import("discord-api-types/v10").RESTGetAPIChannelPinsResult} pins + * @param {import("discord-api-types/v10").RESTGetAPIChannelMessagesPinsResult} pins * @param {{"m.room.pinned_events/"?: {pinned?: string[]}}} kstate */ function pinsToList(pins, kstate) { + /** Most recent last. */ let alreadyPinned = kstate["m.room.pinned_events/"]?.pinned || [] // If any of the already pinned messages are bridged messages then remove them from the already pinned list. @@ -15,13 +16,13 @@ function pinsToList(pins, kstate) { // * Matrix-only unbridged messages that are pinned will remain pinned. alreadyPinned = alreadyPinned.filter(event_id => { const messageID = select("event_message", "message_id", {event_id}).pluck().get() - return !messageID || pins.find(m => m.id === messageID) // if it is bridged then remove it from the filter + return !messageID || pins.items.find(m => m.message.id === messageID) // if it is bridged then remove it from the filter }) /** @type {string[]} */ const result = [] - for (const message of pins) { - const eventID = select("event_message", "event_id", {message_id: message.id, part: 0}).pluck().get() + for (const pin of pins.items) { + const eventID = select("event_message", "event_id", {message_id: pin.message.id, part: 0}).pluck().get() if (eventID && !alreadyPinned.includes(eventID)) result.push(eventID) } result.reverse() diff --git a/src/d2m/converters/pins-to-list.test.js b/src/d2m/converters/pins-to-list.test.js index d0657cb..571735e 100644 --- a/src/d2m/converters/pins-to-list.test.js +++ b/src/d2m/converters/pins-to-list.test.js @@ -1,6 +1,7 @@ const {test} = require("supertape") const data = require("../../../test/data") const {pinsToList} = require("./pins-to-list") +const mixin = require("@cloudrac3r/mixin-deep") test("pins2list: converts known IDs, ignores unknown IDs", t => { const result = pinsToList(data.pins.faked, {}) @@ -46,7 +47,9 @@ test("pins2list: already pinned unknown items are not moved", t => { }) test("pins2list: bridged messages can be unpinned", t => { - const result = pinsToList(data.pins.faked.slice(0, -2), { + const shortPins = mixin({}, data.pins.faked) + shortPins.items = shortPins.items.slice(0, -2) + const result = pinsToList(shortPins, { "m.room.pinned_events/": { pinned: [ "$mtR8cJqM4fKno1bVsm8F4wUVqSntt2sq6jav1lyavuA", diff --git a/src/matrix/kstate.js b/src/matrix/kstate.js index 37eed39..c901ce1 100644 --- a/src/matrix/kstate.js +++ b/src/matrix/kstate.js @@ -140,10 +140,20 @@ function diffKState(actual, target) { /** * Async because it gets all room state from the homeserver. * @param {string} roomID + * @param {[type: string, key: string][]} [limitToEvents] */ -async function roomToKState(roomID) { - const root = await api.getAllState(roomID) - return stateToKState(root) +async function roomToKState(roomID, limitToEvents) { + if (!limitToEvents) { + const root = await api.getAllState(roomID) + return stateToKState(root) + } else { + const root = [] + await Promise.all(limitToEvents.map(async ([type, key]) => { + const outer = await api.getStateEventOuter(roomID, type, key) + root.push(outer) + })) + return stateToKState(root) + } } /** diff --git a/test/data.js b/test/data.js index e80b436..0942a87 100644 --- a/test/data.js +++ b/test/data.js @@ -1256,12 +1256,14 @@ module.exports = { } }, pins: { - faked: [ - {id: "1126786462646550579"}, - {id: "1141501302736695316"}, - {id: "1106366167788044450"}, - {id: "1115688611186193400"} - ] + faked: { + items: [ + {message: {id: "1126786462646550579"}}, + {message: {id: "1141501302736695316"}}, + {message: {id: "1106366167788044450"}}, + {message: {id: "1115688611186193400"}} + ] + } }, message: { // Display order is text content, attachments, then stickers