From 58d8ccf6a7d1616af89bfee1027dac44f99ba1e9 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Thu, 17 Aug 2023 18:27:13 +1200 Subject: [PATCH 1/5] actually call the function lol --- d2m/discord-packets.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/d2m/discord-packets.js b/d2m/discord-packets.js index 3786393..b8c0af6 100644 --- a/d2m/discord-packets.js +++ b/d2m/discord-packets.js @@ -67,6 +67,8 @@ const utils = { } else if (message.t === "MESSAGE_CREATE") { eventDispatcher.onMessageCreate(client, message.d) + } else if (message.t === "MESSAGE_UPDATE") { + eventDispatcher.onMessageUpdate(client, message.d) } else if (message.t === "MESSAGE_REACTION_ADD") { eventDispatcher.onReactionAdd(client, message.d) From 09b7ba570c69bf3365fee8feeff7d26a00a63782 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Thu, 17 Aug 2023 19:03:09 +1200 Subject: [PATCH 2/5] test that edits by webhook work --- d2m/converters/edit-to-changes.js | 9 +- d2m/converters/edit-to-changes.test.js | 205 ++++++++++++++----------- d2m/event-dispatcher.js | 7 + db/ooye.db | Bin 360448 -> 360448 bytes scripts/events.db | Bin 192512 -> 196608 bytes test/data.js | 27 ++++ 6 files changed, 158 insertions(+), 90 deletions(-) diff --git a/d2m/converters/edit-to-changes.js b/d2m/converters/edit-to-changes.js index 4e6892d..3f4b2d2 100644 --- a/d2m/converters/edit-to-changes.js +++ b/d2m/converters/edit-to-changes.js @@ -22,7 +22,14 @@ async function editToChanges(message, guild, api) { // Figure out what events we will be replacing const roomID = db.prepare("SELECT room_id FROM channel_room WHERE channel_id = ?").pluck().get(message.channel_id) - const senderMxid = await registerUser.ensureSimJoined(message.author, roomID) + /** @type {string?} */ + let senderMxid = db.prepare("SELECT mxid FROM sim WHERE discord_id = ?").pluck().get(message.author.id) ?? null + if (senderMxid) { + const senderIsInRoom = db.prepare("SELECT * FROM sim_member WHERE room_id = ? and mxid = ?").get(roomID, senderMxid) + if (!senderIsInRoom) { + senderMxid = null // just send as ooye bot + } + } /** @type {{event_id: string, event_type: string, event_subtype: string?, part: number}[]} */ const oldEventRows = db.prepare("SELECT event_id, event_type, event_subtype, part FROM event_message WHERE message_id = ?").all(message.id) diff --git a/d2m/converters/edit-to-changes.test.js b/d2m/converters/edit-to-changes.test.js index bb3f3ec..674cb15 100644 --- a/d2m/converters/edit-to-changes.test.js +++ b/d2m/converters/edit-to-changes.test.js @@ -3,104 +3,131 @@ const {editToChanges} = require("./edit-to-changes") 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, {}) + t.deepEqual(eventsToRedact, []) + t.deepEqual(eventsToSend, []) + t.deepEqual(eventsToReplace, [{ + oldID: "$zXSlyI78DQqQwwfPUSzZ1b-nXzbUrCDljJgnGDdoI10", + newContent: { + $type: "m.room.message", + msgtype: "m.text", + body: "* test 2", + "m.mentions": {}, + "m.new_content": { + // *** Replaced With: *** + msgtype: "m.text", + body: "test 2", + "m.mentions": {} + }, + "m.relates_to": { + rel_type: "m.replace", + event_id: "$zXSlyI78DQqQwwfPUSzZ1b-nXzbUrCDljJgnGDdoI10" + } + } + }]) + t.equal(senderMxid, null) +}) + test("edit2changes: bot response", async t => { - const {eventsToRedact, eventsToReplace, eventsToSend} = await editToChanges(data.message_update.bot_response, data.guild.general, { - async getJoinedMembers(roomID) { - t.equal(roomID, "!uCtjHhfGlYbVnPVlkG:cadence.moe") - return new Promise(resolve => { - setTimeout(() => { - resolve({ - joined: { - "@cadence:cadence.moe": { - display_name: "cadence [they]", - avatar_url: "whatever" - }, - "@_ooye_botrac4r:cadence.moe": { - display_name: "botrac4r", - avatar_url: "whatever" - } - } - }) - }) - }) - } - }) - t.deepEqual(eventsToRedact, []) - t.deepEqual(eventsToSend, []) - t.deepEqual(eventsToReplace, [{ - oldID: "$fdD9OZ55xg3EAsfvLZza5tMhtjUO91Wg3Otuo96TplY", - newContent: { - $type: "m.room.message", - msgtype: "m.text", - body: "* :ae_botrac4r: @cadence asked ``­``, I respond: Stop drinking paint. (No)\n\nHit :bn_re: to reroll.", - format: "org.matrix.custom.html", - formatted_body: '* :ae_botrac4r: @cadence asked ­, I respond: Stop drinking paint. (No)

Hit :bn_re: to reroll.', - "m.mentions": { - // Client-Server API spec 11.37.7: Copy Discord's behaviour by not re-notifying anyone that an *edit occurred* - }, - // *** Replaced With: *** - "m.new_content": { - msgtype: "m.text", - body: ":ae_botrac4r: @cadence asked ``­``, I respond: Stop drinking paint. (No)\n\nHit :bn_re: to reroll.", - format: "org.matrix.custom.html", - formatted_body: ':ae_botrac4r: @cadence asked ­, I respond: Stop drinking paint. (No)

Hit :bn_re: to reroll.', - "m.mentions": { - // Client-Server API spec 11.37.7: This should contain the mentions for the final version of the event - "user_ids": ["@cadence:cadence.moe"] - } - }, - "m.relates_to": { - rel_type: "m.replace", - event_id: "$fdD9OZ55xg3EAsfvLZza5tMhtjUO91Wg3Otuo96TplY" - } - } - }]) + const {senderMxid, eventsToRedact, eventsToReplace, eventsToSend} = await editToChanges(data.message_update.bot_response, data.guild.general, { + async getJoinedMembers(roomID) { + t.equal(roomID, "!uCtjHhfGlYbVnPVlkG:cadence.moe") + return new Promise(resolve => { + setTimeout(() => { + resolve({ + joined: { + "@cadence:cadence.moe": { + display_name: "cadence [they]", + avatar_url: "whatever" + }, + "@_ooye_botrac4r:cadence.moe": { + display_name: "botrac4r", + avatar_url: "whatever" + } + } + }) + }) + }) + } + }) + t.deepEqual(eventsToRedact, []) + t.deepEqual(eventsToSend, []) + t.deepEqual(eventsToReplace, [{ + oldID: "$fdD9OZ55xg3EAsfvLZza5tMhtjUO91Wg3Otuo96TplY", + newContent: { + $type: "m.room.message", + msgtype: "m.text", + body: "* :ae_botrac4r: @cadence asked ``­``, I respond: Stop drinking paint. (No)\n\nHit :bn_re: to reroll.", + format: "org.matrix.custom.html", + formatted_body: '* :ae_botrac4r: @cadence asked ­, I respond: Stop drinking paint. (No)

Hit :bn_re: to reroll.', + "m.mentions": { + // Client-Server API spec 11.37.7: Copy Discord's behaviour by not re-notifying anyone that an *edit occurred* + }, + // *** Replaced With: *** + "m.new_content": { + msgtype: "m.text", + body: ":ae_botrac4r: @cadence asked ``­``, I respond: Stop drinking paint. (No)\n\nHit :bn_re: to reroll.", + format: "org.matrix.custom.html", + formatted_body: ':ae_botrac4r: @cadence asked ­, I respond: Stop drinking paint. (No)

Hit :bn_re: to reroll.', + "m.mentions": { + // Client-Server API spec 11.37.7: This should contain the mentions for the final version of the event + "user_ids": ["@cadence:cadence.moe"] + } + }, + "m.relates_to": { + rel_type: "m.replace", + event_id: "$fdD9OZ55xg3EAsfvLZza5tMhtjUO91Wg3Otuo96TplY" + } + } + }]) + t.equal(senderMxid, "@_ooye_bojack_horseman:cadence.moe") }) test("edit2changes: remove caption from image", async t => { - const {eventsToRedact, eventsToReplace, eventsToSend} = await editToChanges(data.message_update.removed_caption_from_image, data.guild.general, {}) - t.deepEqual(eventsToRedact, ["$mtR8cJqM4fKno1bVsm8F4wUVqSntt2sq6jav1lyavuA"]) - t.deepEqual(eventsToSend, []) - t.deepEqual(eventsToReplace, []) + const {eventsToRedact, eventsToReplace, eventsToSend} = await editToChanges(data.message_update.removed_caption_from_image, data.guild.general, {}) + t.deepEqual(eventsToRedact, ["$mtR8cJqM4fKno1bVsm8F4wUVqSntt2sq6jav1lyavuA"]) + t.deepEqual(eventsToSend, []) + t.deepEqual(eventsToReplace, []) }) 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, {}) - t.deepEqual(eventsToRedact, []) - t.deepEqual(eventsToSend, [{ - $type: "m.room.message", - msgtype: "m.text", - body: "some text", - "m.mentions": {} - }]) - t.deepEqual(eventsToReplace, []) + const {eventsToRedact, eventsToReplace, eventsToSend} = await editToChanges(data.message_update.added_caption_to_image, data.guild.general, {}) + t.deepEqual(eventsToRedact, []) + t.deepEqual(eventsToSend, [{ + $type: "m.room.message", + msgtype: "m.text", + body: "some text", + "m.mentions": {} + }]) + t.deepEqual(eventsToReplace, []) }) 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, {}) + 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, [{ - oldID: "$vgTKOR5ZTYNMKaS7XvgEIDaOWZtVCEyzLLi5Pc5Gz4M", - 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', - "m.mentions": {}, - "m.new_content": { - msgtype: "m.text", - body: "Edit", - "m.mentions": {} - }, - "m.relates_to": { - rel_type: "m.replace", - event_id: "$vgTKOR5ZTYNMKaS7XvgEIDaOWZtVCEyzLLi5Pc5Gz4M" - } - } - }]) + t.deepEqual(eventsToSend, []) + t.deepEqual(eventsToReplace, [{ + oldID: "$vgTKOR5ZTYNMKaS7XvgEIDaOWZtVCEyzLLi5Pc5Gz4M", + 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', + "m.mentions": {}, + "m.new_content": { + msgtype: "m.text", + body: "Edit", + "m.mentions": {} + }, + "m.relates_to": { + rel_type: "m.replace", + event_id: "$vgTKOR5ZTYNMKaS7XvgEIDaOWZtVCEyzLLi5Pc5Gz4M" + } + } + }]) }) diff --git a/d2m/event-dispatcher.js b/d2m/event-dispatcher.js index 4bb94ff..1527b28 100644 --- a/d2m/event-dispatcher.js +++ b/d2m/event-dispatcher.js @@ -37,6 +37,13 @@ module.exports = { * @param {import("discord-api-types/v10").GatewayMessageUpdateDispatchData} message */ onMessageUpdate(client, data) { + if (data.webhook_id) { + const row = db.prepare("SELECT webhook_id FROM webhook WHERE webhook_id = ?").pluck().get(message.webhook_id) + if (row) { + // The update was sent by the bridge's own webhook on discord. We don't want to reflect this back, so just drop it. + return + } + } // Based on looking at data they've sent me over the gateway, this is the best way to check for meaningful changes. // If the message content is a string then it includes all interesting fields and is meaningful. if (typeof data.content === "string") { diff --git a/db/ooye.db b/db/ooye.db index 93ae79171b0cc07a2e60eaba6613a2c03cdfce6f..1c52510cc5f1d7902d1e6a05829750a2a0dfba70 100644 GIT binary patch delta 2770 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 delta 1473 zcmZuwdrXyO7=JI%d(L;x`7U61Fu^1@89N}zC17TpqaD6dz@W@EE{XTjP+@p21J`nh zcmxjkG&Kdpt^QzYBp<>oHrLi#;SsZ3TbZ>naAK@!7pKsB!1}9id!K#J@43Fu?|Ex$ z-L=iA(&UM1O30$e&X;$tj@MmX5+KXfIxSWYxVqFK?SXbq+o=1Ed9FY8 z8%B@u7ToLhlrM4x;}Y$*8GhK1PJ^k5i-TQThH>Kz?gbe?8v~oT8H}xP;?13qV$#iMIvGpXjg7Aeymf)BG^ONmR&a=tIpgr_#&>!+J7Tvuybe7_V zncVDNH`G4InN50$k)n_4r-QNM%j2K{Od!0ESMy{x!a7(fbJGy5r+zX)dX4H}QG~bK z8vmVQWF9<+k2qDZXgB0wBBv{G4WlLw&64Ujl&%QISNOUc?%j1_no7-}{edTVdotS2 zW|Ayfv^Ab)#A;WyLz<}($W{C46?KQ2!Y{f;U8ng1S6OQ@jk~a$MpL|dNTyvMpgtjQ zW-|O~CwLiYz>+$Nw=Q!%`I>#cu&>T1`MB;Akq(x5gCe z<=CFXm*FgF5mQ6QB_-U8$zI-!14m?~f0^)NeFiVE*6OsQGM+@p1_)WDJ*5ZKCF&u` z<|bpF?hiAmUYA^MhWY=?r&n@B{lWzGI$4hWzlMiQNCxg*$bDEC4PYaGNZ=VPYo=4C zm$1nWxeke?O}AI6aIc7w`m30{j!pS2!@MGv8%~sk3reT4JdFN}nc=wfT)9gO`{#hy zMs7-=00ZawRNx{!Q8n#egr3alURGktWp4H~Qu=cZ)JukX2puF3Z4)fY=o7`P&8Rk# z^$}Z=-ob`xofy_jbvN(TLbhJ5Uh|76b%Ix^y=s-3#3x+CFz9M?mAGcor927z&MVG? ztXQ-;pLa|+dL31cBxP7>Q;L-+`xW~^yWdtN%l-ESOCmC{d?~jbtz;&isF8%8sbMj= zW2aQp-jU+lCO(9*?=sW6)67rO@Cf`EXpyr#u!|Mor9CVc*Ge;9t(M}>+w27t`y^&o zG9T9MW%-yt?Sgx)uPT|st;S=#T3Ara#CF<&$~Y^xHjlF$9e*i;97e8LBNJ?H zBz*#v2%b)#v=&G3FF%jXB)wFLX6~tXvxBFw5ix4dwM~jL5smXgd?ncW#i)EazNuST zpN04%0Y=2t9Y;N_0`lCBbke7c!4c&n z?J7^T?XjhcF?#L7MyLcvJ26luUc~nbWxh2+JVmYVH8G1?Xordfb&e86jJ|0GQCSFS z-H{82>{H#~*AF1k%FTw4ZNY(UK8z7C)Z+tMnCegziu>ZMGC$0* zUh0~wCh2bH56%|Xuu;vr_!qYQs5C-$gsY8hQtm6~jAUhtIzb2)HbMfv;)M#!Xo5~^ zb-ye;48q%qw_0EjFL6&*`!kzq@*2!$zLjLDghjcv!} z#)h4&+sC1ePG<^y2zBUwT{K|FNicCR3na&SG}1^z;AIJTjE9OlZ)FLPMi J{)VOl{{Uwota$(c diff --git a/scripts/events.db b/scripts/events.db index d3b817d2a036453e0de94479f85f48b5b73d0bb7..436e06e8bba877bf32990534de28e2b7c98531fe 100644 GIT binary patch delta 342 zcmZXNJxjw-7)EnTQ>7+NEGj~!sikY1_a^sK1Sc1_il7dPkhMr7W+{k~7Id^+@eVEy z4!T(!1P7sOCx3uL9sD20!AUsN;XFL)bS_P;>x!8eh5^Ic21-Up56*1U@iJk6vfRC# z<|G`at4%u5Ll|Vzpb9XYK6giY+oJd)gG$25C|#=0Z9a*KtY|W zW{K|rTMJ}HE8tQig$_&E4VPI0j7SLd_z^MpuM0^FGah2y{YDEay~(R-)kJ}cS88P< zIc7MdAVWeKa4vNGhL-ho7cG@J{QGbF&HQoF>8N{uv+_Hpk_U`Xn!zclpG8qrEEc05 DRt;He delta 58 zcmZo@;AwckJwckan1O*of1-jtWAVm>CHx#Z{3<{J2K~uA0udVv6&N?SC@$b_R*`R4 Nk!RelBF}W^0szXq5D)+W diff --git a/test/data.js b/test/data.js index bb2570b..7e3fdae 100644 --- a/test/data.js +++ b/test/data.js @@ -788,6 +788,33 @@ module.exports = { } }, message_update: { + edit_by_webhook: { + application_id: "684280192553844747", + attachments: [], + author: { + avatar: null, + bot: true, + discriminator: "0000", + id: "700285844094845050", + username: "cadence [they]" + }, + channel_id: "497161350934560778", + components: [], + content: "test 2", + edited_timestamp: "2023-08-17T06:29:34.167314+00:00", + embeds: [], + flags: 0, + guild_id: "497159726455455754", + id: "1141619794500649020", + mention_everyone: false, + mention_roles: [], + mentions: [], + pinned: false, + timestamp: "2023-08-17T06:29:29.279000+00:00", + tts: false, + type: 0, + webhook_id: "700285844094845050" + }, bot_response: { attachments: [], author: { From 417f935b9d08c9d0f7d3476c1c2372707eaf05de Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Fri, 18 Aug 2023 01:22:14 +1200 Subject: [PATCH 3/5] add error handler and message deleter --- d2m/actions/delete-message.js | 29 ++++++++++++++++++ d2m/discord-packets.js | 22 ++++++++++---- d2m/event-dispatcher.js | 57 +++++++++++++++++++++++++++++++++-- 3 files changed, 100 insertions(+), 8 deletions(-) create mode 100644 d2m/actions/delete-message.js diff --git a/d2m/actions/delete-message.js b/d2m/actions/delete-message.js new file mode 100644 index 0000000..261c8f9 --- /dev/null +++ b/d2m/actions/delete-message.js @@ -0,0 +1,29 @@ +// @ts-check + +const passthrough = require("../../passthrough") +const { sync, db } = passthrough +/** @type {import("../converters/edit-to-changes")} */ +const editToChanges = sync.require("../converters/edit-to-changes") +/** @type {import("../../matrix/api")} */ +const api = sync.require("../../matrix/api") + +/** + * @param {import("discord-api-types/v10").GatewayMessageDeleteDispatchData} data + */ +async function deleteMessage(data) { + /** @type {string?} */ + const roomID = db.prepare("SELECT channel_id FROM channel_room WHERE channel_id = ?").pluck().get(data.channel_id) + if (!roomID) return + + /** @type {string[]} */ + const eventsToRedact = db.prepare("SELECT event_id FROM event_message WHERE message_id = ?").pluck().all(data.id) + + for (const eventID of eventsToRedact) { + // Unfortuately, we can't specify a sender to do the redaction as, unless we find out that info via the audit logs + await api.redactEvent(roomID, eventID) + db.prepare("DELETE from event_message WHERE event_id = ?").run(eventID) + // TODO: Consider whether this code could be reused between edited messages and deleted messages. + } +} + +module.exports.deleteMessage = deleteMessage diff --git a/d2m/discord-packets.js b/d2m/discord-packets.js index b8c0af6..6ae1c22 100644 --- a/d2m/discord-packets.js +++ b/d2m/discord-packets.js @@ -16,6 +16,7 @@ const utils = { /** @type {typeof import("./event-dispatcher")} */ const eventDispatcher = sync.require("./event-dispatcher") + // Client internals, keep track of the state we need if (message.t === "READY") { if (client.ready) return client.ready = true @@ -62,16 +63,25 @@ const utils = { } } } + } + // Event dispatcher for OOYE bridge operations + try { + if (message.t === "MESSAGE_CREATE") { + eventDispatcher.onMessageCreate(client, message.d) - } else if (message.t === "MESSAGE_CREATE") { - eventDispatcher.onMessageCreate(client, message.d) + } else if (message.t === "MESSAGE_UPDATE") { + eventDispatcher.onMessageUpdate(client, message.d) - } else if (message.t === "MESSAGE_UPDATE") { - eventDispatcher.onMessageUpdate(client, message.d) + } else if (message.t === "MESSAGE_DELETE") { + eventDispatcher.onMessageDelete(client, message.d) - } else if (message.t === "MESSAGE_REACTION_ADD") { - eventDispatcher.onReactionAdd(client, message.d) + } else if (message.t === "MESSAGE_REACTION_ADD") { + eventDispatcher.onReactionAdd(client, message.d) + } + } catch (e) { + // Let OOYE try to handle errors too + eventDispatcher.onError(client, e, message) } } } diff --git a/d2m/event-dispatcher.js b/d2m/event-dispatcher.js index 1527b28..fde228d 100644 --- a/d2m/event-dispatcher.js +++ b/d2m/event-dispatcher.js @@ -1,17 +1,61 @@ const assert = require("assert").strict +const util = require("util") const {sync, db} = require("../passthrough") /** @type {import("./actions/send-message")}) */ const sendMessage = sync.require("./actions/send-message") /** @type {import("./actions/edit-message")}) */ const editMessage = sync.require("./actions/edit-message") - +/** @type {import("./actions/delete-message")}) */ +const deleteMessage = sync.require("./actions/delete-message") /** @type {import("./actions/add-reaction")}) */ const addReaction = sync.require("./actions/add-reaction") +/** @type {import("../matrix/api")}) */ +const api = sync.require("../matrix/api") + +let lastReportedEvent = 0 // Grab Discord events we care about for the bridge, check them, and pass them on module.exports = { + /** + * @param {import("./discord-client")} client + * @param {Error} e + * @param {import("cloudstorm").IGatewayMessage} gatewayMessage + */ + onError(client, e, gatewayMessage) { + console.error("hit event-dispatcher's error handler with this exception:") + console.error(e) // TODO: also log errors into a file or into the database, maybe use a library for this? or just wing it? definitely need to be able to store the formatted event body to load back in later + console.error(`while handling this ${gatewayMessage.t} gateway event:`) + console.dir(gatewayMessage.d) + + if (Date.now() - lastReportedEvent > 5000) { + lastReportedEvent = Date.now() + const channelID = gatewayMessage.d.channel_id + if (channelID) { + const roomID = db.prepare("SELECT room_id FROM channel_room WHERE channel_id = ?").pluck().get(channelID) + let stackLines = e.stack.split("\n") + let cloudstormLine = stackLines.findIndex(l => l.includes("/node_modules/cloudstorm/")) + if (cloudstormLine !== -1) { + stackLines = stackLines.slice(0, cloudstormLine - 2) + } + api.sendEvent(roomID, "m.room.message", { + msgtype: "m.text", + body: "\u26a0 Bridged event from Discord not delivered. See formatted content for full details.", + format: "org.matrix.custom.html", + formatted_body: "\u26a0 Bridged event from Discord not delivered" + + `
Gateway event: ${gatewayMessage.t}` + + `
${stackLines.join("\n")}
` + + `
Original payload` + + `
${util.inspect(gatewayMessage.d, false, 4, false)}
`, + "m.mentions": { + user_ids: ["@cadence:cadence.moe"] + } + }) + } + } + }, + /** * @param {import("./discord-client")} client * @param {import("discord-api-types/v10").GatewayMessageCreateDispatchData} message @@ -38,7 +82,7 @@ module.exports = { */ onMessageUpdate(client, data) { if (data.webhook_id) { - const row = db.prepare("SELECT webhook_id FROM webhook WHERE webhook_id = ?").pluck().get(message.webhook_id) + const row = db.prepare("SELECT webhook_id FROM webhook WHERE webhook_id = ?").pluck().get(data.webhook_id) if (row) { // The update was sent by the bridge's own webhook on discord. We don't want to reflect this back, so just drop it. return @@ -67,5 +111,14 @@ module.exports = { if (data.emoji.id !== null) return // TODO: image emoji reactions console.log(data) addReaction.addReaction(data) + }, + + /** + * @param {import("./discord-client")} client + * @param {import("discord-api-types/v10").GatewayMessageDeleteDispatchData} data + */ + onMessageDelete(client, data) { + console.log(data) + deleteMessage.deleteMessage(data) } } From 750a8cd60aeb217e29f3224226ea0f83763d8db5 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Fri, 18 Aug 2023 01:22:33 +1200 Subject: [PATCH 4/5] update discord-markdown to escape less stuff --- d2m/converters/message-to-event.test.js | 10 ++++++++ package-lock.json | 4 ++-- package.json | 2 +- test/data.js | 31 +++++++++++++++++++++++++ 4 files changed, 44 insertions(+), 3 deletions(-) diff --git a/d2m/converters/message-to-event.test.js b/d2m/converters/message-to-event.test.js index df94196..86942a7 100644 --- a/d2m/converters/message-to-event.test.js +++ b/d2m/converters/message-to-event.test.js @@ -39,6 +39,16 @@ test("message2event: simple plaintext", async t => { }]) }) +test("message2event: simple plaintext with quotes", async t => { + const events = await messageToEvent(data.message.simple_plaintext_with_quotes, data.guild.general, {}) + t.deepEqual(events, [{ + $type: "m.room.message", + "m.mentions": {}, + msgtype: "m.text", + body: `then he said, "you and her aren't allowed in here!"` + }]) +}) + test("message2event: simple user mention", async t => { const events = await messageToEvent(data.message.simple_user_mention, data.guild.general, {}) t.deepEqual(events, [{ diff --git a/package-lock.json b/package-lock.json index c6b6004..875e329 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "dependencies": { "better-sqlite3": "^8.3.0", "cloudstorm": "^0.8.0", - "discord-markdown": "git+https://git.sr.ht/~cadence/nodejs-discord-markdown#9799e4f79912d07f89a030e479e82fcc1e75bc81", + "discord-markdown": "git+https://git.sr.ht/~cadence/nodejs-discord-markdown#440130ef343c8183a81c7c09809731484aa3a182", "heatsync": "^2.4.1", "js-yaml": "^4.1.0", "matrix-appservice": "^2.0.0", @@ -1051,7 +1051,7 @@ }, "node_modules/discord-markdown": { "version": "2.4.1", - "resolved": "git+https://git.sr.ht/~cadence/nodejs-discord-markdown#9799e4f79912d07f89a030e479e82fcc1e75bc81", + "resolved": "git+https://git.sr.ht/~cadence/nodejs-discord-markdown#440130ef343c8183a81c7c09809731484aa3a182", "license": "MIT", "dependencies": { "simple-markdown": "^0.7.2" diff --git a/package.json b/package.json index 6557500..bc0a0db 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "dependencies": { "better-sqlite3": "^8.3.0", "cloudstorm": "^0.8.0", - "discord-markdown": "git+https://git.sr.ht/~cadence/nodejs-discord-markdown#9799e4f79912d07f89a030e479e82fcc1e75bc81", + "discord-markdown": "git+https://git.sr.ht/~cadence/nodejs-discord-markdown#440130ef343c8183a81c7c09809731484aa3a182", "heatsync": "^2.4.1", "js-yaml": "^4.1.0", "matrix-appservice": "^2.0.0", diff --git a/test/data.js b/test/data.js index 7e3fdae..a1d3ece 100644 --- a/test/data.js +++ b/test/data.js @@ -170,6 +170,37 @@ module.exports = { flags: 0, components: [] }, + simple_plaintext_with_quotes: { + id: "1126733830494093454", + type: 0, + content: `then he said, "you and her aren't allowed in here!"`, + channel_id: "112760669178241024", + author: { + id: "111604486476181504", + username: "kyuugryphon", + avatar: "e4ce31267ca524d19be80e684d4cafa1", + discriminator: "0", + public_flags: 0, + flags: 0, + banner: null, + accent_color: null, + global_name: "KyuuGryphon", + avatar_decoration: null, + display_name: "KyuuGryphon", + banner_color: null + }, + attachments: [], + embeds: [], + mentions: [], + mention_roles: [], + pinned: false, + mention_everyone: false, + tts: false, + timestamp: "2023-07-07T04:37:58.892000+00:00", + edited_timestamp: null, + flags: 0, + components: [] + }, simple_user_mention: { id: "1126739682080858234", type: 0, From 9de940471dbde4fe33a92e203c4815bccce199f7 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Fri, 18 Aug 2023 01:23:53 +1200 Subject: [PATCH 5/5] remove redactions from database in edit flow --- d2m/actions/edit-message.js | 7 +++++-- db/ooye.db | Bin 360448 -> 360448 bytes scripts/events.db | Bin 196608 -> 208896 bytes 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/d2m/actions/edit-message.js b/d2m/actions/edit-message.js index 9a329b6..1c1b90e 100644 --- a/d2m/actions/edit-message.js +++ b/d2m/actions/edit-message.js @@ -14,7 +14,8 @@ const api = sync.require("../../matrix/api") async function editMessage(message, guild) { console.log(`*** applying edit for message ${message.id} in channel ${message.channel_id}`) const {roomID, eventsToRedact, eventsToReplace, eventsToSend, senderMxid} = await editToChanges.editToChanges(message, guild, api) - console.log("making these changes:", {eventsToRedact, eventsToReplace, eventsToSend}) + console.log("making these changes:") + console.dir({eventsToRedact, eventsToReplace, eventsToSend}, {depth: null}) // 1. Replace all the things. for (const {oldID, newContent} of eventsToReplace) { @@ -34,7 +35,9 @@ async function editMessage(message, guild) { // Not redacting as the last action because the last action is likely to be shown in the room preview in clients, and we don't want it to look like somebody actually deleted a message. for (const eventID of eventsToRedact) { await api.redactEvent(roomID, eventID, senderMxid) - // TODO: I should almost certainly remove the redacted event from our database now, shouldn't I? I mean, it's literally not there any more... you can't do anything else with it... + // TODO: Reconsider whether it's the right thing to do to delete it from our database? I mean, it's literally not there any more... you can't do anything else with it... + // and you definitely want to mark it in *some* way to prevent duplicate redactions... + 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? // TODO: Consider whether this code could be reused between edited messages and deleted messages. } diff --git a/db/ooye.db b/db/ooye.db index 1c52510cc5f1d7902d1e6a05829750a2a0dfba70..c691745001bc56419c8960a59933537a66d9f44f 100644 GIT binary patch delta 13898 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 V8WR?=VUKGwBW%3=NBpWI{~vdJsG|S? delta 5713 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