diff --git a/d2m/actions/edit-message.js b/d2m/actions/edit-message.js index 5cdd690..a1538b9 100644 --- a/d2m/actions/edit-message.js +++ b/d2m/actions/edit-message.js @@ -1,7 +1,7 @@ // @ts-check const passthrough = require("../../passthrough") -const { sync, db } = passthrough +const {sync, db, select} = passthrough /** @type {import("../converters/edit-to-changes")} */ const editToChanges = sync.require("../converters/edit-to-changes") /** @type {import("../../matrix/api")} */ @@ -12,7 +12,7 @@ const api = sync.require("../../matrix/api") * @param {import("discord-api-types/v10").APIGuild} guild */ async function editMessage(message, guild) { - const {roomID, eventsToRedact, eventsToReplace, eventsToSend, senderMxid} = await editToChanges.editToChanges(message, guild, api) + const {roomID, eventsToRedact, eventsToReplace, eventsToSend, senderMxid, promoteEvent, promoteNextEvent} = await editToChanges.editToChanges(message, guild, api) // 1. Replace all the things. for (const {oldID, newContent} of eventsToReplace) { @@ -33,10 +33,17 @@ async function editMessage(message, guild) { for (const eventID of eventsToRedact) { await api.redactEvent(roomID, eventID, senderMxid) db.prepare("DELETE FROM event_message WHERE event_id = ?").run(eventID) - // TODO: If I just redacted part = 0, I should update one of the other events to make it the new part = 0, right? } - // 3. Send all the things. + // 3. Consistency: Ensure there is exactly one part = 0 + let eventPart = 1 + if (promoteEvent) { + db.prepare("UPDATE event_message SET part = 0 WHERE event_id = ?").run(promoteEvent) + } else if (promoteNextEvent) { + eventPart = 0 + } + + // 4. Send all the things. for (const content of eventsToSend) { const eventType = content.$type /** @type {Pick> & { $type?: string }} */ @@ -44,7 +51,9 @@ async function editMessage(message, guild) { delete contentWithoutType.$type const eventID = await api.sendEvent(roomID, eventType, contentWithoutType, senderMxid) - db.prepare("INSERT INTO event_message (event_id, event_type, event_subtype, message_id, part, source) VALUES (?, ?, ?, ?, 1, 1)").run(eventID, eventType, content.msgtype || null, message.id) // part 1 = supporting; source 1 = discord + db.prepare("INSERT INTO event_message (event_id, event_type, event_subtype, message_id, part, source) VALUES (?, ?, ?, ?, ?, 1)").run(eventID, eventType, content.msgtype || null, message.id, eventPart) // part 1 = supporting; source 1 = discord + + eventPart = 1 } } diff --git a/d2m/converters/edit-to-changes.js b/d2m/converters/edit-to-changes.js index 8df3979..6d4bbdd 100644 --- a/d2m/converters/edit-to-changes.js +++ b/d2m/converters/edit-to-changes.js @@ -83,6 +83,20 @@ async function editToChanges(message, guild, api) { // Anything remaining in oldEventRows is present in the old version only and should be redacted. eventsToRedact = oldEventRows + // If events are being deleted, we might be deleting the part = 0. But we want to have a part = 0 at all times. In this case we choose an existing event to promote. + let promoteEvent = null, promoteNextEvent = false + if (eventsToRedact.some(e => e.part === 0)) { + if (eventsToReplace.length) { + // We can choose an existing event to promote. Bigger order is better. + const order = e => 2*+(e.event_type === "m.room.message") + 1*+(e.event_subtype === "m.text") + eventsToReplace.sort((a, b) => order(b) - order(a)) + promoteEvent = eventsToReplace[0].old.event_id + } else { + // Everything is being deleted. Whatever gets sent in their place will be the new part = 0. + promoteNextEvent = true + } + } + // Now, everything in eventsToSend and eventsToRedact is a real change, but everything in eventsToReplace might not have actually changed! // (Example: a MESSAGE_UPDATE for a text+image message - Discord does not allow the image to be changed, but the text might have been.) // So we'll remove entries from eventsToReplace that *definitely* cannot have changed. (This is category 4 mentioned above.) Everything remaining *may* have changed. @@ -103,7 +117,7 @@ async function editToChanges(message, guild, api) { eventsToRedact = eventsToRedact.map(e => e.event_id) eventsToReplace = eventsToReplace.map(e => ({oldID: e.old.event_id, newContent: makeReplacementEventContent(e.old.event_id, e.newFallbackContent, e.newInnerContent)})) - return {roomID, eventsToReplace, eventsToRedact, eventsToSend, senderMxid} + return {roomID, eventsToReplace, eventsToRedact, eventsToSend, senderMxid, promoteEvent, promoteNextEvent} } /** diff --git a/d2m/converters/edit-to-changes.test.js b/d2m/converters/edit-to-changes.test.js index 449af98..c0762c7 100644 --- a/d2m/converters/edit-to-changes.test.js +++ b/d2m/converters/edit-to-changes.test.js @@ -4,7 +4,7 @@ const data = require("../../test/data") const Ty = require("../../types") test("edit2changes: edit by webhook", async t => { - const {senderMxid, eventsToRedact, eventsToReplace, eventsToSend} = await editToChanges(data.message_update.edit_by_webhook, data.guild.general, {}) + const {senderMxid, eventsToRedact, eventsToReplace, eventsToSend, promoteEvent, promoteNextEvent} = await editToChanges(data.message_update.edit_by_webhook, data.guild.general, {}) t.deepEqual(eventsToRedact, []) t.deepEqual(eventsToSend, []) t.deepEqual(eventsToReplace, [{ @@ -27,10 +27,12 @@ test("edit2changes: edit by webhook", async t => { } }]) t.equal(senderMxid, null) + t.equal(promoteEvent, null) + t.equal(promoteNextEvent, false) }) test("edit2changes: bot response", async t => { - const {senderMxid, eventsToRedact, eventsToReplace, eventsToSend} = await editToChanges(data.message_update.bot_response, data.guild.general, { + const {senderMxid, eventsToRedact, eventsToReplace, eventsToSend, promoteEvent, promoteNextEvent} = await editToChanges(data.message_update.bot_response, data.guild.general, { async getJoinedMembers(roomID) { t.equal(roomID, "!hYnGGlPHlbujVVfktC:cadence.moe") return new Promise(resolve => { @@ -82,17 +84,21 @@ test("edit2changes: bot response", async t => { } }]) t.equal(senderMxid, "@_ooye_bojack_horseman:cadence.moe") + t.equal(promoteEvent, null) + t.equal(promoteNextEvent, false) }) test("edit2changes: remove caption from image", async t => { - const {eventsToRedact, eventsToReplace, eventsToSend} = await editToChanges(data.message_update.removed_caption_from_image, data.guild.general, {}) + const {eventsToRedact, eventsToReplace, eventsToSend, promoteEvent, promoteNextEvent} = await editToChanges(data.message_update.removed_caption_from_image, data.guild.general, {}) t.deepEqual(eventsToRedact, ["$mtR8cJqM4fKno1bVsm8F4wUVqSntt2sq6jav1lyavuA"]) t.deepEqual(eventsToSend, []) t.deepEqual(eventsToReplace, []) + t.equal(promoteEvent, "$51f4yqHinwnSbPEQ9dCgoyy4qiIJSX0QYYVUnvwyTCI") + t.equal(promoteNextEvent, false) }) test("edit2changes: add caption back to that image", async t => { - const {eventsToRedact, eventsToReplace, eventsToSend} = await editToChanges(data.message_update.added_caption_to_image, data.guild.general, {}) + const {eventsToRedact, eventsToReplace, eventsToSend, promoteEvent, promoteNextEvent} = await editToChanges(data.message_update.added_caption_to_image, data.guild.general, {}) t.deepEqual(eventsToRedact, []) t.deepEqual(eventsToSend, [{ $type: "m.room.message", @@ -101,6 +107,8 @@ test("edit2changes: add caption back to that image", async t => { "m.mentions": {} }]) t.deepEqual(eventsToReplace, []) + t.equal(promoteEvent, null) + t.equal(promoteNextEvent, false) }) test("edit2changes: stickers and attachments are not changed, only the content can be edited", async t => {