From ff022e8793dc32420fda889035d9188112947233 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Fri, 13 Mar 2026 11:12:44 +1300 Subject: [PATCH] Combine additional embed images into same event --- src/d2m/converters/message-to-event.js | 45 ++++-- .../message-to-event.test.embeds.js | 38 +++++ src/d2m/converters/message-to-event.test.js | 2 +- test/data.js | 135 ++++++++++++++++++ 4 files changed, 209 insertions(+), 11 deletions(-) diff --git a/src/d2m/converters/message-to-event.js b/src/d2m/converters/message-to-event.js index 05a376a..adc56e6 100644 --- a/src/d2m/converters/message-to-event.js +++ b/src/d2m/converters/message-to-event.js @@ -261,6 +261,29 @@ function getFormattedInteraction(interaction, isThinkingInteraction) { } } +/** + * @param {any} newEvents merge into events + * @param {any} events will be modified + * @param {boolean} forceSameMsgtype whether m.text may only be combined with m.text, etc + */ +function mergeTextEvents(newEvents, events, forceSameMsgtype) { + let prev = events.at(-1) + for (const ne of newEvents) { + const isAllText = prev?.body && prev?.formatted_body && ["m.text", "m.notice"].includes(ne.msgtype) && ["m.text", "m.notice"].includes(prev?.msgtype) + const typesPermitted = !forceSameMsgtype || ne?.msgtype === prev?.msgtype + if (isAllText && typesPermitted) { + const rep = new mxUtils.MatrixStringBuilder() + rep.body = prev.body + rep.formattedBody = prev.formatted_body + rep.addLine(ne.body, ne.formatted_body) + prev.body = rep.body + prev.formatted_body = rep.formattedBody + } else { + events.push(ne) + } + } +} + /** * @param {DiscordTypes.APIMessage} message * @param {DiscordTypes.APIGuild} guild @@ -835,15 +858,7 @@ async function messageToEvent(message, guild, options = {}, di) { // 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) - } - } + mergeTextEvents(attachmentEvents, events, false) } // Then components @@ -981,6 +996,7 @@ async function messageToEvent(message, guild, options = {}, di) { // Start building up a replica ("rep") of the embed in Discord-markdown format, which we will convert into both plaintext and formatted body at once const rep = new mxUtils.MatrixStringBuilder() + let isAdditionalImage = false if (isKlipyGIF) { assert(embed.video?.url) @@ -1047,7 +1063,11 @@ async function messageToEvent(message, guild, options = {}, di) { let chosenImage = embed.image?.url // the thumbnail seems to be used for "article" type but displayed big at the bottom by discord if (embed.type === "article" && embed.thumbnail?.url && !chosenImage) chosenImage = embed.thumbnail.url - if (chosenImage) rep.addParagraph(`📸 ${dUtils.getPublicUrlForCdn(chosenImage)}`) + + if (chosenImage) { + isAdditionalImage = !rep.body && !!events.length + rep.addParagraph(`📸 ${dUtils.getPublicUrlForCdn(chosenImage)}`) + } if (embed.video?.url) rep.addParagraph(`🎞️ ${dUtils.getPublicUrlForCdn(embed.video.url)}`) @@ -1056,6 +1076,11 @@ async function messageToEvent(message, guild, options = {}, di) { body = body.split("\n").map(l => "| " + l).join("\n") html = `
${html}
` + if (isAdditionalImage) { + mergeTextEvents([{...rep.get(), body, html, msgtype: "m.notice"}], events, true) + continue + } + // Send as m.notice to apply the usual automated/subtle appearance, showing this wasn't actually typed by the person await addTextEvent(body, html, "m.notice") } diff --git a/src/d2m/converters/message-to-event.test.embeds.js b/src/d2m/converters/message-to-event.test.embeds.js index 259aa66..73244d3 100644 --- a/src/d2m/converters/message-to-event.test.embeds.js +++ b/src/d2m/converters/message-to-event.test.embeds.js @@ -204,6 +204,44 @@ test("message2event embeds: author url without name", async t => { }]) }) +test("message2event embeds: 4 images", async t => { + const events = await messageToEvent(data.message_with_embeds.four_images, data.guild.general) + t.deepEqual(events, [{ + $type: "m.room.message", + msgtype: "m.text", + body: "[🔀 Forwarded message]\n» https://fixupx.com/i/status/2032003668787020046", + format: "org.matrix.custom.html", + formatted_body: "🔀 Forwarded message
https://fixupx.com/i/status/2032003668787020046
", + "m.mentions": {} + }, { + $type: "m.room.message", + msgtype: "m.notice", + body: "» | ## ⏺️ AUTOMATON WEST (@AUTOMATON_ENG) https://x.com/AUTOMATON_ENG/status/2032003668787020046" + + "\n» | " + + "\n» | 4chan owner Hiroyuki, Evangelion director Hideaki Anno and GACKT to participate in “humanity’s last non\\-AI made social network”" + + "\n» | ︀︀" + + "\n» | ︀︀[automaton-media.com/en/news/4chan-owner-hiroyuki-evangelion-director-hideaki-anno-and-gackt-to-participate-in-humanitys-last-non-ai-made-social-network/](https://automaton-media.com/en/news/4chan-owner-hiroyuki-evangelion-director-hideaki-anno-and-gackt-to-participate-in-humanitys-last-non-ai-made-social-network/)" + + "\n» | " + + "\n» | **[💬](https://x.com/intent/tweet?in_reply_to=2032003668787020046) 36 [🔁](https://x.com/intent/retweet?tweet_id=2032003668787020046) 212 [❤](https://x.com/intent/like?tweet_id=2032003668787020046) 3\\.0K 👁 131\\.7K **" + + "\n» | " + + "\n» | 📸 https://pbs.twimg.com/media/HDMUyf6bQAM3yts.jpg?name=orig" + + "\n» | — FixupX" + + "\n» | 📸 https://pbs.twimg.com/media/HDMUgxybQAE4FtJ.jpg?name=orig" + + "\n» | 📸 https://pbs.twimg.com/media/HDMUrPobgAAeb90.jpg?name=orig" + + "\n» | 📸 https://pbs.twimg.com/media/HDMUuy5bgAAInj5.jpg?name=orig", + format: "org.matrix.custom.html", + formatted_body: "

⏺️ AUTOMATON WEST (@AUTOMATON_ENG)

" + + "

4chan owner Hiroyuki, Evangelion director Hideaki Anno and GACKT to participate in “humanity’s last non-AI made social network”" + + "
︀︀
︀︀automaton-media.com/en/news/4chan-owner-hiroyuki-evangelion-director-hideaki-anno-and-gackt-to-participate-in-humanitys-last-non-ai-made-social-network/" + + "

💬 36 🔁 212  3.0K 👁 131.7K 

" + + "

📸 https://pbs.twimg.com/media/HDMUyf6bQAM3yts.jpg?name=orig

— FixupX
" + + "

📸 https://pbs.twimg.com/media/HDMUgxybQAE4FtJ.jpg?name=orig

" + + "

📸 https://pbs.twimg.com/media/HDMUrPobgAAeb90.jpg?name=orig

" + + "

📸 https://pbs.twimg.com/media/HDMUuy5bgAAInj5.jpg?name=orig

", + "m.mentions": {} + }]) +}) + test("message2event embeds: vx image", async t => { const events = await messageToEvent(data.message_with_embeds.vx_image, data.guild.general) t.deepEqual(events, [{ diff --git a/src/d2m/converters/message-to-event.test.js b/src/d2m/converters/message-to-event.test.js index 4728c2b..e805365 100644 --- a/src/d2m/converters/message-to-event.test.js +++ b/src/d2m/converters/message-to-event.test.js @@ -1099,7 +1099,7 @@ test("message2event: multiple attachments are combined into the same event where 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)` + + `📄 Uploaded file: hey.jpg (100 MB)` }, { $type: "m.room.message", "m.mentions": {}, diff --git a/test/data.js b/test/data.js index 6a53cb0..46e8b0f 100644 --- a/test/data.js +++ b/test/data.js @@ -5067,6 +5067,141 @@ module.exports = { pinned: false, mention_everyone: false, tts: false + }, + four_images: { + type: 0, + content: "", + mentions: [], + mention_roles: [], + attachments: [], + embeds: [], + timestamp: "2026-03-12T18:00:50.737000+00:00", + edited_timestamp: null, + flags: 16384, + components: [], + id: "1481713598278533241", + channel_id: "687028734322147344", + author: { + id: "112760500130975744", + username: "minimus", + avatar: "a_a354b9eaff512485b49c82b13691b941", + discriminator: "0", + public_flags: 512, + flags: 512, + banner: null, + accent_color: null, + global_name: "minimus", + avatar_decoration_data: null, + collectibles: null, + display_name_styles: { font_id: 11, effect_id: 5, colors: [ 6106655 ] }, + banner_color: null, + clan: null, + primary_guild: null + }, + pinned: false, + mention_everyone: false, + tts: false, + message_reference: { + type: 1, + channel_id: "637339857118822430", + message_id: "1481696763483258891", + guild_id: "408573045540651009" + }, + message_snapshots: [ + { + message: { + type: 0, + content: "https://fixupx.com/i/status/2032003668787020046", + mentions: [], + mention_roles: [], + attachments: [], + embeds: [ + { + type: "rich", + url: "https://fixupx.com/i/status/2032003668787020046", + description: "4chan owner Hiroyuki, Evangelion director Hideaki Anno and GACKT to participate in “humanity’s last non\\-AI made social network”\n" + + "︀︀\n" + + "︀︀[automaton-media.com/en/news/4chan-owner-hiroyuki-evangelion-director-hideaki-anno-and-gackt-to-participate-in-humanitys-last-non-ai-made-social-network/](https://automaton-media.com/en/news/4chan-owner-hiroyuki-evangelion-director-hideaki-anno-and-gackt-to-participate-in-humanitys-last-non-ai-made-social-network/)\n" + + "\n" + + "**[💬](https://x.com/intent/tweet?in_reply_to=2032003668787020046) 36 [🔁](https://x.com/intent/retweet?tweet_id=2032003668787020046) 212 [❤](https://x.com/intent/like?tweet_id=2032003668787020046) 3\\.0K 👁 131\\.7K **", + color: 6513919, + timestamp: "2026-03-12T08:00:02+00:00", + author: { + name: "AUTOMATON WEST (@AUTOMATON_ENG)", + url: "https://x.com/AUTOMATON_ENG/status/2032003668787020046", + icon_url: "https://pbs.twimg.com/profile_images/1353559126693961729/pz-WVnDc_200x200.jpg", + proxy_icon_url: "https://images-ext-1.discordapp.net/external/1OzGhjvZTRstTxM38_7pqHXlmdbMddqh1F8R0-WrKqw/https/pbs.twimg.com/profile_images/1353559126693961729/pz-WVnDc_200x200.jpg" + }, + image: { + url: "https://pbs.twimg.com/media/HDMUyf6bQAM3yts.jpg?name=orig", + proxy_url: "https://images-ext-1.discordapp.net/external/NkNgp2SyY1OCH9IdS8hqsUqbnbrp3A9oLNwYusVVCVQ/%3Fname%3Dorig/https/pbs.twimg.com/media/HDMUyf6bQAM3yts.jpg", + width: 872, + height: 886, + content_type: "image/jpeg", + placeholder: "6vcFFwL6R3lye2V3l1mIl5l3WPN5FZ8H", + placeholder_version: 1, + flags: 0 + }, + footer: { + text: "FixupX", + icon_url: "https://assets.fxembed.com/logos/fixupx64.png", + proxy_icon_url: "https://images-ext-1.discordapp.net/external/LwQ70Uiqfu0OCN4ZbA4f482TGCgQa-xGsnUFYfhIgYA/https/assets.fxembed.com/logos/fixupx64.png" + }, + content_scan_version: 4 + }, + { + type: "rich", + url: "https://fixupx.com/i/status/2032003668787020046", + image: { + url: "https://pbs.twimg.com/media/HDMUgxybQAE4FtJ.jpg?name=orig", + proxy_url: "https://images-ext-1.discordapp.net/external/Rquh1ec-tG9hMqdHqIVSphO7zf5B5Fg_7yTWhCjlsek/%3Fname%3Dorig/https/pbs.twimg.com/media/HDMUgxybQAE4FtJ.jpg", + width: 1114, + height: 991, + content_type: "image/jpeg", + placeholder: "JQgKDoL3epZ8ZIdnlmmHZ4d4CIGmUEc=", + placeholder_version: 1, + flags: 0 + }, + content_scan_version: 4 + }, + { + type: "rich", + url: "https://fixupx.com/i/status/2032003668787020046", + image: { + url: "https://pbs.twimg.com/media/HDMUrPobgAAeb90.jpg?name=orig", + proxy_url: "https://images-ext-1.discordapp.net/external/XrkhHNH3CvlZYvjkdykVnf-_xdz6HWX8uwesoAwwSfY/%3Fname%3Dorig/https/pbs.twimg.com/media/HDMUrPobgAAeb90.jpg", + width: 944, + height: 954, + content_type: "image/jpeg", + placeholder: "m/cJDwCbV0mfaoZzlihqeXdqCVN9A6oD", + placeholder_version: 1, + flags: 0 + }, + content_scan_version: 4 + }, + { + type: "rich", + url: "https://fixupx.com/i/status/2032003668787020046", + image: { + url: "https://pbs.twimg.com/media/HDMUuy5bgAAInj5.jpg?name=orig", + proxy_url: "https://images-ext-1.discordapp.net/external/lO-5hBMU9bGH13Ax9xum2T2Mg0ATdv0b6BEx_VeVi80/%3Fname%3Dorig/https/pbs.twimg.com/media/HDMUuy5bgAAInj5.jpg", + width: 1200, + height: 630, + content_type: "image/jpeg", + placeholder: "tfcJDIK3mIl1eIiPdY23dX9b9w==", + placeholder_version: 1, + flags: 0 + }, + content_scan_version: 4 + } + ], + timestamp: "2026-03-12T16:53:57.009000+00:00", + edited_timestamp: null, + flags: 0, + components: [] + } + } + ] } }, message_with_components: {