diff --git a/package-lock.json b/package-lock.json index aa7822f..803fe53 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "license": "AGPL-3.0-or-later", "dependencies": { "@chriscdn/promise-semaphore": "^3.0.1", - "@cloudrac3r/discord-markdown": "^2.6.6", + "@cloudrac3r/discord-markdown": "^2.6.5", "@cloudrac3r/giframe": "^0.4.3", "@cloudrac3r/html-template-tag": "^5.0.1", "@cloudrac3r/in-your-element": "^1.1.1", @@ -35,7 +35,7 @@ "lru-cache": "^11.0.2", "prettier-bytes": "^1.0.4", "sharp": "^0.33.4", - "snowtransfer": "^0.15.0", + "snowtransfer": "^0.14.2", "stream-mime-type": "^1.0.2", "try-to-catch": "^3.0.1", "uqr": "^0.1.2", @@ -119,9 +119,9 @@ } }, "node_modules/@chriscdn/promise-semaphore": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@chriscdn/promise-semaphore/-/promise-semaphore-3.1.1.tgz", - "integrity": "sha512-ALLLLYlPfd/QZLptcVi6HQRK1zaCDWZoqYYw+axLmCatFs4gVTSZ5nqlyxwFe4qwR/K84HvOMa9hxda881FqMA==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@chriscdn/promise-semaphore/-/promise-semaphore-3.0.1.tgz", + "integrity": "sha512-fVlCnoYE4hDzpcYRPtmN7dmcpmd2zxyPWjyfjIKI9Y+gsI7rwZSkjtuwMi8HFtlkSmNh8L7Zr37hdqeL13sYrw==", "license": "MIT" }, "node_modules/@cloudcmd/stub": { @@ -225,9 +225,9 @@ } }, "node_modules/@cloudrac3r/discord-markdown": { - "version": "2.6.6", - "resolved": "https://registry.npmjs.org/@cloudrac3r/discord-markdown/-/discord-markdown-2.6.6.tgz", - "integrity": "sha512-4FNO7WmACPvcTrQjeLQLr9WRuP7JDUVUGFrRJvmAjiMs2UlUAsShfSRuU2SCqz3QqmX8vyJ06wy2hkjTTyRtbw==", + "version": "2.6.5", + "resolved": "https://registry.npmjs.org/@cloudrac3r/discord-markdown/-/discord-markdown-2.6.5.tgz", + "integrity": "sha512-B4uQNsyva5JNW0CVYkcunMQwWfrok1Hd5FYww/cWcvb98zp/pJdJfE3hoRl9EbnxNK2l62IJQ9j8HmssMFHJ9Q==", "license": "MIT", "dependencies": { "simple-markdown": "^0.7.3" @@ -949,9 +949,9 @@ "license": "MIT" }, "node_modules/@stackoverflow/stacks": { - "version": "2.8.4", - "resolved": "https://registry.npmjs.org/@stackoverflow/stacks/-/stacks-2.8.4.tgz", - "integrity": "sha512-FfA7Bw7a0AQrMw3/bG6G4BUrZ698F7Cdk6HkR9T7jdaufORkiX5d16wI4j4b5Sqm1FwkaZAF+ZSKLL1w0tAsew==", + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/@stackoverflow/stacks/-/stacks-2.8.3.tgz", + "integrity": "sha512-ZGBeuXJC7moK/f+lgl2dCAW85etD/RO0DNubocdH2qzpJMuuGXX0GMeEAfrTOe+B00I8E1OqTnS1cpkqGdHBdQ==", "license": "MIT", "dependencies": { "@hotwired/stimulus": "^3.2.2", @@ -1107,9 +1107,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "22.18.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.18.0.tgz", - "integrity": "sha512-m5ObIqwsUp6BZzyiy4RdZpzWGub9bqLJMvZDD0QMXhxjqMHMENlj+SqF5QxoUwaQNFe+8kz8XM8ZQhqkQPTgMQ==", + "version": "22.17.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.17.1.tgz", + "integrity": "sha512-y3tBaz+rjspDTylNjAX37jEC3TETEFGNJL6uQDxwF9/8GLLIjW1rvVHlynyuUKMnMr1Roq8jOv3vkopBjC4/VA==", "dev": true, "license": "MIT", "dependencies": { @@ -1452,13 +1452,13 @@ } }, "node_modules/cloudstorm": { - "version": "0.14.1", - "resolved": "https://registry.npmjs.org/cloudstorm/-/cloudstorm-0.14.1.tgz", - "integrity": "sha512-x95WCKg818E1rE1Ru45NPD3RoIq0pg3WxwvF0GE7Eq07pAeLcjSRqM1lUmbmfjdOqZrWdSRYA1NETVZ8QhVrIA==", + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/cloudstorm/-/cloudstorm-0.14.0.tgz", + "integrity": "sha512-EgjMGxb2Z+L6Acti6DzL/bEbR495AIqPThyW4DaG6Jpvd0ZuM5eC13EiyxV8wlqAME612QO2LjqbhkdXn/327Q==", "license": "MIT", "dependencies": { - "discord-api-types": "^0.38.21", - "snowtransfer": "^0.15.0" + "discord-api-types": "^0.38.12", + "snowtransfer": "^0.14.2" }, "engines": { "node": ">=22.0.0" @@ -1616,9 +1616,9 @@ } }, "node_modules/discord-api-types": { - "version": "0.38.22", - "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.38.22.tgz", - "integrity": "sha512-2gnYrgXN3yTlv2cKBISI/A8btZwsSZLwKpIQXeI1cS8a7W7wP3sFVQOm3mPuuinTD8jJCKGPGNH399zE7Un1kA==", + "version": "0.38.19", + "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.38.19.tgz", + "integrity": "sha512-NUNMTgjYrgxt7wrTNEqnEez4hIAYbfyBpsjxT5gW7+82GjQCPDZvN+em6t+4/P5kGWnnwDa4ci070BV7eI6GbA==", "license": "MIT", "workspaces": [ "scripts/actions/documentation" @@ -2719,12 +2719,12 @@ } }, "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==", + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/snowtransfer/-/snowtransfer-0.14.2.tgz", + "integrity": "sha512-Fi8OdRmaIgeCj58oVej+tQAoY2I+Xp/6PAYV8X93jE/2E6Anc87SbTbDV6WZXCnuzTQz3gty8JOGz02qI7Qs9A==", "license": "MIT", "dependencies": { - "discord-api-types": "^0.38.21" + "discord-api-types": "^0.38.8" }, "engines": { "node": ">=16.15.0" @@ -3447,9 +3447,9 @@ } }, "node_modules/zod": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.5.tgz", - "integrity": "sha512-rcUUZqlLJgBC33IT3PNMgsCq6TzLQEG/Ei/KTCU0PedSWRMAXoOUN+4t/0H+Q8bdnLPdqUYnvboJT0bn/229qg==", + "version": "4.0.17", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.0.17.tgz", + "integrity": "sha512-1PHjlYRevNxxdy2JZ8JcNAw7rX8V9P1AKkP+x/xZfxB0K5FYfuV+Ug6P/6NVSR2jHQ+FzDDoDHS04nYUsOIyLQ==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" diff --git a/package.json b/package.json index 2fb21f2..a7d3eaa 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ }, "dependencies": { "@chriscdn/promise-semaphore": "^3.0.1", - "@cloudrac3r/discord-markdown": "^2.6.6", + "@cloudrac3r/discord-markdown": "^2.6.5", "@cloudrac3r/giframe": "^0.4.3", "@cloudrac3r/html-template-tag": "^5.0.1", "@cloudrac3r/in-your-element": "^1.1.1", @@ -44,7 +44,7 @@ "lru-cache": "^11.0.2", "prettier-bytes": "^1.0.4", "sharp": "^0.33.4", - "snowtransfer": "^0.15.0", + "snowtransfer": "^0.14.2", "stream-mime-type": "^1.0.2", "try-to-catch": "^3.0.1", "uqr": "^0.1.2", diff --git a/src/d2m/converters/edit-to-changes.js b/src/d2m/converters/edit-to-changes.js index c615a3f..c38c24e 100644 --- a/src/d2m/converters/edit-to-changes.js +++ b/src/d2m/converters/edit-to-changes.js @@ -22,10 +22,6 @@ function eventCanBeEdited(ev) { return true } -function eventIsText(ev) { - return ev.old.event_type === "m.room.message" && (ev.old.event_subtype === "m.text" || ev.old.event_subtype === "m.notice") -} - /** * @param {import("discord-api-types/v10").GatewayMessageCreateDispatchData} message * @param {import("discord-api-types/v10").APIGuild} guild @@ -125,20 +121,6 @@ async function editToChanges(message, guild, api) { unchangedEvents.push(...eventsToReplace.filter(ev => !eventCanBeEdited(ev))) // Move them from eventsToRedact to unchangedEvents. eventsToReplace = eventsToReplace.filter(eventCanBeEdited) - // Now, everything in eventsToReplace has the potential to have changed, but did it actually? - // (Example: if a URL preview was generated or updated, the message text won't have changed.) - // Only way to detect this is by text content. So we'll remove text events from eventsToReplace that have the same new text as text currently in the event. - for (let i = eventsToReplace.length; i--;) { // move backwards through array - const event = eventsToReplace[i] - if (!eventIsText(event)) continue // not text, can't analyse - const oldEvent = await api.getEvent(roomID, eventsToReplace[i].old.event_id) - 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. - unchangedEvents.push(...eventsToReplace.filter(ev => ev.old.event_id === event.old.event_id)) - eventsToReplace = eventsToReplace.filter(ev => ev.old.event_id !== event.old.event_id) - } - // We want to maintain exactly one part = 0 and one reaction_part = 0 database row at all times. // This would be disrupted if existing events that are (reaction_)part = 0 will be redacted. // If that is the case, pick a different existing or newly sent event to be (reaction_)part = 0. @@ -211,3 +193,4 @@ function makeReplacementEventContent(oldID, newFallbackContent, newInnerContent) } module.exports.editToChanges = editToChanges +module.exports.makeReplacementEventContent = makeReplacementEventContent diff --git a/src/d2m/converters/edit-to-changes.test.js b/src/d2m/converters/edit-to-changes.test.js index 30549c7..9721a85 100644 --- a/src/d2m/converters/edit-to-changes.test.js +++ b/src/d2m/converters/edit-to-changes.test.js @@ -4,14 +4,7 @@ const data = require("../../../test/data") const Ty = require("../../types") test("edit2changes: edit by webhook", async t => { - let called = 0 - const {senderMxid, eventsToRedact, eventsToReplace, eventsToSend, promotions} = await editToChanges(data.message_update.edit_by_webhook, data.guild.general, { - getEvent(roomID, eventID) { - called++ - t.equal(eventID, "$zXSlyI78DQqQwwfPUSzZ1b-nXzbUrCDljJgnGDdoI10") - return {content: {body: "dummy"}} - } - }) + const {senderMxid, eventsToRedact, eventsToReplace, eventsToSend, promotions} = await editToChanges(data.message_update.edit_by_webhook, data.guild.general, {}) t.deepEqual(eventsToRedact, []) t.deepEqual(eventsToSend, []) t.deepEqual(eventsToReplace, [{ @@ -35,15 +28,10 @@ test("edit2changes: edit by webhook", async t => { }]) t.equal(senderMxid, null) t.deepEqual(promotions, []) - t.equal(called, 1) }) 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"}} - }, async getJoinedMembers(roomID) { t.equal(roomID, "!hYnGGlPHlbujVVfktC:cadence.moe") return new Promise(resolve => { @@ -135,14 +123,7 @@ test("edit2changes: add caption back to that image (due to it having a reaction, }) test("edit2changes: stickers and attachments are not changed, only the content can be edited", async t => { - let called = 0 - const {eventsToRedact, eventsToReplace, eventsToSend} = await editToChanges(data.message_update.edited_content_with_sticker_and_attachments, data.guild.general, { - getEvent(roomID, eventID) { - called++ - t.equal(eventID, "$lnAF9IosAECTnlv9p2e18FG8rHn-JgYKHEHIh5qdFv4") - return {content: {body: "dummy"}} - } - }) + const {eventsToRedact, eventsToReplace, eventsToSend} = await editToChanges(data.message_update.edited_content_with_sticker_and_attachments, data.guild.general, {}) t.deepEqual(eventsToRedact, []) t.deepEqual(eventsToSend, []) t.deepEqual(eventsToReplace, [{ @@ -164,16 +145,10 @@ test("edit2changes: stickers and attachments are not changed, only the content c } } }]) - t.equal(called, 1) }) test("edit2changes: edit of reply to skull webp attachment with content", async t => { - const {eventsToRedact, eventsToReplace, eventsToSend} = await editToChanges(data.message_update.edit_of_reply_to_skull_webp_attachment_with_content, data.guild.general, { - getEvent(roomID, eventID) { - t.equal(eventID, "$vgTKOR5ZTYNMKaS7XvgEIDaOWZtVCEyzLLi5Pc5Gz4M") - return {content: {body: "dummy"}} - } - }) + const {eventsToRedact, eventsToReplace, eventsToSend} = await editToChanges(data.message_update.edit_of_reply_to_skull_webp_attachment_with_content, data.guild.general, {}) t.deepEqual(eventsToRedact, []) t.deepEqual(eventsToSend, []) t.deepEqual(eventsToReplace, [{ @@ -202,12 +177,7 @@ test("edit2changes: edit of reply to skull webp attachment with content", async }) test("edit2changes: edits the text event when multiple rows have part = 0 (should never happen in real life, but make sure the safety net works)", async t => { - const {eventsToRedact, eventsToReplace, eventsToSend} = await editToChanges(data.message_update.edited_content_with_sticker_and_attachments_but_all_parts_equal_0, data.guild.general, { - getEvent(roomID, eventID) { - t.equal(eventID, "$lnAF9IosAECTnlv9p2e18FG8rHn-JgYKHEHIh5qd999") - return {content: {body: "dummy"}} - } - }) + const {eventsToRedact, eventsToReplace, eventsToSend} = await editToChanges(data.message_update.edited_content_with_sticker_and_attachments_but_all_parts_equal_0, data.guild.general, {}) t.deepEqual(eventsToRedact, []) t.deepEqual(eventsToSend, []) t.deepEqual(eventsToReplace, [{ @@ -232,12 +202,7 @@ test("edit2changes: edits the text event when multiple rows have part = 0 (shoul }) test("edit2changes: promotes the text event when multiple rows have part = 1 (should never happen in real life, but make sure the safety net works)", async t => { - const {eventsToRedact, eventsToReplace, eventsToSend, promotions} = await editToChanges(data.message_update.edited_content_with_sticker_and_attachments_but_all_parts_equal_1, data.guild.general, { - getEvent(roomID, eventID) { - t.equal(eventID, "$lnAF9IosAECTnlv9p2e18FG8rHn-JgYKHEHIh5qd111") - return {content: {body: "dummy"}} - } - }) + const {eventsToRedact, eventsToReplace, eventsToSend, promotions} = await editToChanges(data.message_update.edited_content_with_sticker_and_attachments_but_all_parts_equal_1, data.guild.general, {}) t.deepEqual(eventsToRedact, []) t.deepEqual(eventsToSend, []) t.deepEqual(eventsToReplace, [{ @@ -314,31 +279,32 @@ test("edit2changes: generated embed", async t => { }) test("edit2changes: generated embed on a reply", async t => { - let called = 0 - const {senderMxid, eventsToRedact, eventsToReplace, eventsToSend, promotions} = await editToChanges(data.message_update.embed_generated_on_reply, data.guild.general, { - getEvent(roomID, eventID) { - called++ - t.equal(eventID, "$UTqiL3Zj3FC4qldxRLggN1fhygpKl8sZ7XGY5f9MNbF") - return { - type: "m.room.message", - content: { - // Unfortunately the edited message doesn't include the message_reference field. Fine. Whatever. It looks normal if you're using a good client. - body: "> a Discord user: [Replied-to message content wasn't provided by Discord]" - + "\n\nhttps://matrix.to/#/!BnKuBPCvyfOkhcUjEu:cadence.moe/$aLVZyiC3HlOu-prCSIaXlQl68I8leUdnPFiCwkgn6qM", - format: "org.matrix.custom.html", - formatted_body: "
In reply to a Discord user
[Replied-to message content wasn't provided by Discord]
https://matrix.to/#/!BnKuBPCvyfOkhcUjEu:cadence.moe/$aLVZyiC3HlOu-prCSIaXlQl68I8leUdnPFiCwkgn6qM", - "m.mentions": {}, - "m.relates_to": { - event_id: "$UTqiL3Zj3FC4qldxRLggN1fhygpKl8sZ7XGY5f9MNbF", - rel_type: "m.replace", - }, - msgtype: "m.text", - } - } - } - }) + const {senderMxid, eventsToRedact, eventsToReplace, eventsToSend, promotions} = await editToChanges(data.message_update.embed_generated_on_reply, data.guild.general, {}) t.deepEqual(eventsToRedact, []) - t.deepEqual(eventsToReplace, []) + t.deepEqual(eventsToReplace, [{ + oldID: "$UTqiL3Zj3FC4qldxRLggN1fhygpKl8sZ7XGY5f9MNbF", + newContent: { + $type: "m.room.message", + // Unfortunately the edited message doesn't include the message_reference field. Fine. Whatever. It looks normal if you're using a good client. + body: "> a Discord user: [Replied-to message content wasn't provided by Discord]" + + "\n\n* https://matrix.to/#/!BnKuBPCvyfOkhcUjEu:cadence.moe/$aLVZyiC3HlOu-prCSIaXlQl68I8leUdnPFiCwkgn6qM", + format: "org.matrix.custom.html", + formatted_body: "
In reply to a Discord user
[Replied-to message content wasn't provided by Discord]
* https://matrix.to/#/!BnKuBPCvyfOkhcUjEu:cadence.moe/$aLVZyiC3HlOu-prCSIaXlQl68I8leUdnPFiCwkgn6qM", + "m.mentions": {}, + "m.new_content": { + body: "https://matrix.to/#/!BnKuBPCvyfOkhcUjEu:cadence.moe/$aLVZyiC3HlOu-prCSIaXlQl68I8leUdnPFiCwkgn6qM", + format: "org.matrix.custom.html", + formatted_body: "https://matrix.to/#/!BnKuBPCvyfOkhcUjEu:cadence.moe/$aLVZyiC3HlOu-prCSIaXlQl68I8leUdnPFiCwkgn6qM", + "m.mentions": {}, + msgtype: "m.text", + }, + "m.relates_to": { + event_id: "$UTqiL3Zj3FC4qldxRLggN1fhygpKl8sZ7XGY5f9MNbF", + rel_type: "m.replace", + }, + msgtype: "m.text", + }, + }]) t.deepEqual(eventsToSend, [{ $type: "m.room.message", msgtype: "m.notice", @@ -358,5 +324,4 @@ test("edit2changes: generated embed on a reply", async t => { "nextEvent": true, }]) t.equal(senderMxid, "@_ooye_cadence:cadence.moe") - t.equal(called, 1) })