From af9e2d89a5362c52402add2491d01c6ba8eb3041 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Fri, 30 Jan 2026 20:01:08 +1300 Subject: [PATCH] Wrangle generated embeds; fix edit m.mentions --- package.json | 2 +- src/d2m/converters/edit-to-changes.js | 38 ++++++++++++++-------- src/d2m/converters/edit-to-changes.test.js | 10 +++++- 3 files changed, 35 insertions(+), 15 deletions(-) diff --git a/package.json b/package.json index 1cad178..d0a154c 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "author": "Cadence, PapiOphidian", "license": "AGPL-3.0-or-later", "engines": { - "node": ">=20" + "node": ">=22" }, "dependencies": { "@chriscdn/promise-semaphore": "^3.0.1", diff --git a/src/d2m/converters/edit-to-changes.js b/src/d2m/converters/edit-to-changes.js index 48b7dd3..86a4028 100644 --- a/src/d2m/converters/edit-to-changes.js +++ b/src/d2m/converters/edit-to-changes.js @@ -46,7 +46,7 @@ async function editToChanges(message, guild, api) { // Now, this code path is only used by generated embeds for messages that were originally sent from Matrix. const originallyFromMatrix = oldEventRows.find(r => r.part === 0)?.source === 0 - const isGeneratedEmbed = !("content" in message) || originallyFromMatrix + const mightBeGeneratedEmbed = !("content" in message) || originallyFromMatrix // Figure out who to send as @@ -79,7 +79,7 @@ async function editToChanges(message, guild, api) { */ /** * 1. Events that are matched, and should be edited by sending another m.replace event - * @type {{old: typeof oldEventRows[0], newFallbackContent: typeof newFallbackContent[0], newInnerContent: typeof newInnerContent[0]}[]} + * @type {{old: typeof oldEventRows[0], oldMentions?: any, newFallbackContent: typeof newFallbackContent[0], newInnerContent: typeof newInnerContent[0]}[]} */ let eventsToReplace = [] /** @@ -133,20 +133,20 @@ async function editToChanges(message, guild, api) { // If this is a generated embed update, only allow the embeds to be updated, since the system only sends data about events. Ignore changes to other things. // This also prevents Matrix events that were re-subtyped during conversion (e.g. large image -> text link) from being mistakenly included. - if (isGeneratedEmbed) { + if (mightBeGeneratedEmbed) { unchangedEvents = unchangedEvents.concat( dUtils.filterTo(eventsToRedact, e => e.old.event_subtype === "m.notice" && e.old.source === 1), // Move everything except embeds from eventsToRedact to unchangedEvents. dUtils.filterTo(eventsToReplace, e => e.old.event_subtype === "m.notice" && e.old.source === 1) // Move everything except embeds from eventsToReplace to unchangedEvents. ) eventsToSend = eventsToSend.filter(e => e.msgtype === "m.notice") // Don't send new events that aren't the embed. + } - // Don't post new generated embeds for messages if it's been a while since the message was sent. Detached embeds look weird. - const messageTooOld = message.timestamp && new Date(message.timestamp).getTime() < Date.now() - 30 * 1000 // older than 30 seconds ago - // Don't post new generated embeds for messages if the setting was disabled. - const embedsEnabled = select("guild_space", "url_preview", {guild_id: guild?.id}).pluck().get() ?? 1 - if (messageTooOld || !embedsEnabled) { - eventsToSend = [] - } + // Don't post new generated embeds for messages if it's been a while since the message was sent. Detached embeds look weird. + const messageTooOld = message.timestamp && new Date(message.timestamp).getTime() < Date.now() - 30 * 1000 // older than 30 seconds ago + // Don't post new generated embeds for messages if the setting was disabled. + const embedsEnabled = select("guild_space", "url_preview", {guild_id: guild?.id}).pluck().get() ?? 1 + if ((messageTooOld || !embedsEnabled) && !message.author.bot) { + eventsToSend = eventsToSend.filter(e => e.msgtype !== "m.notice") // Only send events that aren't embeds. } // Now, everything in eventsToSend and eventsToRedact is a real change, but everything in eventsToReplace might not have actually changed! @@ -161,6 +161,7 @@ async function editToChanges(message, guild, api) { const event = eventsToReplace[i] if (!eventIsText(event)) continue // not text, can't analyse const oldEvent = await api.getEvent(roomID, eventsToReplace[i].old.event_id) + eventsToReplace[i].oldMentions = oldEvent.content["m.mentions"] const oldEventBodyWithoutQuotedReply = oldEvent.content.body?.replace(/^(>.*\n)*\n*/sm, "") if (oldEventBodyWithoutQuotedReply !== event.newInnerContent.body) continue // event changed, must replace it // Move it from eventsToRedact to unchangedEvents. @@ -210,7 +211,7 @@ async function editToChanges(message, guild, api) { // Removing unnecessary properties before returning return { roomID, - eventsToReplace: eventsToReplace.map(e => ({oldID: e.old.event_id, newContent: makeReplacementEventContent(e.old.event_id, e.newFallbackContent, e.newInnerContent)})), + eventsToReplace: eventsToReplace.map(e => ({oldID: e.old.event_id, newContent: makeReplacementEventContent(e.old.event_id, e.oldMentions, e.newFallbackContent, e.newInnerContent)})), eventsToRedact: eventsToRedact.map(e => e.old.event_id), eventsToSend, senderMxid, @@ -221,14 +222,25 @@ async function editToChanges(message, guild, api) { /** * @template T * @param {string} oldID + * @param {any} oldMentions * @param {T} newFallbackContent * @param {T} newInnerContent * @returns {import("../../types").Event.ReplacementContent} content */ -function makeReplacementEventContent(oldID, newFallbackContent, newInnerContent) { +function makeReplacementEventContent(oldID, oldMentions, newFallbackContent, newInnerContent) { + const mentions = {} + const newMentionUsers = new Set(newFallbackContent["m.mentions"]?.user_ids || []) + const oldMentionUsers = new Set(oldMentions?.user_ids || []) + const mentionDiff = newMentionUsers.difference(oldMentionUsers) + if (mentionDiff.size) { + mentions.user_ids = [...mentionDiff.values()] + } + if (newFallbackContent["m.mentions"]?.room && !oldMentions?.room) { + mentions.room = true + } const content = { - "m.mentions": {}, ...newFallbackContent, + "m.mentions": mentions, "m.new_content": { ...newInnerContent }, diff --git a/src/d2m/converters/edit-to-changes.test.js b/src/d2m/converters/edit-to-changes.test.js index d687702..0fd85b8 100644 --- a/src/d2m/converters/edit-to-changes.test.js +++ b/src/d2m/converters/edit-to-changes.test.js @@ -42,7 +42,14 @@ test("edit2changes: bot response", async t => { const {senderMxid, eventsToRedact, eventsToReplace, eventsToSend, promotions} = await editToChanges(data.message_update.bot_response, data.guild.general, { getEvent(roomID, eventID) { t.equal(eventID, "$fdD9OZ55xg3EAsfvLZza5tMhtjUO91Wg3Otuo96TplY") - return {content: {body: "dummy"}} + return { + content: { + "m.mentions": { + user_ids: ["@cadence:cadence.moe"], + }, + body: "dummy" + } + } }, async getJoinedMembers(roomID) { t.equal(roomID, "!hYnGGlPHlbujVVfktC:cadence.moe") @@ -365,6 +372,7 @@ test("edit2changes: generated embed", async t => { test("edit2changes: generated embed on a reply", async t => { let called = 0 + data.message_update.embed_generated_on_reply.timestamp = new Date().toISOString() const {senderMxid, eventsToRedact, eventsToReplace, eventsToSend, promotions} = await editToChanges(data.message_update.embed_generated_on_reply, data.guild.general, { getEvent(roomID, eventID) { called++