From 61ac53599535b43da242ec7b23a31c80c09072f6 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Fri, 13 Oct 2023 02:05:44 +1300 Subject: [PATCH 001/464] d->m fix replying to matrix user in thread --- d2m/converters/message-to-event.js | 5 ++ d2m/converters/message-to-event.test.js | 39 +++++++++++ m2d/actions/send-event.js | 2 +- test/data.js | 86 +++++++++++++++++++++++++ test/ooye-test-data.sql | 7 +- 5 files changed, 136 insertions(+), 3 deletions(-) diff --git a/d2m/converters/message-to-event.js b/d2m/converters/message-to-event.js index 09ac39a..7c4a998 100644 --- a/d2m/converters/message-to-event.js +++ b/d2m/converters/message-to-event.js @@ -215,6 +215,11 @@ async function messageToEvent(message, guild, options = {}, di) { repliedToUserHtml = repliedToDisplayName } let repliedToContent = message.referenced_message?.content + if (repliedToContent?.startsWith("> <:L1:")) { + // If the Discord user is replying to a Matrix user's reply, the fallback is going to contain the emojis and stuff from the bridged rep of the Matrix user's reply quote. + // Need to remove that previous reply rep from this fallback body. The fallbody body should only contain the Matrix user's actual message. + repliedToContent = repliedToContent.split("\n").slice(2).join("\n") + } if (repliedToContent == "") repliedToContent = "[Media]" else if (!repliedToContent) repliedToContent = "[Replied-to message content wasn't provided by Discord]" const repliedToHtml = markdown.toHTML(repliedToContent, { diff --git a/d2m/converters/message-to-event.test.js b/d2m/converters/message-to-event.test.js index 19f0111..4b4b6a0 100644 --- a/d2m/converters/message-to-event.test.js +++ b/d2m/converters/message-to-event.test.js @@ -273,6 +273,45 @@ test("message2event: simple reply to matrix user, reply fallbacks disabled", asy }]) }) +test("message2event: simple reply in thread to a matrix user's reply", async t => { + const events = await messageToEvent(data.message.simple_reply_to_reply_in_thread, data.guild.general, {}, { + api: { + getEvent: mockGetEvent(t, "!FuDZhlOAtqswlyxzeR:cadence.moe", "$nUM-ABBF8KdnvrhXwLlYAE9dgDl_tskOvvcNIBrtsVo", { + type: "m.room.message", + sender: "@cadence:cadence.moe", + content: { + msgtype: "m.text", + body: "> <@_ooye_cadence:cadence.moe> So what I'm wondering is about replies.\n\nWhat about them?", + format: "org.matrix.custom.html", + formatted_body: "
In reply to @_ooye_cadence:cadence.moe
So what I'm wondering is about replies.
What about them?", + "m.relates_to": { + "m.in_reply_to": { + event_id: "$fWQT8uOrzLzAXNVXz88VkGx7Oo724iS5uD8Qn5KUy9w" + } + } + }, + event_id: "$nUM-ABBF8KdnvrhXwLlYAE9dgDl_tskOvvcNIBrtsVo", + room_id: "!FuDZhlOAtqswlyxzeR:cadence.moe" + }) + } + }) + t.deepEqual(events, [{ + $type: "m.room.message", + "m.relates_to": { + "m.in_reply_to": { + event_id: "$nUM-ABBF8KdnvrhXwLlYAE9dgDl_tskOvvcNIBrtsVo" + } + }, + "m.mentions": { + user_ids: ["@cadence:cadence.moe"] + }, + msgtype: "m.text", + body: "> cadence: What about them?\n\nWell, they don't seem to...", + format: "org.matrix.custom.html", + formatted_body: "
In reply to cadence
What about them?
Well, they don't seem to...", + }]) +}) + test("message2event: simple written @mention for matrix user", async t => { const events = await messageToEvent(data.message.simple_written_at_mention_for_matrix, data.guild.general, {}, { api: { diff --git a/m2d/actions/send-event.js b/m2d/actions/send-event.js index 13966fb..c1e3ba3 100644 --- a/m2d/actions/send-event.js +++ b/m2d/actions/send-event.js @@ -99,7 +99,7 @@ async function sendEvent(event) { for (const message of messagesToSend) { const messageResponse = await channelWebhook.sendMessageWithWebhook(channelID, message, threadID) - db.prepare("REPLACE INTO message_channel (message_id, channel_id) VALUES (?, ?)").run(messageResponse.id, channelID) + db.prepare("REPLACE INTO message_channel (message_id, channel_id) VALUES (?, ?)").run(messageResponse.id, threadID || channelID) db.prepare("INSERT INTO event_message (event_id, event_type, event_subtype, message_id, part, source) VALUES (?, ?, ?, ?, ?, 0)").run(event.event_id, event.type, event.content["msgtype"] || null, messageResponse.id, eventPart) // source 0 = matrix eventPart = 1 diff --git a/test/data.js b/test/data.js index c5a1f8e..ea9f66a 100644 --- a/test/data.js +++ b/test/data.js @@ -939,6 +939,92 @@ module.exports = { attachments: [], guild_id: "112760669178241024" }, + simple_reply_to_reply_in_thread: { + type: 19, + tts: false, + timestamp: "2023-10-12T12:35:12.721000+00:00", + referenced_message: { + webhook_id: "1142275246532083723", + type: 0, + tts: false, + timestamp: "2023-10-12T12:35:06.578000+00:00", + position: 1, + pinned: false, + mentions: [ + { + username: "cadence.worm", + public_flags: 0, + id: "772659086046658620", + global_name: "cadence", + discriminator: "0", + avatar_decoration_data: null, + avatar: "4b5c4b28051144e4c111f0113a0f1cf1" + } + ], + mention_roles: [], + mention_everyone: false, + id: "1162005526675193909", + flags: 0, + embeds: [], + edited_timestamp: null, + content: "> <:L1:1144820033948762203><:L2:1144820084079087647>https://discord.com/channels/1100319549670301727/1162005314908999790/1162005501782011975 <@772659086046658620>:\n" + + "> So what I'm wondering is about replies.\n" + + "What about them?", + components: [], + channel_id: "1162005314908999790", + author: { + username: "cadence [they]", + id: "1142275246532083723", + global_name: null, + discriminator: "0000", + bot: true, + avatar: "af0ead3b92cf6e448fdad80b4e7fc9e5" + }, + attachments: [], + application_id: "684280192553844747" + }, + position: 2, + pinned: false, + nonce: "1162005551190638592", + message_reference: { + message_id: "1162005526675193909", + guild_id: "1100319549670301727", + channel_id: "1162005314908999790" + }, + mentions: [], + mention_roles: [], + mention_everyone: false, + member: { + roles: [], + premium_since: null, + pending: false, + nick: "worm", + mute: false, + joined_at: "2023-04-25T07:17:03.696000+00:00", + flags: 0, + deaf: false, + communication_disabled_until: null, + avatar: null + }, + id: "1162005552440815646", + flags: 0, + embeds: [], + edited_timestamp: null, + content: "Well, they don't seem to...", + components: [], + channel_id: "1162005314908999790", + author: { + username: "cadence.worm", + public_flags: 0, + id: "772659086046658620", + global_name: "cadence", + discriminator: "0", + avatar_decoration_data: null, + avatar: "4b5c4b28051144e4c111f0113a0f1cf1" + }, + attachments: [], + guild_id: "1100319549670301727" + }, sticker: { id: "1106366167788044450", type: 0, diff --git a/test/ooye-test-data.sql b/test/ooye-test-data.sql index 2070b66..f5cfb5c 100644 --- a/test/ooye-test-data.sql +++ b/test/ooye-test-data.sql @@ -8,6 +8,7 @@ INSERT INTO channel_room (channel_id, room_id, name, nick, thread_parent, custom ('497161350934560778', '!CzvdIdUQXgUjDVKxeU:cadence.moe', 'amanda-spam', NULL, NULL, NULL), ('160197704226439168', '!hYnGGlPHlbujVVfktC:cadence.moe', 'the-stanley-parable-channel', 'bots', NULL, NULL), ('1100319550446252084', '!BnKuBPCvyfOkhcUjEu:cadence.moe', 'worm-farm', NULL, NULL, NULL), +('1162005314908999790', '!FuDZhlOAtqswlyxzeR:cadence.moe', 'Hey.', NULL, '1100319550446252084', NULL), ('297272183716052993', '!rEOspnYqdOalaIFniV:cadence.moe', 'general', NULL, NULL, NULL), ('122155380120748034', '!cqeGDbPiMFAhLsqqqq:cadence.moe', 'cadences-mind', 'coding', NULL, NULL); @@ -36,7 +37,8 @@ INSERT INTO message_channel (message_id, channel_id) VALUES ('1144865310588014633', '687028734322147344'), ('1145688633186193479', '1100319550446252084'), ('1145688633186193480', '1100319550446252084'), -('1145688633186193481', '1100319550446252084'); +('1145688633186193481', '1100319550446252084'), +('1162005526675193909', '1162005314908999790'); INSERT INTO event_message (event_id, event_type, event_subtype, message_id, part, source) VALUES ('$X16nfVks1wsrhq4E9SSLiqrf2N8KD0erD0scZG7U5xg', 'm.room.message', 'm.text', '1126786462646550579', 0, 1), @@ -57,7 +59,8 @@ INSERT INTO event_message (event_id, event_type, event_subtype, message_id, part ('$v_Gtr-bzv9IVlSLBO5DstzwmiDd-GSFaNfHX66IupV8', 'm.room.message', 'm.text', '1144874214311067708', 0, 0), ('$7LIdiJCEqjcWUrpzWzS8TELOlFfBEe4ytgS7zn2lbSs', 'm.room.message', 'm.text', '1145688633186193479', 0, 0), ('$7LIdiJCEqjcWUrpzWzS8TELOlFfBEe4ytgS7zn2lbSt', 'm.room.message', 'm.text', '1145688633186193480', 0, 0), -('$7LIdiJCEqjcWUrpzWzS8TELOlFfBEe4ytgS7zn2lbSt', 'm.room.message', 'm.text', '1145688633186193481', 1, 0); +('$7LIdiJCEqjcWUrpzWzS8TELOlFfBEe4ytgS7zn2lbSt', 'm.room.message', 'm.text', '1145688633186193481', 1, 0), +('$nUM-ABBF8KdnvrhXwLlYAE9dgDl_tskOvvcNIBrtsVo', 'm.room.message', 'm.text', '1162005526675193909', 0, 0); INSERT INTO file (discord_url, mxc_url) VALUES ('https://cdn.discordapp.com/attachments/497161332244742154/1124628646431297546/image.png', 'mxc://cadence.moe/qXoZktDqNtEGuOCZEADAMvhM'), From 850de85d8250a00b5a2eb744163bf64a7e9c9a77 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Fri, 13 Oct 2023 02:06:06 +1300 Subject: [PATCH 002/464] Add //thread command for Matrix users --- docs/user-guide.md | 2 ++ matrix/matrix-command-handler.js | 41 ++++++++++++++++++++++++++++---- 2 files changed, 38 insertions(+), 5 deletions(-) diff --git a/docs/user-guide.md b/docs/user-guide.md index 0228e87..d360806 100644 --- a/docs/user-guide.md +++ b/docs/user-guide.md @@ -28,6 +28,8 @@ If a room is newly created, it will be added to the space, but it will not be an If a thread is newly created, it will be added to the space, and an announcement will also be posted to the parent channel with a link to quickly join. +Matrix users can create their own thread with `/thread `. This will create a real thread channel on Discord-side and announce its creation on both sides in the usual way. + ## Custom Room Icons Normally on Matrix, the room icons will match the space icon. Since Matrix allows for room-specific icons, the bridge will keep track of any custom icon that was set on a room. diff --git a/matrix/matrix-command-handler.js b/matrix/matrix-command-handler.js index a22d2b1..8056f96 100644 --- a/matrix/matrix-command-handler.js +++ b/matrix/matrix-command-handler.js @@ -73,6 +73,7 @@ function onReactionAdd(event) { * @callback CommandExecute * @param {Ty.Event.Outer_M_Room_Message} event * @param {string} realBody + * @param {string[]} words * @param {any} [ctx] */ @@ -85,13 +86,13 @@ function onReactionAdd(event) { /** @param {CommandExecute} execute */ function replyctx(execute) { /** @type {CommandExecute} */ - return function(event, realBody, ctx = {}) { + return function(event, realBody, words, ctx = {}) { ctx["m.relates_to"] = { "m.in_reply_to": { event_id: event.event_id } } - return execute(event, realBody, ctx) + return execute(event, realBody, words, ctx) } } @@ -148,7 +149,7 @@ class MatrixStringBuilder { const commands = [{ aliases: ["emoji"], execute: replyctx( - async (event, realBody, ctx) => { + async (event, realBody, words, ctx) => { // Guard /** @type {string} */ // @ts-ignore const channelID = select("channel_room", "channel_id", {room_id: event.room_id}).pluck().get() @@ -165,7 +166,7 @@ const commands = [{ const permissions = dUtils.getPermissions([], guild.roles) if (guild.emojis.length >= slots) { matrixOnlyReason = "CAPACITY" - } else if (!(permissions | 0x40000000n)) { // MANAGE_GUILD_EXPRESSIONS (apparently CREATE_GUILD_EXPRESSIONS isn't good enough...) + } else if (!(permissions & 0x40000000n)) { // MANAGE_GUILD_EXPRESSIONS (apparently CREATE_GUILD_EXPRESSIONS isn't good enough...) matrixOnlyReason = "USER_PERMISSIONS" } } @@ -284,6 +285,36 @@ const commands = [{ }) } ) +}, { + aliases: ["thread"], + execute: replyctx( + async (event, realBody, words, ctx) => { + // Guard + /** @type {string} */ // @ts-ignore + const channelID = select("channel_room", "channel_id", {room_id: event.room_id}).pluck().get() + const guildID = discord.channels.get(channelID)?.["guild_id"] + if (!guildID) { + return api.sendEvent(event.room_id, "m.room.message", { + ...ctx, + msgtype: "m.text", + body: "This room isn't bridged to the other side." + }) + } + + const guild = discord.guilds.get(guildID) + assert(guild) + const permissions = dUtils.getPermissions([], guild.roles) + if (!(permissions & 0x800000000n)) { // CREATE_PUBLIC_THREADS + return api.sendEvent(event.room_id, "m.room.message", { + ...ctx, + msgtype: "m.text", + body: "This command creates a thread on Discord. But you aren't allowed to do this, because if you were a Discord user, you wouldn't have the Create Public Threads permission." + }) + } + + await discord.snow.channel.createThreadWithoutMessage(channelID, {type: 11, name: words.slice(1).join(" ")}) + } + ) }] @@ -308,7 +339,7 @@ async function execute(event) { const command = commands.find(c => c.aliases.includes(commandName)) if (!command) return - await command.execute(event, realBody) + await command.execute(event, realBody, words) } module.exports.execute = execute From 67305bb636ad2ba5f58fdba485e71b85a58317e2 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Fri, 13 Oct 2023 21:32:42 +1300 Subject: [PATCH 003/464] Allow m.notice (embeds) to be edited --- d2m/converters/edit-to-changes.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/d2m/converters/edit-to-changes.js b/d2m/converters/edit-to-changes.js index 9d1fc0b..dc42708 100644 --- a/d2m/converters/edit-to-changes.js +++ b/d2m/converters/edit-to-changes.js @@ -102,7 +102,7 @@ async function editToChanges(message, guild, api) { // So we'll remove entries from eventsToReplace that *definitely* cannot have changed. (This is category 4 mentioned above.) Everything remaining *may* have changed. eventsToReplace = eventsToReplace.filter(ev => { // Discord does not allow files, images, attachments, or videos to be edited. - if (ev.old.event_type === "m.room.message" && ev.old.event_subtype !== "m.text" && ev.old.event_subtype !== "m.emote") { + if (ev.old.event_type === "m.room.message" && ev.old.event_subtype !== "m.text" && ev.old.event_subtype !== "m.emote" && ev.old.event_subtype !== "m.notice") { return false } // Discord does not allow stickers to be edited. From 480c7a6bd9c8fb82f545bf8b82d2d3f735f09a78 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Fri, 13 Oct 2023 23:05:07 +1300 Subject: [PATCH 004/464] m->d: Use Matrix displayname in m/m reply preview --- m2d/converters/event-to-message.js | 3 ++- m2d/converters/event-to-message.test.js | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/m2d/converters/event-to-message.js b/m2d/converters/event-to-message.js index fcf0bf8..ff31607 100644 --- a/m2d/converters/event-to-message.js +++ b/m2d/converters/event-to-message.js @@ -324,11 +324,12 @@ async function eventToMessage(event, guild, di) { replyLine += `https://discord.com/channels/${guild.id}/${row.channel_id}/${row.message_id} ` } const sender = repliedToEvent.sender - const senderName = sender.match(/@([^:]*)/)?.[1] || sender const authorID = select("sim", "user_id", {mxid: repliedToEvent.sender}).pluck().get() if (authorID) { replyLine += `<@${authorID}>` } else { + let senderName = select("member_cache", "displayname", {mxid: repliedToEvent.sender}).pluck().get() + if (!senderName) senderName = sender.match(/@([^:]*)/)?.[1] || sender replyLine += `Ⓜ️**${senderName}**` } // If the event has been edited, the homeserver will include the relation in `unsigned`. diff --git a/m2d/converters/event-to-message.test.js b/m2d/converters/event-to-message.test.js index 87a4b12..41d63b2 100644 --- a/m2d/converters/event-to-message.test.js +++ b/m2d/converters/event-to-message.test.js @@ -1151,7 +1151,7 @@ test("event2message: rich reply to a matrix user's long message with formatting" messagesToEdit: [], messagesToSend: [{ username: "cadence [they]", - content: "> <:L1:1144820033948762203><:L2:1144820084079087647>https://discord.com/channels/112760669178241024/687028734322147344/1144865310588014633 Ⓜ️**cadence**:" + content: "> <:L1:1144820033948762203><:L2:1144820084079087647>https://discord.com/channels/112760669178241024/687028734322147344/1144865310588014633 Ⓜ️**cadence [they]**:" + "\n> i should have a little happy test list bold em..." + "\n**no you can't!!!**", avatar_url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/azCAhThKTojXSZJRoWwZmhvU" @@ -1312,7 +1312,7 @@ test("event2message: with layered rich replies, the preview should only be the r messagesToEdit: [], messagesToSend: [{ username: "cadence [they]", - content: "> <:L1:1144820033948762203><:L2:1144820084079087647>https://discord.com/channels/112760669178241024/687028734322147344/1144865310588014633 Ⓜ️**cadence**:" + content: "> <:L1:1144820033948762203><:L2:1144820084079087647>https://discord.com/channels/112760669178241024/687028734322147344/1144865310588014633 Ⓜ️**cadence [they]**:" + "\n> two" + "\nthree", avatar_url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/azCAhThKTojXSZJRoWwZmhvU" From 999276e4073ca0d941f4e7f936ececf94aa907da Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Fri, 13 Oct 2023 23:23:15 +1300 Subject: [PATCH 005/464] m->d: Fix HTML entities showing in reply preview --- m2d/converters/event-to-message.js | 13 ++++--- m2d/converters/event-to-message.test.js | 49 +++++++++++++++++++++++++ package-lock.json | 12 ++++++ package.json | 1 + readme.md | 1 + 5 files changed, 71 insertions(+), 5 deletions(-) diff --git a/m2d/converters/event-to-message.js b/m2d/converters/event-to-message.js index ff31607..e7bbda5 100644 --- a/m2d/converters/event-to-message.js +++ b/m2d/converters/event-to-message.js @@ -5,6 +5,7 @@ const DiscordTypes = require("discord-api-types/v10") const chunk = require("chunk-text") const TurndownService = require("turndown") const assert = require("assert").strict +const entities = require("entities") const passthrough = require("../../passthrough") const {sync, db, discord, select, from} = passthrough @@ -349,11 +350,13 @@ async function eventToMessage(event, guild, di) { } else { const repliedToContent = repliedToEvent.content.formatted_body || repliedToEvent.content.body const contentPreviewChunks = chunk( - repliedToContent.replace(/.*<\/mx-reply>/, "") // Remove everything before replies, so just use the actual message body - .replace(/
.*?<\/blockquote>/, "") // If the message starts with a blockquote, don't count it and use the message body afterwards - .replace(/(?:\n|
)+/g, " ") // Should all be on one line - .replace(/]*data-mx-spoiler\b[^>]*>.*?<\/span>/g, "[spoiler]") // Good enough method of removing spoiler content. (I don't want to break out the HTML parser unless I have to.) - .replace(/<[^>]+>/g, ""), 50) // Completely strip all other formatting. + entities.decodeHTML5Strict( // Remove entities like & " + repliedToContent.replace(/.*<\/mx-reply>/, "") // Remove everything before replies, so just use the actual message body + .replace(/
.*?<\/blockquote>/, "") // If the message starts with a blockquote, don't count it and use the message body afterwards + .replace(/(?:\n|
)+/g, " ") // Should all be on one line + .replace(/]*data-mx-spoiler\b[^>]*>.*?<\/span>/g, "[spoiler]") // Good enough method of removing spoiler content. (I don't want to break out the HTML parser unless I have to.) + .replace(/<[^>]+>/g, "") // Completely strip all HTML tags and formatting. + ), 50) contentPreview = ":\n> " contentPreview += contentPreviewChunks.length > 1 ? contentPreviewChunks[0] + "..." : contentPreviewChunks[0] } diff --git a/m2d/converters/event-to-message.test.js b/m2d/converters/event-to-message.test.js index 41d63b2..1a1c3f0 100644 --- a/m2d/converters/event-to-message.test.js +++ b/m2d/converters/event-to-message.test.js @@ -813,6 +813,55 @@ test("event2message: should include a reply preview when message ends with a blo ) }) +test("event2message: entities are not escaped in main message or reply preview", async t => { + // Intended result: Testing? in italics, followed by the sequence "':.`[]&things + t.deepEqual( + await eventToMessage({ + type: "m.room.message", + sender: "@cadence:cadence.moe", + content: { + msgtype: "m.text", + body: "> <@cadence:cadence.moe> _Testing?_ \"':.`[]&things\n\n_Testing?_ \"':.`[]&things", + format: "org.matrix.custom.html", + formatted_body: "
In reply to @cadence:cadence.moe
Testing? \"':.`[]&things
Testing? "':.`[]&things", + "m.relates_to": { + "m.in_reply_to": { + event_id: "$yIWjZPi6Xk56fBxJwqV4ANs_hYLjnWI2cNKbZ2zwk60" + } + } + }, + event_id: "$2I7odT9okTdpwDcqOjkJb_A3utdO4V8Cp3LK6-Rvwcs", + room_id: "!fGgIymcYWOqjbSRUdV:cadence.moe" + }, data.guild.general, { + api: { + getEvent: mockGetEvent(t, "!fGgIymcYWOqjbSRUdV:cadence.moe", "$yIWjZPi6Xk56fBxJwqV4ANs_hYLjnWI2cNKbZ2zwk60", { + type: "m.room.message", + sender: "@cadence:cadence.moe", + content: { + "msgtype": "m.text", + "body": "_Testing?_ \"':.`[]&things", + "format": "org.matrix.custom.html", + "formatted_body": "Testing? "':.`[]&things" + }, + event_id: "$yIWjZPi6Xk56fBxJwqV4ANs_hYLjnWI2cNKbZ2zwk60", + room_id: "!fGgIymcYWOqjbSRUdV:cadence.moe" + }) + } + }), + { + messagesToDelete: [], + messagesToEdit: [], + messagesToSend: [{ + username: "cadence [they]", + content: "> <:L1:1144820033948762203><:L2:1144820084079087647>Ⓜ️**cadence [they]**:" + + "\n> Testing? \"':.`[]&things" + + "\n_Testing?_ \"':.\\`\\[\\]&things", + avatar_url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/azCAhThKTojXSZJRoWwZmhvU" + }] + } + ) +}) + test("event2message: editing a rich reply to a sim user", async t => { const eventsFetched = [] t.deepEqual( diff --git a/package-lock.json b/package-lock.json index d1ed09e..3847ee1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "chunk-text": "^2.0.1", "cloudstorm": "^0.8.0", "discord-markdown": "git+https://git.sr.ht/~cadence/nodejs-discord-markdown#abc56d544072a1dc5624adfea455b0e902adf7b3", + "entities": "^4.5.0", "giframe": "github:cloudrac3r/giframe#v0.4.1", "heatsync": "^2.4.1", "js-yaml": "^4.1.0", @@ -1096,6 +1097,17 @@ "once": "^1.4.0" } }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/es-get-iterator": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.3.tgz", diff --git a/package.json b/package.json index e8eec83..6a9deea 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "chunk-text": "^2.0.1", "cloudstorm": "^0.8.0", "discord-markdown": "git+https://git.sr.ht/~cadence/nodejs-discord-markdown#abc56d544072a1dc5624adfea455b0e902adf7b3", + "entities": "^4.5.0", "giframe": "github:cloudrac3r/giframe#v0.4.1", "heatsync": "^2.4.1", "js-yaml": "^4.1.0", diff --git a/readme.md b/readme.md index 77239f7..5c97745 100644 --- a/readme.md +++ b/readme.md @@ -164,6 +164,7 @@ Follow these steps: * (1) discord-markdown: This is my fork! I make sure it does what I want. * (0) giframe: This is my fork! It should do what I want. * (1) heatsync: Module hot-reloader that I trust. +* (0) entities: Looks fine. No dependencies. * (1) js-yaml: It seems to do what I want, and it's already pulled in by matrix-appservice. * (70) matrix-appservice: I wish it didn't pull in express :( * (0) minimist: It's already pulled in by better-sqlite3->prebuild-install From 44f90cbb5fd2780636f8f44937adb65c7fd0cfb2 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Sat, 14 Oct 2023 01:00:28 +1300 Subject: [PATCH 006/464] Use Discord global_name as sim user displayname --- d2m/actions/register-user.js | 2 +- d2m/actions/register-user.test.js | 20 ++++++++++++++++++ test/data.js | 34 +++++++++++++++++++++++++++++++ test/ooye-test-data.sql | 3 ++- 4 files changed, 57 insertions(+), 2 deletions(-) diff --git a/d2m/actions/register-user.js b/d2m/actions/register-user.js index 62e0fb6..9b5527f 100644 --- a/d2m/actions/register-user.js +++ b/d2m/actions/register-user.js @@ -97,7 +97,7 @@ async function ensureSimJoined(user, roomID) { */ async function memberToStateContent(user, member, guildID) { let displayname = user.username - // if (member.nick && member.nick !== displayname) displayname = member.nick + " | " + displayname // prepend nick if present + if (user.global_name) displayname = user.global_name if (member.nick) displayname = member.nick const content = { diff --git a/d2m/actions/register-user.test.js b/d2m/actions/register-user.test.js index 5cee80b..96c73aa 100644 --- a/d2m/actions/register-user.test.js +++ b/d2m/actions/register-user.test.js @@ -22,6 +22,26 @@ test("member2state: without member nick or avatar", async t => { ) }) +test("member2state: with global name, without member nick or avatar", async t => { + t.deepEqual( + await _memberToStateContent(testData.member.papiophidian.user, testData.member.papiophidian, testData.guild.general.id), + { + avatar_url: "mxc://cadence.moe/JPzSmALLirnIprlSMKohSSoX", + displayname: "PapiOphidian", + membership: "join", + "moe.cadence.ooye.member": { + avatar: "/avatars/320067006521147393/5fc4ad85c1ea876709e9a7d3374a78a1.png?size=1024" + }, + "uk.half-shot.discord.member": { + bot: false, + displayColor: 1579292, + id: "320067006521147393", + username: "@papiophidian" + } + } + ) +}) + test("member2state: with member nick and avatar", async t => { t.deepEqual( await _memberToStateContent(testData.member.sheep.user, testData.member.sheep, testData.guild.general.id), diff --git a/test/data.js b/test/data.js index ea9f66a..1a526aa 100644 --- a/test/data.js +++ b/test/data.js @@ -222,6 +222,40 @@ module.exports = { }, mute: false, deaf: false + }, + papiophidian: { + avatar: null, + communication_disabled_until: null, + flags: 0, + joined_at: "2018-08-05T09:40:47.076000+00:00", + nick: null, + pending: false, + premium_since: "2021-09-30T18:58:44.996000+00:00", + roles: [ + "475599410068324352", + "475599471049310208", + "497586624390234112", + "613685290938138625", + "475603310955593729", + "1151970058730487898", + "1151970058730487901" + ], + unusual_dm_activity_until: null, + user: { + id: "320067006521147393", + username: "papiophidian", + avatar: "5fc4ad85c1ea876709e9a7d3374a78a1", + discriminator: "0", + public_flags: 4194880, + flags: 4194880, + banner: "a_6f311cf6a3851a98e2fa0335af85b1d1", + accent_color: 1579292, + global_name: "PapiOphidian", + avatar_decoration_data: null, + banner_color: "#18191c" + }, + mute: false, + deaf: false } }, pins: { diff --git a/test/ooye-test-data.sql b/test/ooye-test-data.sql index f5cfb5c..f8039cc 100644 --- a/test/ooye-test-data.sql +++ b/test/ooye-test-data.sql @@ -75,7 +75,8 @@ INSERT INTO file (discord_url, mxc_url) VALUES ('https://cdn.discordapp.com/emojis/230201364309868544.png', 'mxc://cadence.moe/qWmbXeRspZRLPcjseyLmeyXC'), ('https://cdn.discordapp.com/emojis/393635038903926784.gif', 'mxc://cadence.moe/WbYqNlACRuicynBfdnPYtmvc'), ('https://cdn.discordapp.com/attachments/176333891320283136/1157854643037163610/Screenshot_20231001_034036.jpg', 'mxc://cadence.moe/zAXdQriaJuLZohDDmacwWWDR'), -('https://cdn.discordapp.com/emojis/1125827250609201255.png', 'mxc://cadence.moe/pgdGTxAyEltccRgZKxdqzHHP'); +('https://cdn.discordapp.com/emojis/1125827250609201255.png', 'mxc://cadence.moe/pgdGTxAyEltccRgZKxdqzHHP'), +('https://cdn.discordapp.com/avatars/320067006521147393/5fc4ad85c1ea876709e9a7d3374a78a1.png?size=1024', 'mxc://cadence.moe/JPzSmALLirnIprlSMKohSSoX'); INSERT INTO emoji (emoji_id, name, animated, mxc_url) VALUES ('230201364309868544', 'hippo', 0, 'mxc://cadence.moe/qWmbXeRspZRLPcjseyLmeyXC'), From d9d672bffd92b1de49a7e3bc5a06b21979a0e3d2 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Sat, 14 Oct 2023 02:15:21 +1300 Subject: [PATCH 007/464] d->m: Make role mentions really pretty --- d2m/converters/message-to-event.js | 29 +++++++--- d2m/converters/message-to-event.test.js | 12 ++++ test/data.js | 76 ++++++++++++++++++++++++- 3 files changed, 109 insertions(+), 8 deletions(-) diff --git a/d2m/converters/message-to-event.js b/d2m/converters/message-to-event.js index 7c4a998..c0c937e 100644 --- a/d2m/converters/message-to-event.js +++ b/d2m/converters/message-to-event.js @@ -17,7 +17,12 @@ const reg = require("../../matrix/read-registration") const userRegex = reg.namespaces.users.map(u => new RegExp(u.regex)) -function getDiscordParseCallbacks(message, useHTML) { +/** + * @param {DiscordTypes.APIMessage} message + * @param {DiscordTypes.APIGuild} guild + * @param {boolean} useHTML + */ +function getDiscordParseCallbacks(message, guild, useHTML) { return { /** @param {{id: string, type: "discordUser"}} node */ user: node => { @@ -53,8 +58,18 @@ function getDiscordParseCallbacks(message, useHTML) { return `:${node.name}:` } }, - role: node => - "@&" + node.id, + role: node => { + const role = guild.roles.find(r => r.id === node.id) + if (!role) { + return "@&" + node.id // fallback for if the cache breaks. if this happens, fix discord-packets.js to store the role info. + } else if (useHTML && role.color) { + return `@${role.name}` + } else if (useHTML) { + return `@${role.name}` + } else { + return `@${role.name}:` + } + }, everyone: node => "@room", here: node => @@ -160,11 +175,11 @@ async function messageToEvent(message, guild, options = {}, di) { })) let html = markdown.toHTML(content, { - discordCallback: getDiscordParseCallbacks(message, true) + discordCallback: getDiscordParseCallbacks(message, guild, true) }, null, null) let body = markdown.toHTML(content, { - discordCallback: getDiscordParseCallbacks(message, false), + discordCallback: getDiscordParseCallbacks(message, guild, false), discordOnly: true, escapeHTML: false, }, null, null) @@ -223,10 +238,10 @@ async function messageToEvent(message, guild, options = {}, di) { if (repliedToContent == "") repliedToContent = "[Media]" else if (!repliedToContent) repliedToContent = "[Replied-to message content wasn't provided by Discord]" const repliedToHtml = markdown.toHTML(repliedToContent, { - discordCallback: getDiscordParseCallbacks(message, true) + discordCallback: getDiscordParseCallbacks(message, guild, true) }, null, null) const repliedToBody = markdown.toHTML(repliedToContent, { - discordCallback: getDiscordParseCallbacks(message, false), + discordCallback: getDiscordParseCallbacks(message, guild, false), discordOnly: true, escapeHTML: false, }, null, null) diff --git a/d2m/converters/message-to-event.test.js b/d2m/converters/message-to-event.test.js index 4b4b6a0..8e2ba53 100644 --- a/d2m/converters/message-to-event.test.js +++ b/d2m/converters/message-to-event.test.js @@ -73,6 +73,18 @@ test("message2event: simple room mention", async t => { }]) }) +test("message2event: simple role mentions", async t => { + const events = await messageToEvent(data.message.simple_role_mentions, data.guild.general, {}) + t.deepEqual(events, [{ + $type: "m.room.message", + "m.mentions": {}, + msgtype: "m.text", + body: "I'm just @!!DLCS!!: testing a few role pings @Master Wonder Mage: don't mind me", + format: "org.matrix.custom.html", + formatted_body: `I'm just @!!DLCS!! testing a few role pings @Master Wonder Mage don't mind me` + }]) +}) + test("message2event: simple message link", async t => { const events = await messageToEvent(data.message.simple_message_link, data.guild.general, {}) t.deepEqual(events, [{ diff --git a/test/data.js b/test/data.js index 1a526aa..3d3f1f0 100644 --- a/test/data.js +++ b/test/data.js @@ -119,7 +119,51 @@ module.exports = { } ], premium_subscription_count: 14, - roles: [], + roles: [ + { + version: 1696964862461, + unicode_emoji: null, + tags: {}, + position: 22, + permissions: '0', + name: 'Master Wonder Mage', + mentionable: true, + managed: false, + id: '503685967463448616', + icon: null, + hoist: false, + flags: 0, + color: 0 + }, { + version: 1696964862776, + unicode_emoji: null, + tags: {}, + position: 131, + permissions: '0', + name: '!!DLCS!!', + mentionable: true, + managed: false, + id: '212762309364285440', + icon: null, + hoist: true, + flags: 0, + color: 11076095 + }, { + version: 1696964862698, + unicode_emoji: '🍂', + tags: {}, + position: 102, + permissions: '0', + name: 'corporate overlord', + mentionable: false, + managed: false, + id: '217013981053845504', + icon: null, + hoist: true, + flags: 0, + color: 16745267 + } + ], discovery_splash: null, default_message_notifications: 1, region: "deprecated", @@ -434,6 +478,36 @@ module.exports = { attachments: [], guild_id: "112760669178241024" }, + simple_role_mentions: { + id: "1162374402785153106", + type: 0, + content: "I'm just <@&212762309364285440> testing a few role pings <@&503685967463448616> don't mind me", + channel_id: "160197704226439168", + author: { + id: "772659086046658620", + username: "cadence.worm", + avatar: "4b5c4b28051144e4c111f0113a0f1cf1", + discriminator: "0", + public_flags: 0, + flags: 0, + banner: null, + accent_color: null, + global_name: "cadence", + avatar_decoration_data: null, + banner_color: null + }, + attachments: [], + embeds: [], + mentions: [], + mention_roles: [ "212762309364285440", "503685967463448616" ], + pinned: false, + mention_everyone: false, + tts: false, + timestamp: "2023-10-13T13:00:53.496000+00:00", + edited_timestamp: null, + flags: 0, + components: [] + }, simple_message_link: { id: "1126788210308161626", type: 0, From 1016fb1d6703f1eff6a8d9e5fd60145d98b5115f Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Sat, 14 Oct 2023 17:23:55 +1300 Subject: [PATCH 008/464] Always use OOYE bot to send thread start context --- d2m/actions/send-message.js | 2 +- d2m/converters/message-to-event.js | 3 ++- d2m/converters/message-to-event.test.js | 1 + m2d/converters/emoji.js | 1 + 4 files changed, 5 insertions(+), 2 deletions(-) diff --git a/d2m/actions/send-message.js b/d2m/actions/send-message.js index a0027b0..082cce4 100644 --- a/d2m/actions/send-message.js +++ b/d2m/actions/send-message.js @@ -40,7 +40,7 @@ async function sendMessage(message, guild) { } for (const event of events) { const eventType = event.$type - if (event.$sender) senderMxid = event.$sender + if ("$sender" in event) senderMxid = event.$sender /** @type {Pick> & { $type?: string, $sender?: string }} */ const eventWithoutType = {...event} delete eventWithoutType.$type diff --git a/d2m/converters/message-to-event.js b/d2m/converters/message-to-event.js index c0c937e..1a063a5 100644 --- a/d2m/converters/message-to-event.js +++ b/d2m/converters/message-to-event.js @@ -108,7 +108,8 @@ async function messageToEvent(message, guild, options = {}, di) { const event = await di.api.getEvent(roomID, eventID) return [{ ...event.content, - $type: event.type + $type: event.type, + $sender: null }] } diff --git a/d2m/converters/message-to-event.test.js b/d2m/converters/message-to-event.test.js index 8e2ba53..8f89a43 100644 --- a/d2m/converters/message-to-event.test.js +++ b/d2m/converters/message-to-event.test.js @@ -485,6 +485,7 @@ test("message2event: thread start message reference", async t => { }) t.deepEqual(events, [{ $type: "m.room.message", + $sender: null, msgtype: "m.text", body: "layer 4", "m.mentions": {} diff --git a/m2d/converters/emoji.js b/m2d/converters/emoji.js index 214022f..2c39a86 100644 --- a/m2d/converters/emoji.js +++ b/m2d/converters/emoji.js @@ -36,6 +36,7 @@ function encodeEmoji(input, shortcode) { const forceTrimmedList = [ "%F0%9F%91%8D", // 👍 + "%F0%9F%91%8E", // 👎️ "%E2%AD%90", // ⭐ "%F0%9F%90%88", // 🐈 "%E2%9D%93", // ❓ From b7f90db20afd78d324216fe79033bccfbc431438 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Sat, 14 Oct 2023 19:27:45 +1300 Subject: [PATCH 009/464] Fix reply preview "undefined" on embed description --- d2m/converters/user-to-mxid.test.js | 2 +- m2d/converters/event-to-message.js | 2 +- m2d/converters/event-to-message.test.js | 80 +++++++++++++++++++++++++ test/ooye-test-data.sql | 13 ++-- 4 files changed, 91 insertions(+), 6 deletions(-) diff --git a/d2m/converters/user-to-mxid.test.js b/d2m/converters/user-to-mxid.test.js index 1b31260..e709473 100644 --- a/d2m/converters/user-to-mxid.test.js +++ b/d2m/converters/user-to-mxid.test.js @@ -17,7 +17,7 @@ test("user2name: works on emojis", t => { }) test("user2name: works on single emoji at the end", t => { - t.equal(userToSimName({username: "Amanda 🎵", discriminator: "2192"}), "amanda") + t.equal(userToSimName({username: "Melody 🎵", discriminator: "2192"}), "melody") }) test("user2name: works on crazy name", t => { diff --git a/m2d/converters/event-to-message.js b/m2d/converters/event-to-message.js index e7bbda5..5f6f3e6 100644 --- a/m2d/converters/event-to-message.js +++ b/m2d/converters/event-to-message.js @@ -352,7 +352,7 @@ async function eventToMessage(event, guild, di) { const contentPreviewChunks = chunk( entities.decodeHTML5Strict( // Remove entities like & " repliedToContent.replace(/.*<\/mx-reply>/, "") // Remove everything before replies, so just use the actual message body - .replace(/
.*?<\/blockquote>/, "") // If the message starts with a blockquote, don't count it and use the message body afterwards + .replace(/^\s*
.*?<\/blockquote>(.....)/s, "$1") // If the message starts with a blockquote, don't count it and use the message body afterwards .replace(/(?:\n|
)+/g, " ") // Should all be on one line .replace(/]*data-mx-spoiler\b[^>]*>.*?<\/span>/g, "[spoiler]") // Good enough method of removing spoiler content. (I don't want to break out the HTML parser unless I have to.) .replace(/<[^>]+>/g, "") // Completely strip all HTML tags and formatting. diff --git a/m2d/converters/event-to-message.test.js b/m2d/converters/event-to-message.test.js index 1a1c3f0..074da1d 100644 --- a/m2d/converters/event-to-message.test.js +++ b/m2d/converters/event-to-message.test.js @@ -813,6 +813,86 @@ test("event2message: should include a reply preview when message ends with a blo ) }) +test("event2message: should include a reply preview when replying to a description-only bot embed", async t => { + t.deepEqual( + await eventToMessage({ + type: "m.room.message", + sender: "@cadence:cadence.moe", + content: { + msgtype: "m.text", + body: "> <@_ooye_amanda:cadence.moe> > It looks like this queue has ended.\n\nso you're saying on matrix side I would have to edit ^this^ to add \"Timed out\" before the blockquote?", + format: "org.matrix.custom.html", + formatted_body: "
In reply to @_ooye_amanda:cadence.moe
It looks like this queue has ended.
so you're saying on matrix side I would have to edit ^this^ to add "Timed out" before the blockquote?", + "m.relates_to": { + "m.in_reply_to": { + event_id: "$zJFjTvNn1w_YqpR4o4ISKUFisNRgZcu1KSMI_LADPVQ" + } + } + }, + event_id: "$qCOlszCawu5hlnF2a2PGyXeGGvtoNJdXyRAEaTF0waA", + room_id: "!CzvdIdUQXgUjDVKxeU:cadence.moe" + }, data.guild.general, { + api: { + getEvent: mockGetEvent(t, "!CzvdIdUQXgUjDVKxeU:cadence.moe", "$zJFjTvNn1w_YqpR4o4ISKUFisNRgZcu1KSMI_LADPVQ", { + type: "m.room.message", + room_id: "!edUxjVdzgUvXDUIQCK:cadence.moe", + sender: "@_ooye_amanda:cadence.moe", + content: { + "m.mentions": {}, + msgtype: "m.notice", + body: "> Now Playing: [**LOADING**](https://amanda.moe)\n" + + "> \n" + + "> `[​====[LOADING]=====]`", + format: "org.matrix.custom.html", + formatted_body: '
Now Playing: LOADING

[​====[LOADING]=====]
' + }, + unsigned: { + "m.relations": { + "m.replace": { + type: "m.room.message", + room_id: "!edUxjVdzgUvXDUIQCK:cadence.moe", + sender: "@_ooye_amanda:cadence.moe", + content: { + "m.mentions": {}, + msgtype: "m.notice", + body: "* > It looks like this queue has ended.", + format: "org.matrix.custom.html", + formatted_body: "*
It looks like this queue has ended.
", + "m.new_content": { + "m.mentions": {}, + msgtype: "m.notice", + body: "> It looks like this queue has ended.", + format: "org.matrix.custom.html", + formatted_body: "
It looks like this queue has ended.
" + }, + "m.relates_to": { + rel_type: "m.replace", + event_id: "$zJFjTvNn1w_YqpR4o4ISKUFisNRgZcu1KSMI_LADPVQ" + } + }, + event_id: "$nrLF310vALFIXPNk6MEIy0lYiGXi210Ok0DATSaF5jQ", + user_id: "@_ooye_amanda:cadence.moe", + } + }, + user_id: "@_ooye_amanda:cadence.moe", + } + }) + } + }), + { + messagesToDelete: [], + messagesToEdit: [], + messagesToSend: [{ + username: "cadence [they]", + content: "> <:L1:1144820033948762203><:L2:1144820084079087647>https://discord.com/channels/112760669178241024/497161350934560778/1162625810109317170 <@1109360903096369153>:" + + "\n> It looks like this queue has ended." + + `\nso you're saying on matrix side I would have to edit ^this^ to add "Timed out" before the blockquote?`, + avatar_url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/azCAhThKTojXSZJRoWwZmhvU" + }] + } + ) +}) + test("event2message: entities are not escaped in main message or reply preview", async t => { // Intended result: Testing? in italics, followed by the sequence "':.`[]&things t.deepEqual( diff --git a/test/ooye-test-data.sql b/test/ooye-test-data.sql index f8039cc..4fba480 100644 --- a/test/ooye-test-data.sql +++ b/test/ooye-test-data.sql @@ -18,7 +18,8 @@ INSERT INTO sim (user_id, sim_name, localpart, mxid) VALUES ('771520384671416320', 'bojack_horseman', '_ooye_bojack_horseman', '@_ooye_bojack_horseman:cadence.moe'), ('112890272819507200', '.wing.', '_ooye_.wing.', '@_ooye_.wing.:cadence.moe'), ('114147806469554185', 'extremity', '_ooye_extremity', '@_ooye_extremity:cadence.moe'), -('111604486476181504', 'kyuugryphon', '_ooye_kyuugryphon', '@_ooye_kyuugryphon:cadence.moe');; +('111604486476181504', 'kyuugryphon', '_ooye_kyuugryphon', '@_ooye_kyuugryphon:cadence.moe'), +('1109360903096369153', 'amanda', '_ooye_amanda', '@_ooye_amanda:cadence.moe'); INSERT INTO sim_member (mxid, room_id, hashed_profile_content) VALUES ('@_ooye_bojack_horseman:cadence.moe', '!hYnGGlPHlbujVVfktC:cadence.moe', NULL); @@ -38,7 +39,8 @@ INSERT INTO message_channel (message_id, channel_id) VALUES ('1145688633186193479', '1100319550446252084'), ('1145688633186193480', '1100319550446252084'), ('1145688633186193481', '1100319550446252084'), -('1162005526675193909', '1162005314908999790'); +('1162005526675193909', '1162005314908999790'), +('1162625810109317170', '497161350934560778'); INSERT INTO event_message (event_id, event_type, event_subtype, message_id, part, source) VALUES ('$X16nfVks1wsrhq4E9SSLiqrf2N8KD0erD0scZG7U5xg', 'm.room.message', 'm.text', '1126786462646550579', 0, 1), @@ -60,7 +62,9 @@ INSERT INTO event_message (event_id, event_type, event_subtype, message_id, part ('$7LIdiJCEqjcWUrpzWzS8TELOlFfBEe4ytgS7zn2lbSs', 'm.room.message', 'm.text', '1145688633186193479', 0, 0), ('$7LIdiJCEqjcWUrpzWzS8TELOlFfBEe4ytgS7zn2lbSt', 'm.room.message', 'm.text', '1145688633186193480', 0, 0), ('$7LIdiJCEqjcWUrpzWzS8TELOlFfBEe4ytgS7zn2lbSt', 'm.room.message', 'm.text', '1145688633186193481', 1, 0), -('$nUM-ABBF8KdnvrhXwLlYAE9dgDl_tskOvvcNIBrtsVo', 'm.room.message', 'm.text', '1162005526675193909', 0, 0); +('$nUM-ABBF8KdnvrhXwLlYAE9dgDl_tskOvvcNIBrtsVo', 'm.room.message', 'm.text', '1162005526675193909', 0, 0), +('$0wEdIP8fhTq-P68xwo_gyUw-Zv0KA2aS2tfhdFSrLZc', 'm.room.message', 'm.text', '1162625810109317170', 1, 1), +('$zJFjTvNn1w_YqpR4o4ISKUFisNRgZcu1KSMI_LADPVQ', 'm.room.message', 'm.notice', '1162625810109317170', 1, 1); INSERT INTO file (discord_url, mxc_url) VALUES ('https://cdn.discordapp.com/attachments/497161332244742154/1124628646431297546/image.png', 'mxc://cadence.moe/qXoZktDqNtEGuOCZEADAMvhM'), @@ -91,7 +95,8 @@ INSERT INTO member_cache (room_id, mxid, displayname, avatar_url) VALUES ('!BpMdOUkWWhFxmTrENV:cadence.moe', '@cadence:cadence.moe', 'cadence [they]', 'malformed mxc'), ('!fGgIymcYWOqjbSRUdV:cadence.moe', '@cadence:cadence.moe', 'cadence [they]', 'mxc://cadence.moe/azCAhThKTojXSZJRoWwZmhvU'), ('!BnKuBPCvyfOkhcUjEu:cadence.moe', '@cadence:cadence.moe', 'cadence [they]', 'mxc://cadence.moe/azCAhThKTojXSZJRoWwZmhvU'), -('!maggESguZBqGBZtSnr:cadence.moe', '@cadence:cadence.moe', 'cadence [they]', 'mxc://cadence.moe/azCAhThKTojXSZJRoWwZmhvU'); +('!maggESguZBqGBZtSnr:cadence.moe', '@cadence:cadence.moe', 'cadence [they]', 'mxc://cadence.moe/azCAhThKTojXSZJRoWwZmhvU'), +('!CzvdIdUQXgUjDVKxeU:cadence.moe', '@cadence:cadence.moe', 'cadence [they]', 'mxc://cadence.moe/azCAhThKTojXSZJRoWwZmhvU'); INSERT INTO "auto_emoji" ("name","emoji_id","guild_id") VALUES ('L1','1144820033948762203','529176156398682115'), From c24752625d28947ee35a769557f020141480bd98 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Sat, 14 Oct 2023 22:08:10 +1300 Subject: [PATCH 010/464] Split part and reaction_part Now, reactions should always end up on the bottom of a message group, instead of sometimes being in the middle. --- d2m/actions/add-reaction.js | 2 +- d2m/actions/edit-message.js | 24 ++++++---- d2m/actions/remove-reaction.js | 2 +- d2m/actions/send-message.js | 10 ++-- d2m/converters/edit-to-changes.js | 30 ++++++------ d2m/converters/edit-to-changes.test.js | 25 ++++------ .../0007-split-part-and-reaction-part.sql | 24 ++++++++++ db/orm-defs.d.ts | 1 + m2d/actions/add-reaction.js | 2 +- m2d/actions/send-event.js | 3 +- scripts/check-migrate.js | 15 ++++++ test/ooye-test-data.sql | 46 +++++++++---------- test/test.js | 8 ++-- 13 files changed, 121 insertions(+), 71 deletions(-) create mode 100644 db/migrations/0007-split-part-and-reaction-part.sql create mode 100644 scripts/check-migrate.js diff --git a/d2m/actions/add-reaction.js b/d2m/actions/add-reaction.js index f962a73..b131f13 100644 --- a/d2m/actions/add-reaction.js +++ b/d2m/actions/add-reaction.js @@ -21,7 +21,7 @@ async function addReaction(data) { const user = data.member?.user assert.ok(user && user.username) - const parentID = select("event_message", "event_id", {message_id: data.message_id, part: 0}).pluck().get() // 0 = primary + const parentID = select("event_message", "event_id", {message_id: data.message_id, reaction_part: 0}).pluck().get() if (!parentID) return // Nothing can be done if the parent message was never bridged. assert.equal(typeof parentID, "string") diff --git a/d2m/actions/edit-message.js b/d2m/actions/edit-message.js index a1538b9..2a08526 100644 --- a/d2m/actions/edit-message.js +++ b/d2m/actions/edit-message.js @@ -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, promoteEvent, promoteNextEvent} = await editToChanges.editToChanges(message, guild, api) + const {roomID, eventsToRedact, eventsToReplace, eventsToSend, senderMxid, promotions} = await editToChanges.editToChanges(message, guild, api) // 1. Replace all the things. for (const {oldID, newContent} of eventsToReplace) { @@ -36,24 +36,30 @@ async function editMessage(message, guild) { } // 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 + const sendNewEventParts = new Set() + for (const promotion of promotions) { + if ("eventID" in promotion) { + db.prepare(`UPDATE event_message SET ${promotion.column} = 0 WHERE event_id = ?`).run(promotion.eventID) + } else if ("nextEvent" in promotion) { + sendNewEventParts.add(promotion.column) + } } // 4. Send all the things. + if (eventsToSend.length) { + db.prepare("REPLACE INTO message_channel (message_id, channel_id) VALUES (?, ?)").run(message.id, message.channel_id) + } for (const content of eventsToSend) { const eventType = content.$type /** @type {Pick> & { $type?: string }} */ const contentWithoutType = {...content} delete contentWithoutType.$type + delete contentWithoutType.$sender + const part = sendNewEventParts.has("part") && eventsToSend[0] === content ? 0 : 1 + const reactionPart = sendNewEventParts.has("reaction_part") && eventsToSend[eventsToSend.length - 1] === content ? 0 : 1 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)").run(eventID, eventType, content.msgtype || null, message.id, eventPart) // part 1 = supporting; source 1 = discord - - eventPart = 1 + db.prepare("INSERT INTO event_message (event_id, event_type, event_subtype, message_id, part, reaction_part, source) VALUES (?, ?, ?, ?, ?, ?, 1)").run(eventID, eventType, content.msgtype || null, message.id, part, reactionPart) // source 1 = discord } } diff --git a/d2m/actions/remove-reaction.js b/d2m/actions/remove-reaction.js index 3047c32..b2dba0f 100644 --- a/d2m/actions/remove-reaction.js +++ b/d2m/actions/remove-reaction.js @@ -20,7 +20,7 @@ const converter = sync.require("../converters/remove-reaction") async function removeSomeReactions(data) { const roomID = select("channel_room", "room_id", {channel_id: data.channel_id}).pluck().get() if (!roomID) return - const eventIDForMessage = select("event_message", "event_id", {message_id: data.message_id, part: 0}).pluck().get() + const eventIDForMessage = select("event_message", "event_id", {message_id: data.message_id, reaction_part: 0}).pluck().get() if (!eventIDForMessage) return /** @type {Ty.Pagination>} */ diff --git a/d2m/actions/send-message.js b/d2m/actions/send-message.js index 082cce4..b59fc7f 100644 --- a/d2m/actions/send-message.js +++ b/d2m/actions/send-message.js @@ -33,12 +33,14 @@ async function sendMessage(message, guild) { const events = await messageToEvent.messageToEvent(message, guild, {}, {api}) const eventIDs = [] - let eventPart = 0 // 0 is primary, 1 is supporting if (events.length) { db.prepare("REPLACE INTO message_channel (message_id, channel_id) VALUES (?, ?)").run(message.id, message.channel_id) if (senderMxid) api.sendTyping(roomID, false, senderMxid) } for (const event of events) { + const part = event === events[0] ? 0 : 1 + const reactionPart = event === events[events.length - 1] ? 0 : 1 + const eventType = event.$type if ("$sender" in event) senderMxid = event.$sender /** @type {Pick> & { $type?: string, $sender?: string }} */ @@ -48,12 +50,14 @@ async function sendMessage(message, guild) { const useTimestamp = message["backfill"] ? new Date(message.timestamp).getTime() : undefined const eventID = await api.sendEvent(roomID, eventType, eventWithoutType, senderMxid, useTimestamp) - db.prepare("INSERT INTO event_message (event_id, event_type, event_subtype, message_id, part, source) VALUES (?, ?, ?, ?, ?, 1)").run(eventID, eventType, event.msgtype || null, message.id, eventPart) // source 1 = discord + db.prepare("INSERT INTO event_message (event_id, event_type, event_subtype, message_id, part, reaction_part, source) VALUES (?, ?, ?, ?, ?, ?, 1)").run(eventID, eventType, event.msgtype || null, message.id, part, reactionPart) // source 1 = discord // The primary event is part = 0 and has the most important and distinct information. It is used to provide reply previews, be pinned, and possibly future uses. // The first event is chosen to be the primary part because it is usually the message text content and is more likely to be distinct. // For example, "Reply to 'this meme made me think of you'" is more useful than "Replied to image". - eventPart = 1 + + // The last event gets reaction_part = 0. Reactions are managed there because reactions are supposed to appear at the bottom. + eventIDs.push(eventID) } diff --git a/d2m/converters/edit-to-changes.js b/d2m/converters/edit-to-changes.js index dc42708..f86921c 100644 --- a/d2m/converters/edit-to-changes.js +++ b/d2m/converters/edit-to-changes.js @@ -26,7 +26,7 @@ async function editToChanges(message, guild, api) { /** @type {string?} Null if we don't have a sender in the room, which will happen if it's a webhook's message. The bridge bot will do the edit instead. */ const senderMxid = from("sim").join("sim_member", "mxid").where({user_id: message.author.id, room_id: roomID}).pluck("mxid").get() || null - const oldEventRows = select("event_message", ["event_id", "event_type", "event_subtype", "part"], {message_id: message.id}).all() + const oldEventRows = select("event_message", ["event_id", "event_type", "event_subtype", "part", "reaction_part"], {message_id: message.id}).all() // Figure out what we will be replacing them with @@ -83,17 +83,21 @@ 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 + // We want to maintain exactly one part = 0 and one reaction_part = 0 database row at all times. + /** @type {({column: string, eventID: string} | {column: string, nextEvent: true})[]} */ + const promotions = [] + for (const column of ["part", "reaction_part"]) { + // If no events with part = 0 exist (or will exist), we need to do some management. + if (!eventsToReplace.some(e => e.old[column] === 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)) + promotions.push({column, eventID: eventsToReplace[0].old.event_id}) + } else { + // No existing events to promote, but new events are being sent. Whatever gets sent will be the next part = 0. + promotions.push({column, nextEvent: true}) + } } } @@ -117,7 +121,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, promoteEvent, promoteNextEvent} + return {roomID, eventsToReplace, eventsToRedact, eventsToSend, senderMxid, promotions} } /** diff --git a/d2m/converters/edit-to-changes.test.js b/d2m/converters/edit-to-changes.test.js index 8c67f6a..7c29787 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, promoteEvent, promoteNextEvent} = await editToChanges(data.message_update.edit_by_webhook, data.guild.general, {}) + 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, [{ @@ -27,12 +27,11 @@ test("edit2changes: edit by webhook", async t => { } }]) t.equal(senderMxid, null) - t.equal(promoteEvent, null) - t.equal(promoteNextEvent, false) + t.deepEqual(promotions, []) }) test("edit2changes: bot response", async t => { - const {senderMxid, eventsToRedact, eventsToReplace, eventsToSend, promoteEvent, promoteNextEvent} = await editToChanges(data.message_update.bot_response, data.guild.general, { + const {senderMxid, eventsToRedact, eventsToReplace, eventsToSend, promotions} = await editToChanges(data.message_update.bot_response, data.guild.general, { async getJoinedMembers(roomID) { t.equal(roomID, "!hYnGGlPHlbujVVfktC:cadence.moe") return new Promise(resolve => { @@ -84,21 +83,19 @@ test("edit2changes: bot response", async t => { } }]) t.equal(senderMxid, "@_ooye_bojack_horseman:cadence.moe") - t.equal(promoteEvent, null) - t.equal(promoteNextEvent, false) + t.deepEqual(promotions, []) }) test("edit2changes: remove caption from image", async t => { - const {eventsToRedact, eventsToReplace, eventsToSend, promoteEvent, promoteNextEvent} = await editToChanges(data.message_update.removed_caption_from_image, data.guild.general, {}) + const {eventsToRedact, eventsToReplace, eventsToSend, promotions} = 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) + t.deepEqual(promotions, [{column: "part", eventID: "$51f4yqHinwnSbPEQ9dCgoyy4qiIJSX0QYYVUnvwyTCI"}]) }) test("edit2changes: change file type", async t => { - const {eventsToRedact, eventsToReplace, eventsToSend, promoteEvent, promoteNextEvent} = await editToChanges(data.message_update.changed_file_type, data.guild.general, {}) + const {eventsToRedact, eventsToReplace, eventsToSend, promotions} = await editToChanges(data.message_update.changed_file_type, data.guild.general, {}) t.deepEqual(eventsToRedact, ["$51f4yqHinwnSbPEQ9dCgoyy4qiIJSX0QYYVUnvwyTCJ"]) t.deepEqual(eventsToSend, [{ $type: "m.room.message", @@ -109,12 +106,11 @@ test("edit2changes: change file type", async t => { msgtype: "m.text" }]) t.deepEqual(eventsToReplace, []) - t.equal(promoteEvent, null) - t.equal(promoteNextEvent, true) + t.deepEqual(promotions, [{column: "part", nextEvent: true}, {column: "reaction_part", nextEvent: true}]) }) test("edit2changes: add caption back to that image", async t => { - const {eventsToRedact, eventsToReplace, eventsToSend, promoteEvent, promoteNextEvent} = await editToChanges(data.message_update.added_caption_to_image, data.guild.general, {}) + const {eventsToRedact, eventsToReplace, eventsToSend, promotions} = await editToChanges(data.message_update.added_caption_to_image, data.guild.general, {}) t.deepEqual(eventsToRedact, []) t.deepEqual(eventsToSend, [{ $type: "m.room.message", @@ -123,8 +119,7 @@ test("edit2changes: add caption back to that image", async t => { "m.mentions": {} }]) t.deepEqual(eventsToReplace, []) - t.equal(promoteEvent, null) - t.equal(promoteNextEvent, false) + t.deepEqual(promotions, []) }) test("edit2changes: stickers and attachments are not changed, only the content can be edited", async t => { diff --git a/db/migrations/0007-split-part-and-reaction-part.sql b/db/migrations/0007-split-part-and-reaction-part.sql new file mode 100644 index 0000000..4aad6c1 --- /dev/null +++ b/db/migrations/0007-split-part-and-reaction-part.sql @@ -0,0 +1,24 @@ +BEGIN TRANSACTION; + +-- Add column reaction_part to event_message, copying the existing value from part + +CREATE TABLE "new_event_message" ( + "event_id" TEXT NOT NULL, + "event_type" TEXT, + "event_subtype" TEXT, + "message_id" TEXT NOT NULL, + "part" INTEGER NOT NULL, + "reaction_part" INTEGER NOT NULL, + "source" INTEGER NOT NULL, + PRIMARY KEY("message_id","event_id") +) WITHOUT ROWID; + +INSERT INTO new_event_message SELECT event_id, event_type, event_subtype, message_id, part, part, source FROM event_message; + +DROP TABLE event_message; + +ALTER TABLE new_event_message RENAME TO event_message; + +COMMIT; + +VACUUM; diff --git a/db/orm-defs.d.ts b/db/orm-defs.d.ts index 0714e0b..ec8d498 100644 --- a/db/orm-defs.d.ts +++ b/db/orm-defs.d.ts @@ -14,6 +14,7 @@ export type Models = { event_type: string | null event_subtype: string | null part: number + reaction_part: number source: number } diff --git a/m2d/actions/add-reaction.js b/m2d/actions/add-reaction.js index e6a94d9..cfd471b 100644 --- a/m2d/actions/add-reaction.js +++ b/m2d/actions/add-reaction.js @@ -16,7 +16,7 @@ const emoji = sync.require("../converters/emoji") async function addReaction(event) { const channelID = select("channel_room", "channel_id", {room_id: event.room_id}).pluck().get() if (!channelID) return // We just assume the bridge has already been created - const messageID = select("event_message", "message_id", {event_id: event.content["m.relates_to"].event_id, part: 0}).pluck().get() // 0 = primary + const messageID = select("event_message", "message_id", {event_id: event.content["m.relates_to"].event_id}, "ORDER BY reaction_part").pluck().get() if (!messageID) return // Nothing can be done if the parent message was never bridged. const key = event.content["m.relates_to"].key diff --git a/m2d/actions/send-event.js b/m2d/actions/send-event.js index c1e3ba3..13b5650 100644 --- a/m2d/actions/send-event.js +++ b/m2d/actions/send-event.js @@ -98,9 +98,10 @@ async function sendEvent(event) { } for (const message of messagesToSend) { + const reactionPart = messagesToEdit.length === 0 && message === messagesToSend[messagesToSend.length - 1] ? 0 : 1 const messageResponse = await channelWebhook.sendMessageWithWebhook(channelID, message, threadID) db.prepare("REPLACE INTO message_channel (message_id, channel_id) VALUES (?, ?)").run(messageResponse.id, threadID || channelID) - db.prepare("INSERT INTO event_message (event_id, event_type, event_subtype, message_id, part, source) VALUES (?, ?, ?, ?, ?, 0)").run(event.event_id, event.type, event.content["msgtype"] || null, messageResponse.id, eventPart) // source 0 = matrix + db.prepare("INSERT INTO event_message (event_id, event_type, event_subtype, message_id, part, reaction_part, source) VALUES (?, ?, ?, ?, ?, ?, 0)").run(event.event_id, event.type, event.content["msgtype"] || null, messageResponse.id, eventPart, reactionPart) // source 0 = matrix eventPart = 1 messageResponses.push(messageResponse) diff --git a/scripts/check-migrate.js b/scripts/check-migrate.js new file mode 100644 index 0000000..308ea87 --- /dev/null +++ b/scripts/check-migrate.js @@ -0,0 +1,15 @@ +// @ts-check + +// Trigger the database migration flow and exit after committing. +// You can use this to run migrations locally and check the result using sqlitebrowser. + +const sqlite = require("better-sqlite3") + +const config = require("../config") +const passthrough = require("../passthrough") +const db = new sqlite("db/ooye.db") +const migrate = require("../db/migrate") + +Object.assign(passthrough, {config, db }) + +migrate.migrate(db) diff --git a/test/ooye-test-data.sql b/test/ooye-test-data.sql index 4fba480..0627d08 100644 --- a/test/ooye-test-data.sql +++ b/test/ooye-test-data.sql @@ -42,29 +42,29 @@ INSERT INTO message_channel (message_id, channel_id) VALUES ('1162005526675193909', '1162005314908999790'), ('1162625810109317170', '497161350934560778'); -INSERT INTO event_message (event_id, event_type, event_subtype, message_id, part, source) VALUES -('$X16nfVks1wsrhq4E9SSLiqrf2N8KD0erD0scZG7U5xg', 'm.room.message', 'm.text', '1126786462646550579', 0, 1), -('$Ij3qo7NxMA4VPexlAiIx2CB9JbsiGhJeyt-2OvkAUe4', 'm.room.message', 'm.text', '1128118177155526666', 0, 0), -('$zXSlyI78DQqQwwfPUSzZ1b-nXzbUrCDljJgnGDdoI10', 'm.room.message', 'm.text', '1141619794500649020', 0, 1), -('$fdD9OZ55xg3EAsfvLZza5tMhtjUO91Wg3Otuo96TplY', 'm.room.message', 'm.text', '1141206225632112650', 0, 1), -('$mtR8cJqM4fKno1bVsm8F4wUVqSntt2sq6jav1lyavuA', 'm.room.message', 'm.text', '1141501302736695316', 0, 1), -('$51f4yqHinwnSbPEQ9dCgoyy4qiIJSX0QYYVUnvwyTCI', 'm.room.message', 'm.image', '1141501302736695316', 1, 1), -('$51f4yqHinwnSbPEQ9dCgoyy4qiIJSX0QYYVUnvwyTCJ', 'm.room.message', 'm.image', '1141501302736695317', 0, 1), -('$vgTKOR5ZTYNMKaS7XvgEIDaOWZtVCEyzLLi5Pc5Gz4M', 'm.room.message', 'm.text', '1128084851279536279', 0, 1), -('$YUJFa5j0ZJe7PUvD2DykRt9g51RoadUEYmuJLdSEbJ0', 'm.room.message', 'm.image', '1128084851279536279', 1, 1), -('$oLyUTyZ_7e_SUzGNWZKz880ll9amLZvXGbArJCKai2Q', 'm.room.message', 'm.text', '1128084748338741392', 0, 1), -('$FchUVylsOfmmbj-VwEs5Z9kY49_dt2zd0vWfylzy5Yo', 'm.room.message', 'm.text', '1143121514925928541', 0, 1), -('$lnAF9IosAECTnlv9p2e18FG8rHn-JgYKHEHIh5qdFv4', 'm.room.message', 'm.text', '1106366167788044450', 0, 1), -('$Ijf1MFCD39ktrNHxrA-i2aKoRWNYdAV2ZXYQeiZIgEU', 'm.room.message', 'm.image', '1106366167788044450', 1, 0), -('$f9cjKiacXI9qPF_nUAckzbiKnJEi0LM399kOkhdd8f8', 'm.sticker', NULL, '1106366167788044450', 1, 0), -('$Fxy8SMoJuTduwReVkHZ1uHif9EuvNx36Hg79cltiA04', 'm.room.message', 'm.text', '1144865310588014633', 0, 1), -('$v_Gtr-bzv9IVlSLBO5DstzwmiDd-GSFaNfHX66IupV8', 'm.room.message', 'm.text', '1144874214311067708', 0, 0), -('$7LIdiJCEqjcWUrpzWzS8TELOlFfBEe4ytgS7zn2lbSs', 'm.room.message', 'm.text', '1145688633186193479', 0, 0), -('$7LIdiJCEqjcWUrpzWzS8TELOlFfBEe4ytgS7zn2lbSt', 'm.room.message', 'm.text', '1145688633186193480', 0, 0), -('$7LIdiJCEqjcWUrpzWzS8TELOlFfBEe4ytgS7zn2lbSt', 'm.room.message', 'm.text', '1145688633186193481', 1, 0), -('$nUM-ABBF8KdnvrhXwLlYAE9dgDl_tskOvvcNIBrtsVo', 'm.room.message', 'm.text', '1162005526675193909', 0, 0), -('$0wEdIP8fhTq-P68xwo_gyUw-Zv0KA2aS2tfhdFSrLZc', 'm.room.message', 'm.text', '1162625810109317170', 1, 1), -('$zJFjTvNn1w_YqpR4o4ISKUFisNRgZcu1KSMI_LADPVQ', 'm.room.message', 'm.notice', '1162625810109317170', 1, 1); +INSERT INTO event_message (event_id, event_type, event_subtype, message_id, part, reaction_part, source) VALUES +('$X16nfVks1wsrhq4E9SSLiqrf2N8KD0erD0scZG7U5xg', 'm.room.message', 'm.text', '1126786462646550579', 0, 0, 1), +('$Ij3qo7NxMA4VPexlAiIx2CB9JbsiGhJeyt-2OvkAUe4', 'm.room.message', 'm.text', '1128118177155526666', 0, 0, 0), +('$zXSlyI78DQqQwwfPUSzZ1b-nXzbUrCDljJgnGDdoI10', 'm.room.message', 'm.text', '1141619794500649020', 0, 0, 1), +('$fdD9OZ55xg3EAsfvLZza5tMhtjUO91Wg3Otuo96TplY', 'm.room.message', 'm.text', '1141206225632112650', 0, 0, 1), +('$mtR8cJqM4fKno1bVsm8F4wUVqSntt2sq6jav1lyavuA', 'm.room.message', 'm.text', '1141501302736695316', 0, 1, 1), +('$51f4yqHinwnSbPEQ9dCgoyy4qiIJSX0QYYVUnvwyTCI', 'm.room.message', 'm.image', '1141501302736695316', 1, 0, 1), +('$51f4yqHinwnSbPEQ9dCgoyy4qiIJSX0QYYVUnvwyTCJ', 'm.room.message', 'm.image', '1141501302736695317', 0, 0, 1), +('$vgTKOR5ZTYNMKaS7XvgEIDaOWZtVCEyzLLi5Pc5Gz4M', 'm.room.message', 'm.text', '1128084851279536279', 0, 1, 1), +('$YUJFa5j0ZJe7PUvD2DykRt9g51RoadUEYmuJLdSEbJ0', 'm.room.message', 'm.image', '1128084851279536279', 1, 0, 1), +('$oLyUTyZ_7e_SUzGNWZKz880ll9amLZvXGbArJCKai2Q', 'm.room.message', 'm.text', '1128084748338741392', 0, 0, 1), +('$FchUVylsOfmmbj-VwEs5Z9kY49_dt2zd0vWfylzy5Yo', 'm.room.message', 'm.text', '1143121514925928541', 0, 0, 1), +('$lnAF9IosAECTnlv9p2e18FG8rHn-JgYKHEHIh5qdFv4', 'm.room.message', 'm.text', '1106366167788044450', 0, 1, 1), +('$Ijf1MFCD39ktrNHxrA-i2aKoRWNYdAV2ZXYQeiZIgEU', 'm.room.message', 'm.image', '1106366167788044450', 1, 1, 0), +('$f9cjKiacXI9qPF_nUAckzbiKnJEi0LM399kOkhdd8f8', 'm.sticker', NULL, '1106366167788044450', 1, 0, 0), +('$Fxy8SMoJuTduwReVkHZ1uHif9EuvNx36Hg79cltiA04', 'm.room.message', 'm.text', '1144865310588014633', 0, 0, 1), +('$v_Gtr-bzv9IVlSLBO5DstzwmiDd-GSFaNfHX66IupV8', 'm.room.message', 'm.text', '1144874214311067708', 0, 0, 0), +('$7LIdiJCEqjcWUrpzWzS8TELOlFfBEe4ytgS7zn2lbSs', 'm.room.message', 'm.text', '1145688633186193479', 0, 0, 0), +('$7LIdiJCEqjcWUrpzWzS8TELOlFfBEe4ytgS7zn2lbSt', 'm.room.message', 'm.text', '1145688633186193480', 0, 0, 0), +('$7LIdiJCEqjcWUrpzWzS8TELOlFfBEe4ytgS7zn2lbSt', 'm.room.message', 'm.text', '1145688633186193481', 1, 0, 0), +('$nUM-ABBF8KdnvrhXwLlYAE9dgDl_tskOvvcNIBrtsVo', 'm.room.message', 'm.text', '1162005526675193909', 0, 0, 0), +('$0wEdIP8fhTq-P68xwo_gyUw-Zv0KA2aS2tfhdFSrLZc', 'm.room.message', 'm.text', '1162625810109317170', 1, 1, 1), +('$zJFjTvNn1w_YqpR4o4ISKUFisNRgZcu1KSMI_LADPVQ', 'm.room.message', 'm.notice', '1162625810109317170', 1, 0, 1); INSERT INTO file (discord_url, mxc_url) VALUES ('https://cdn.discordapp.com/attachments/497161332244742154/1124628646431297546/image.png', 'mxc://cadence.moe/qXoZktDqNtEGuOCZEADAMvhM'), diff --git a/test/test.js b/test/test.js index 5e15a14..90e3f5a 100644 --- a/test/test.js +++ b/test/test.js @@ -50,16 +50,16 @@ file._actuallyUploadDiscordFileToMxc = function(url, res) { throw new Error(`Not require("../matrix/file.test") require("../matrix/read-registration.test") require("../matrix/txnid.test") + require("../d2m/actions/create-room.test") + require("../d2m/actions/register-user.test") + require("../d2m/converters/edit-to-changes.test") + require("../d2m/converters/emoji-to-key.test") require("../d2m/converters/message-to-event.test") require("../d2m/converters/message-to-event.embeds.test") - require("../d2m/converters/edit-to-changes.test") require("../d2m/converters/pins-to-list.test") require("../d2m/converters/remove-reaction.test") require("../d2m/converters/thread-to-announcement.test") require("../d2m/converters/user-to-mxid.test") - require("../d2m/converters/emoji-to-key.test") - require("../d2m/actions/create-room.test") - require("../d2m/actions/register-user.test") require("../m2d/converters/event-to-message.test") require("../m2d/converters/utils.test") })() From 040e987d032bd2b28b1d84662ee8d9c5ac61bc3b Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Sat, 14 Oct 2023 23:34:02 +1300 Subject: [PATCH 011/464] Paginate removing all reactions from Matrix-side --- d2m/actions/remove-reaction.js | 36 ++++++++++++-------- d2m/converters/remove-reaction.js | 14 ++++---- d2m/converters/remove-reaction.test.js | 46 ++++++++++++-------------- matrix/api.js | 6 +++- 4 files changed, 56 insertions(+), 46 deletions(-) diff --git a/d2m/actions/remove-reaction.js b/d2m/actions/remove-reaction.js index b2dba0f..95fc0aa 100644 --- a/d2m/actions/remove-reaction.js +++ b/d2m/actions/remove-reaction.js @@ -23,14 +23,22 @@ async function removeSomeReactions(data) { const eventIDForMessage = select("event_message", "event_id", {message_id: data.message_id, reaction_part: 0}).pluck().get() if (!eventIDForMessage) return - /** @type {Ty.Pagination>} */ - const relations = await api.getRelations(roomID, eventIDForMessage, "m.annotation") + /** @type {Ty.Event.Outer[]} */ + let reactions = [] + /** @type {string | undefined} */ + let nextBatch = undefined + do { + /** @type {Ty.Pagination>} */ + const res = await api.getRelations(roomID, eventIDForMessage, {from: nextBatch}, "m.annotation") + reactions = reactions.concat(res.chunk) + nextBatch = res.next_batch + } while (nextBatch) // Run the proper strategy and any strategy-specific database changes const removals = await - ( "user_id" in data ? removeReaction(data, relations) - : "emoji" in data ? removeEmojiReaction(data, relations) - : removeAllReactions(data, relations)) + ( "user_id" in data ? removeReaction(data, reactions) + : "emoji" in data ? removeEmojiReaction(data, reactions) + : removeAllReactions(data, reactions)) // Redact the events and delete individual stored events in the database for (const removal of removals) { @@ -41,33 +49,33 @@ async function removeSomeReactions(data) { /** * @param {DiscordTypes.GatewayMessageReactionRemoveDispatchData} data - * @param {Ty.Pagination>} relations + * @param {Ty.Event.Outer[]} reactions */ -async function removeReaction(data, relations) { +async function removeReaction(data, reactions) { const key = await emojiToKey.emojiToKey(data.emoji) - return converter.removeReaction(data, relations, key) + return converter.removeReaction(data, reactions, key) } /** * @param {DiscordTypes.GatewayMessageReactionRemoveEmojiDispatchData} data - * @param {Ty.Pagination>} relations + * @param {Ty.Event.Outer[]} reactions */ -async function removeEmojiReaction(data, relations) { +async function removeEmojiReaction(data, reactions) { const key = await emojiToKey.emojiToKey(data.emoji) const discordPreferredEncoding = emoji.encodeEmoji(key, undefined) db.prepare("DELETE FROM reaction WHERE message_id = ? AND encoded_emoji = ?").run(data.message_id, discordPreferredEncoding) - return converter.removeEmojiReaction(data, relations, key) + return converter.removeEmojiReaction(data, reactions, key) } /** * @param {DiscordTypes.GatewayMessageReactionRemoveAllDispatchData} data - * @param {Ty.Pagination>} relations + * @param {Ty.Event.Outer[]} reactions */ -async function removeAllReactions(data, relations) { +async function removeAllReactions(data, reactions) { db.prepare("DELETE FROM reaction WHERE message_id = ?").run(data.message_id) - return converter.removeAllReactions(data, relations) + return converter.removeAllReactions(data, reactions) } module.exports.removeSomeReactions = removeSomeReactions diff --git a/d2m/converters/remove-reaction.js b/d2m/converters/remove-reaction.js index 4fed269..a6c8ace 100644 --- a/d2m/converters/remove-reaction.js +++ b/d2m/converters/remove-reaction.js @@ -17,15 +17,15 @@ const utils = sync.require("../../m2d/converters/utils") /** * @param {DiscordTypes.GatewayMessageReactionRemoveDispatchData} data - * @param {Ty.Pagination>} relations + * @param {Ty.Event.Outer[]} reactions * @param {string} key */ -function removeReaction(data, relations, key) { +function removeReaction(data, reactions, key) { /** @type {ReactionRemoveRequest[]} */ const removals = [] const wantToRemoveMatrixReaction = data.user_id === discord.application.id - for (const event of relations.chunk) { + for (const event of reactions) { const eventID = event.event_id if (event.content["m.relates_to"].key === key) { const lookingAtMatrixReaction = !utils.eventSenderIsFromDiscord(event.sender) @@ -52,14 +52,14 @@ function removeReaction(data, relations, key) { /** * @param {DiscordTypes.GatewayMessageReactionRemoveEmojiDispatchData} data - * @param {Ty.Pagination>} relations + * @param {Ty.Event.Outer[]} relations * @param {string} key */ function removeEmojiReaction(data, relations, key) { /** @type {ReactionRemoveRequest[]} */ const removals = [] - for (const event of relations.chunk) { + for (const event of relations) { const eventID = event.event_id if (event.content["m.relates_to"].key === key) { const mxid = utils.eventSenderIsFromDiscord(event.sender) ? event.sender : null @@ -72,11 +72,11 @@ function removeEmojiReaction(data, relations, key) { /** * @param {DiscordTypes.GatewayMessageReactionRemoveAllDispatchData} data - * @param {Ty.Pagination>} relations + * @param {Ty.Event.Outer[]} relations * @returns {ReactionRemoveRequest[]} */ function removeAllReactions(data, relations) { - return relations.chunk.map(event => { + return relations.map(event => { const eventID = event.event_id const mxid = utils.eventSenderIsFromDiscord(event.sender) ? event.sender : null return {eventID, mxid} diff --git a/d2m/converters/remove-reaction.test.js b/d2m/converters/remove-reaction.test.js index a63721f..dc6eda5 100644 --- a/d2m/converters/remove-reaction.test.js +++ b/d2m/converters/remove-reaction.test.js @@ -29,30 +29,28 @@ function fakeAllReactionRemoval() { } } -function fakeChunk(chunk) { - return { - chunk: chunk.map(({sender, key}, i) => ({ - content: { - "m.relates_to": { - rel_type: "m.annotation", - event_id: "$message", - key - } - }, - event_id: `$reaction_${i}`, - sender, - type: "m.reaction", - origin_server_ts: 0, - room_id: "!THE_ROOM", - unsigned: null - })) - } +function fakeReactions(reactions) { + return reactions.map(({sender, key}, i) => ({ + content: { + "m.relates_to": { + rel_type: "m.annotation", + event_id: "$message", + key + } + }, + event_id: `$reaction_${i}`, + sender, + type: "m.reaction", + origin_server_ts: 0, + room_id: "!THE_ROOM", + unsigned: null + })) } test("remove reaction: a specific discord user's reaction is removed", t => { const removals = removeReaction.removeReaction( fakeSpecificReactionRemoval("820865262526005258", "🐈", null), - fakeChunk([{key: "🐈", sender: "@_ooye_crunch_god:cadence.moe"}]), + fakeReactions([{key: "🐈", sender: "@_ooye_crunch_god:cadence.moe"}]), "🐈" ) t.deepEqual(removals, [{ @@ -64,7 +62,7 @@ test("remove reaction: a specific discord user's reaction is removed", t => { test("remove reaction: a specific matrix user's reaction is removed", t => { const removals = removeReaction.removeReaction( fakeSpecificReactionRemoval(BRIDGE_ID, "🐈", null), - fakeChunk([{key: "🐈", sender: "@cadence:cadence.moe"}]), + fakeReactions([{key: "🐈", sender: "@cadence:cadence.moe"}]), "🐈" ) t.deepEqual(removals, [{ @@ -77,7 +75,7 @@ test("remove reaction: a specific matrix user's reaction is removed", t => { test("remove reaction: a specific discord user's reaction is removed when there are multiple reactions", t => { const removals = removeReaction.removeReaction( fakeSpecificReactionRemoval("820865262526005258", "🐈", null), - fakeChunk([ + fakeReactions([ {key: "🐈‍⬛", sender: "@_ooye_crunch_god:cadence.moe"}, {key: "🐈", sender: "@_ooye_crunch_god:cadence.moe"}, {key: "🐈", sender: "@_ooye_extremity:cadence.moe"}, @@ -95,7 +93,7 @@ test("remove reaction: a specific discord user's reaction is removed when there test("remove reaction: a specific reaction leads to all matrix users' reaction of the emoji being removed", t => { const removals = removeReaction.removeReaction( fakeSpecificReactionRemoval(BRIDGE_ID, "🐈", null), - fakeChunk([ + fakeReactions([ {key: "🐈", sender: "@_ooye_crunch_god:cadence.moe"}, {key: "🐈", sender: "@cadence:cadence.moe"}, {key: "🐈‍⬛", sender: "@zoe:cadence.moe"}, @@ -118,7 +116,7 @@ test("remove reaction: a specific reaction leads to all matrix users' reaction o test("remove reaction: an emoji removes all instances of the emoij from both sides", t => { const removals = removeReaction.removeEmojiReaction( fakeEmojiReactionRemoval("🐈", null), - fakeChunk([ + fakeReactions([ {key: "🐈", sender: "@_ooye_crunch_god:cadence.moe"}, {key: "🐈", sender: "@cadence:cadence.moe"}, {key: "🐈‍⬛", sender: "@zoe:cadence.moe"}, @@ -145,7 +143,7 @@ test("remove reaction: an emoji removes all instances of the emoij from both sid test("remove reaction: remove all removes all from both sides", t => { const removals = removeReaction.removeAllReactions( fakeAllReactionRemoval(), - fakeChunk([ + fakeReactions([ {key: "🐈", sender: "@_ooye_crunch_god:cadence.moe"}, {key: "🐈", sender: "@cadence:cadence.moe"}, {key: "🐈‍⬛", sender: "@zoe:cadence.moe"}, diff --git a/matrix/api.js b/matrix/api.js index 7ec044a..5509930 100644 --- a/matrix/api.js +++ b/matrix/api.js @@ -112,12 +112,16 @@ function getJoinedMembers(roomID) { /** * @param {string} roomID * @param {string} eventID + * @param {{from?: string, limit?: any}} pagination * @param {string?} [relType] * @returns {Promise>>} */ -function getRelations(roomID, eventID, relType) { +function getRelations(roomID, eventID, pagination, relType) { let path = `/client/v1/rooms/${roomID}/relations/${eventID}` if (relType) path += `/${relType}` + if (!pagination.from) delete pagination.from + if (!pagination.limit) pagination.limit = 50 // get a little more consistency between homeservers + path += `?${new URLSearchParams(pagination)}` return mreq.mreq("GET", path) } From fff8f0d94c344c13a84463ced6e7c748d37f0141 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Sat, 14 Oct 2023 23:58:22 +1300 Subject: [PATCH 012/464] Review and upgrade dependencies --- package-lock.json | 162 ++++++++++++++++++++-------------------------- package.json | 8 +-- readme.md | 2 +- 3 files changed, 75 insertions(+), 97 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3847ee1..a005645 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,9 +10,9 @@ "license": "AGPL-3.0-or-later", "dependencies": { "@chriscdn/promise-semaphore": "^2.0.1", - "better-sqlite3": "^8.3.0", + "better-sqlite3": "^9.0.0", "chunk-text": "^2.0.1", - "cloudstorm": "^0.8.0", + "cloudstorm": "^0.9.0", "discord-markdown": "git+https://git.sr.ht/~cadence/nodejs-discord-markdown#abc56d544072a1dc5624adfea455b0e902adf7b3", "entities": "^4.5.0", "giframe": "github:cloudrac3r/giframe#v0.4.1", @@ -25,7 +25,7 @@ "pngjs": "^7.0.0", "prettier-bytes": "^1.0.4", "sharp": "^0.32.6", - "snowtransfer": "^0.8.0", + "snowtransfer": "^0.9.0", "stream-mime-type": "^1.0.2", "try-to-catch": "^3.0.1", "turndown": "^7.1.2", @@ -36,7 +36,7 @@ "@types/node-fetch": "^2.6.3", "c8": "^8.0.1", "cross-env": "^7.0.3", - "discord-api-types": "^0.37.53", + "discord-api-types": "^0.37.60", "supertape": "^8.3.0", "tap-dot": "github:cloudrac3r/tap-dot#9dd7750ececeae3a96afba91905be812b6b2cc2d" } @@ -48,9 +48,9 @@ "dev": true }, "node_modules/@chriscdn/promise-semaphore": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@chriscdn/promise-semaphore/-/promise-semaphore-2.0.1.tgz", - "integrity": "sha512-C0Ku5DNZFbafbSRXagidIaRgzhgGmSHk4aAgPpmmHEostazBiSaMryovC/Aix3vRLNuaeGDKN/DHoNECmMD6jg==" + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@chriscdn/promise-semaphore/-/promise-semaphore-2.0.7.tgz", + "integrity": "sha512-xsa5SAYSBnYjqvGnzmaLca4X/RFeOl+ziCsIHl5iHkFBgE4NgWupB4z3A1rVMBM2I8TEKaah+5iu9Cm7gQu9JQ==" }, "node_modules/@cloudcmd/stub": { "version": "4.0.1", @@ -152,6 +152,14 @@ "node": ">=8" } }, + "node_modules/@fastify/busboy": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.0.0.tgz", + "integrity": "sha512-JUFJad5lv7jxj926GPgymrWQxxjPYuJNiNjNMzqT+HiuP6Vl3dk5xzG+8sTX96np0ZAluvaMzPsjhHZ5rNuNQQ==", + "engines": { + "node": ">=14" + } + }, "node_modules/@istanbuljs/schema": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", @@ -231,15 +239,15 @@ "dev": true }, "node_modules/@supertape/engine-loader": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@supertape/engine-loader/-/engine-loader-1.1.3.tgz", - "integrity": "sha512-5ilgEng0WBvMQjNJWQ/bnAA6HKgbLKxTya2C0RxFH0LYSN5faBVtgxjLDvTQ+5L+ZxjK/7ooQDDaRS1Mo0ga5Q==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@supertape/engine-loader/-/engine-loader-2.0.0.tgz", + "integrity": "sha512-1G2MmfZnSxx546omLPAVNgvG/iqOQZGiXHnjJ2JXKvuf2lpPdDRnNm5eLl81lvEG473zE9neX979TzeFcr3Dxw==", "dev": true, "dependencies": { "try-catch": "^3.0.0" }, "engines": { - "node": ">=14" + "node": ">=16" } }, "node_modules/@supertape/formatter-fail": { @@ -268,9 +276,9 @@ } }, "node_modules/@supertape/formatter-progress-bar": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@supertape/formatter-progress-bar/-/formatter-progress-bar-3.0.0.tgz", - "integrity": "sha512-rVFAQ21eApq3TQV8taFLNcCxcGZvvOPxQC63swdmHFCp+07Dt3tvC/aFxF35NLobc3rySasGSEuPucpyoPrjfg==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@supertape/formatter-progress-bar/-/formatter-progress-bar-4.1.0.tgz", + "integrity": "sha512-MYwso7kbiTE0DaZgbiSlNOikmEcFdL4RQUu1JvnW+cS6ZLl3fqNnmvKa1a14VChKyHzfaTKYLuqToN8zgUjP2g==", "dev": true, "dependencies": { "chalk": "^4.1.0", @@ -280,7 +288,7 @@ "once": "^1.4.0" }, "engines": { - "node": ">=14" + "node": ">=16" } }, "node_modules/@supertape/formatter-short": { @@ -325,19 +333,19 @@ "dev": true }, "node_modules/@types/node": { - "version": "18.16.5", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.16.5.tgz", - "integrity": "sha512-seOA34WMo9KB+UA78qaJoCO20RJzZGVXQ5Sh6FWu0g/hfT44nKXnej3/tCQl7FL97idFpBhisLYCTB50S0EirA==", + "version": "18.18.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.18.5.tgz", + "integrity": "sha512-4slmbtwV59ZxitY4ixUZdy1uRLf9eSIvBWPQxNjhHYWEtn0FryfKpyS2cvADYXTayWdKEIsJengncrVvkI4I6A==", "dev": true }, "node_modules/@types/node-fetch": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.3.tgz", - "integrity": "sha512-ETTL1mOEdq/sxUtgtOhKjyB2Irra4cjxksvcMUR5Zr4n+PxVhsCD9WS46oPbHL3et9Zde7CNRr+WUNlcHvsX+w==", + "version": "2.6.6", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.6.tgz", + "integrity": "sha512-95X8guJYhfqiuVVhRFxVQcf4hW/2bCuoPwDasMf/531STFoNoWTT7YDnWdXHEZKqAGUigmpG31r2FE70LwnzJw==", "dev": true, "dependencies": { "@types/node": "*", - "form-data": "^3.0.0" + "form-data": "^4.0.0" } }, "node_modules/@types/prop-types": { @@ -446,7 +454,8 @@ "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true }, "node_modules/available-typed-arrays": { "version": "1.0.5", @@ -512,13 +521,13 @@ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" }, "node_modules/better-sqlite3": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-8.4.0.tgz", - "integrity": "sha512-NmsNW1CQvqMszu/CFAJ3pLct6NEFlNfuGM6vw72KHkjOD1UDnL96XNN1BMQc1hiHo8vE2GbOWQYIpZ+YM5wrZw==", + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-9.0.0.tgz", + "integrity": "sha512-lDxQ9qg/XuUHZG6xzrQaMHkNWl37t35/LPB/VJGV8DdScSuGFNfFSqgscXEd8UIuyk/d9wU8iaMxQa4If5Wqog==", "hasInstallScript": true, "dependencies": { "bindings": "^1.5.0", - "prebuild-install": "^7.1.0" + "prebuild-install": "^7.1.1" } }, "node_modules/bindings": { @@ -631,17 +640,6 @@ "ieee754": "^1.2.1" } }, - "node_modules/busboy": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", - "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", - "dependencies": { - "streamsearch": "^1.1.0" - }, - "engines": { - "node": ">=10.16.0" - } - }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -784,14 +782,14 @@ } }, "node_modules/cloudstorm": { - "version": "0.8.3", - "resolved": "https://registry.npmjs.org/cloudstorm/-/cloudstorm-0.8.3.tgz", - "integrity": "sha512-4c2rqFFvzM4P3pcnjnGUlYuyBjx/xnMew6imB0sFwmNLITLCTLYa3qGkrnhI1g/tM0fqg+Gr+EmDHiDZfEr9LQ==", + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/cloudstorm/-/cloudstorm-0.9.0.tgz", + "integrity": "sha512-n5M5TVnvm/X5vdNKy85q8muMregnvPWxv7HGSDCChL/FReOh2PGOm0FZJVm4hcB+KIM07KmiJTiCSQTnrTrSnQ==", "dependencies": { - "snowtransfer": "^0.8.3" + "snowtransfer": "^0.9.0" }, "engines": { - "node": ">=12.0.0" + "node": ">=14.8.0" } }, "node_modules/color": { @@ -841,6 +839,7 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, "dependencies": { "delayed-stream": "~1.0.0" }, @@ -1014,6 +1013,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, "engines": { "node": ">=0.4.0" } @@ -1053,9 +1053,9 @@ } }, "node_modules/discord-api-types": { - "version": "0.37.53", - "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.37.53.tgz", - "integrity": "sha512-N6uUgv50OyP981Mfxrrt0uxcqiaNr0BDaQIoqfk+3zM2JpZtwU9v7ce1uaFAP53b2xSDvcbrk80Kneui6XJgGg==" + "version": "0.37.60", + "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.37.60.tgz", + "integrity": "sha512-5BELXTsv7becqVHrD81nZrqT4oEyXXWBwbsO/kwDDu6X3u19VV1tYDB5I5vaVAK+c1chcDeheI9zACBLm41LiQ==" }, "node_modules/discord-markdown": { "version": "2.4.1", @@ -1345,9 +1345,9 @@ } }, "node_modules/form-data": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", - "integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", "dev": true, "dependencies": { "asynckit": "^0.4.0", @@ -1549,9 +1549,9 @@ } }, "node_modules/heatsync": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/heatsync/-/heatsync-2.4.1.tgz", - "integrity": "sha512-cRzLwnKnJ5O4dQWXiJyFp4myKY8lGfK+49/SbPsvnr3pf2PNG1Xh8pPono303cjJeFpaPSTs609mQH1xhPVyzA==", + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/heatsync/-/heatsync-2.4.2.tgz", + "integrity": "sha512-s+YzwGpCjsJLRCuz6Ur8JFKz7vXEMFAPLqDbaEvMR5Um/IPPJpmupBH7LKeiyfGkIScFz9iyBPa1TifcoU4D7A==", "dependencies": { "backtracker": "3.3.2" } @@ -2192,9 +2192,9 @@ "integrity": "sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==" }, "node_modules/node-fetch": { - "version": "2.6.12", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.12.tgz", - "integrity": "sha512-C/fGU2E8ToujUivIO0H+tpQ6HWo4eEmchoPIoXtxCrVghxdKq+QOHqEZW7tuP3KlV3bC8FRMO5nMCC7Zm1VP6g==", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", "dependencies": { "whatwg-url": "^5.0.0" }, @@ -2922,29 +2922,15 @@ } }, "node_modules/snowtransfer": { - "version": "0.8.3", - "resolved": "https://registry.npmjs.org/snowtransfer/-/snowtransfer-0.8.3.tgz", - "integrity": "sha512-0X6NLFBUKppYT5VH/mVQNGX+ufv0AndunZC84MqGAR/3rfTIGQblgGJlHlDQbeCytlXdMpgRHIGQnBFlE094NQ==", + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/snowtransfer/-/snowtransfer-0.9.0.tgz", + "integrity": "sha512-43Q0pvk7ZV8uZwcL/IhEFYKFZj53FOqxr2dVDwduPT87eHOJzfs8aQ+tNDqsjW6OMUBurwR3XZZFEpQ2f/XzXA==", "dependencies": { - "discord-api-types": "^0.37.47", - "form-data": "^4.0.0", - "undici": "^5.22.1" + "discord-api-types": "^0.37.60", + "undici": "^5.26.3" }, "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/snowtransfer/node_modules/form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" + "node": ">=14.18.0" } }, "node_modules/source-map": { @@ -3022,14 +3008,6 @@ "node": ">=10" } }, - "node_modules/streamsearch": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", - "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", - "engines": { - "node": ">=10.0.0" - } - }, "node_modules/streamx": { "version": "2.15.1", "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.15.1.tgz", @@ -3122,18 +3100,18 @@ } }, "node_modules/supertape": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/supertape/-/supertape-8.3.0.tgz", - "integrity": "sha512-dcMylmkr1Mctr5UBCrlvZynuBRuLvlkWJLGXdL/PcI41BERnObO+kV0PeZhH5n6lwVnvK2xfvZyN32WIAPf/tw==", + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/supertape/-/supertape-8.6.0.tgz", + "integrity": "sha512-zvAXZaliVu8qpGRx5KiYQfPZcQD9B361lmRtXb3zyilpHHc0/5ygQ9MfWYZEwXywxHDfve3w8ZukI/NKPT9PyA==", "dev": true, "dependencies": { "@cloudcmd/stub": "^4.0.0", "@putout/cli-keypress": "^1.0.0", "@putout/cli-validate-args": "^1.0.1", - "@supertape/engine-loader": "^1.0.0", + "@supertape/engine-loader": "^2.0.0", "@supertape/formatter-fail": "^3.0.0", "@supertape/formatter-json-lines": "^2.0.0", - "@supertape/formatter-progress-bar": "^3.0.0", + "@supertape/formatter-progress-bar": "^4.0.0", "@supertape/formatter-short": "^2.0.0", "@supertape/formatter-tap": "^3.0.0", "@supertape/operator-stub": "^3.0.0", @@ -3414,11 +3392,11 @@ } }, "node_modules/undici": { - "version": "5.22.1", - "resolved": "https://registry.npmjs.org/undici/-/undici-5.22.1.tgz", - "integrity": "sha512-Ji2IJhFXZY0x/0tVBXeQwgPlLWw13GVzpsWPQ3rV50IFMMof2I55PZZxtm4P6iNq+L5znYN9nSTAq0ZyE6lSJw==", + "version": "5.26.3", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.26.3.tgz", + "integrity": "sha512-H7n2zmKEWgOllKkIUkLvFmsJQj062lSm3uA4EYApG8gLuiOM0/go9bIoC3HVaSnfg4xunowDE2i9p8drkXuvDw==", "dependencies": { - "busboy": "^1.6.0" + "@fastify/busboy": "^2.0.0" }, "engines": { "node": ">=14.0" diff --git a/package.json b/package.json index 6a9deea..29f6f1c 100644 --- a/package.json +++ b/package.json @@ -16,9 +16,9 @@ "license": "AGPL-3.0-or-later", "dependencies": { "@chriscdn/promise-semaphore": "^2.0.1", - "better-sqlite3": "^8.3.0", + "better-sqlite3": "^9.0.0", "chunk-text": "^2.0.1", - "cloudstorm": "^0.8.0", + "cloudstorm": "^0.9.0", "discord-markdown": "git+https://git.sr.ht/~cadence/nodejs-discord-markdown#abc56d544072a1dc5624adfea455b0e902adf7b3", "entities": "^4.5.0", "giframe": "github:cloudrac3r/giframe#v0.4.1", @@ -31,7 +31,7 @@ "pngjs": "^7.0.0", "prettier-bytes": "^1.0.4", "sharp": "^0.32.6", - "snowtransfer": "^0.8.0", + "snowtransfer": "^0.9.0", "stream-mime-type": "^1.0.2", "try-to-catch": "^3.0.1", "turndown": "^7.1.2", @@ -42,7 +42,7 @@ "@types/node-fetch": "^2.6.3", "c8": "^8.0.1", "cross-env": "^7.0.3", - "discord-api-types": "^0.37.53", + "discord-api-types": "^0.37.60", "supertape": "^8.3.0", "tap-dot": "github:cloudrac3r/tap-dot#9dd7750ececeae3a96afba91905be812b6b2cc2d" }, diff --git a/readme.md b/readme.md index 5c97745..61d1951 100644 --- a/readme.md +++ b/readme.md @@ -73,7 +73,7 @@ You'll need: Follow these steps: -1. [Get Node.js version 18 or later](https://nodejs.org/en/download/releases) (the version is required by the matrix-appservice dependency) +1. [Get Node.js version 18 or later](https://nodejs.org/en/download/releases) (the version is required by the better-sqlite3 and matrix-appservice dependencies) 1. Clone this repo and checkout a specific tag. (Development happens on main. Stabler versions are tagged.) From 9c3f1abd3a2c12d30b57bdec55d85ad352a5dab5 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Sun, 15 Oct 2023 00:26:52 +1300 Subject: [PATCH 013/464] Upload files to Discord as streams for speed --- m2d/actions/channel-webhook.js | 5 ++-- m2d/actions/send-event.js | 40 +++++++++++++++--------------- m2d/converters/event-to-message.js | 5 ++-- 3 files changed, 25 insertions(+), 25 deletions(-) diff --git a/m2d/actions/channel-webhook.js b/m2d/actions/channel-webhook.js index 3bb728d..52b4095 100644 --- a/m2d/actions/channel-webhook.js +++ b/m2d/actions/channel-webhook.js @@ -2,6 +2,7 @@ const assert = require("assert").strict const DiscordTypes = require("discord-api-types/v10") +const {Readable} = require("stream") const passthrough = require("../../passthrough") const {discord, db, select} = passthrough @@ -51,7 +52,7 @@ async function withWebhook(channelID, callback) { /** * @param {string} channelID - * @param {DiscordTypes.RESTPostAPIWebhookWithTokenJSONBody & {files?: {name: string, file: Buffer}[]}} data + * @param {DiscordTypes.RESTPostAPIWebhookWithTokenJSONBody & {files?: {name: string, file: Buffer | Readable}[]}} data * @param {string} [threadID] */ async function sendMessageWithWebhook(channelID, data, threadID) { @@ -64,7 +65,7 @@ async function sendMessageWithWebhook(channelID, data, threadID) { /** * @param {string} channelID * @param {string} messageID - * @param {DiscordTypes.RESTPatchAPIWebhookWithTokenMessageJSONBody & {files?: {name: string, file: Buffer}[]}} data + * @param {DiscordTypes.RESTPatchAPIWebhookWithTokenMessageJSONBody & {files?: {name: string, file: Buffer | Readable}[]}} data * @param {string} [threadID] */ async function editMessageWithWebhook(channelID, messageID, data, threadID) { diff --git a/m2d/actions/send-event.js b/m2d/actions/send-event.js index 13b5650..9b3fbec 100644 --- a/m2d/actions/send-event.js +++ b/m2d/actions/send-event.js @@ -1,11 +1,11 @@ // @ts-check -const assert = require("assert").strict -const crypto = require("crypto") -const {pipeline} = require("stream") -const {promisify} = require("util") const Ty = require("../../types") const DiscordTypes = require("discord-api-types/v10") +const {Readable} = require("stream") +const assert = require("assert").strict +const crypto = require("crypto") +const fetch = require("node-fetch").default const passthrough = require("../../passthrough") const {sync, discord, db, select} = passthrough @@ -17,13 +17,12 @@ const eventToMessage = sync.require("../converters/event-to-message") const api = sync.require("../../matrix/api") /** - * @param {DiscordTypes.RESTPostAPIWebhookWithTokenJSONBody & {files?: {name: string, file: Buffer}[], pendingFiles?: ({name: string, url: string} | {name: string, url: string, key: string, iv: string} | {name: string, buffer: Buffer})[]}} message - * @returns {Promise} + * @param {DiscordTypes.RESTPostAPIWebhookWithTokenJSONBody & {files?: {name: string, file: Buffer | Readable}[], pendingFiles?: ({name: string, url: string} | {name: string, url: string, key: string, iv: string} | {name: string, buffer: Buffer | Readable})[]}} message + * @returns {Promise} */ async function resolvePendingFiles(message) { if (!message.pendingFiles) return message const files = await Promise.all(message.pendingFiles.map(async p => { - let fileBuffer if ("buffer" in p) { return { name: p.name, @@ -31,21 +30,22 @@ async function resolvePendingFiles(message) { } } if ("key" in p) { - // Encrypted + // Encrypted file const d = crypto.createDecipheriv("aes-256-ctr", Buffer.from(p.key, "base64url"), Buffer.from(p.iv, "base64url")) - fileBuffer = await fetch(p.url).then(res => res.arrayBuffer()).then(x => { - return Buffer.concat([ - d.update(Buffer.from(x)), - d.final() - ]) - }) + // @ts-ignore + fetch(p.url).then(res => res.body.pipe(d)) + return { + name: p.name, + file: d + } } else { - // Unencrypted - fileBuffer = await fetch(p.url).then(res => res.arrayBuffer()).then(x => Buffer.from(x)) - } - return { - name: p.name, - file: fileBuffer // TODO: Once SnowTransfer supports ReadableStreams for attachment uploads, pass in those instead of Buffers + // Unencrypted file + /** @type {Readable} */ // @ts-ignore + const body = await fetch(p.url).then(res => res.body) + return { + name: p.name, + file: body + } } })) const newMessage = { diff --git a/m2d/converters/event-to-message.js b/m2d/converters/event-to-message.js index 5f6f3e6..8907508 100644 --- a/m2d/converters/event-to-message.js +++ b/m2d/converters/event-to-message.js @@ -2,6 +2,7 @@ const Ty = require("../../types") const DiscordTypes = require("discord-api-types/v10") +const {Readable} = require("stream") const chunk = require("chunk-text") const TurndownService = require("turndown") const assert = require("assert").strict @@ -9,8 +10,6 @@ const entities = require("entities") const passthrough = require("../../passthrough") const {sync, db, discord, select, from} = passthrough -/** @type {import("../../matrix/file")} */ -const file = sync.require("../../matrix/file") /** @type {import("../converters/utils")} */ const utils = sync.require("../converters/utils") /** @type {import("./emoji-sheet")} */ @@ -245,7 +244,7 @@ async function uploadEndOfMessageSpriteSheet(content, attachments, pendingFiles) * @param {{api: import("../../matrix/api")}} di simple-as-nails dependency injection for the matrix API */ async function eventToMessage(event, guild, di) { - /** @type {(DiscordTypes.RESTPostAPIWebhookWithTokenJSONBody & {files?: {name: string, file: Buffer}[]})[]} */ + /** @type {(DiscordTypes.RESTPostAPIWebhookWithTokenJSONBody & {files?: {name: string, file: Buffer | Readable}[]})[]} */ let messages = [] let displayName = event.sender From a542bbdca79ba0825e08fe589f85224e212c9b88 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Mon, 16 Oct 2023 16:24:48 +1300 Subject: [PATCH 014/464] Update discord-markdown to support list and header --- package-lock.json | 6 +++--- package.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index a005645..11585f8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,7 @@ "better-sqlite3": "^9.0.0", "chunk-text": "^2.0.1", "cloudstorm": "^0.9.0", - "discord-markdown": "git+https://git.sr.ht/~cadence/nodejs-discord-markdown#abc56d544072a1dc5624adfea455b0e902adf7b3", + "discord-markdown": "git+https://git.sr.ht/~cadence/nodejs-discord-markdown#de519353668c87ecc8c543e9749093481bc72ff8", "entities": "^4.5.0", "giframe": "github:cloudrac3r/giframe#v0.4.1", "heatsync": "^2.4.1", @@ -1058,8 +1058,8 @@ "integrity": "sha512-5BELXTsv7becqVHrD81nZrqT4oEyXXWBwbsO/kwDDu6X3u19VV1tYDB5I5vaVAK+c1chcDeheI9zACBLm41LiQ==" }, "node_modules/discord-markdown": { - "version": "2.4.1", - "resolved": "git+https://git.sr.ht/~cadence/nodejs-discord-markdown#abc56d544072a1dc5624adfea455b0e902adf7b3", + "version": "2.5.1", + "resolved": "git+https://git.sr.ht/~cadence/nodejs-discord-markdown#de519353668c87ecc8c543e9749093481bc72ff8", "license": "MIT", "dependencies": { "simple-markdown": "^0.7.2" diff --git a/package.json b/package.json index 29f6f1c..42988c8 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "better-sqlite3": "^9.0.0", "chunk-text": "^2.0.1", "cloudstorm": "^0.9.0", - "discord-markdown": "git+https://git.sr.ht/~cadence/nodejs-discord-markdown#abc56d544072a1dc5624adfea455b0e902adf7b3", + "discord-markdown": "git+https://git.sr.ht/~cadence/nodejs-discord-markdown#de519353668c87ecc8c543e9749093481bc72ff8", "entities": "^4.5.0", "giframe": "github:cloudrac3r/giframe#v0.4.1", "heatsync": "^2.4.1", From 762e48230c682fb9256119a51d600dc6bbafbadf Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Mon, 16 Oct 2023 16:47:42 +1300 Subject: [PATCH 015/464] Ensure the appservice bot user is registered Synapse does it automatically, but Conduit requires this HTTP call. --- d2m/actions/register-user.js | 2 +- scripts/seed.js | 17 ++++++++++++----- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/d2m/actions/register-user.js b/d2m/actions/register-user.js index 9b5527f..b605c3a 100644 --- a/d2m/actions/register-user.js +++ b/d2m/actions/register-user.js @@ -36,7 +36,7 @@ async function createSim(user) { await api.register(localpart) } catch (e) { // If user creation fails, manually undo the database change. Still isn't perfect, but should help. - // (A transaction would be preferable, but I don't think it's safe to leave transaction open across event loop ticks.) + // (I would prefer a transaction, but it's not safe to leave transactions open across event loop ticks.) db.prepare("DELETE FROM sim WHERE user_id = ?").run(user.id) throw e } diff --git a/scripts/seed.js b/scripts/seed.js index 0f2f23d..4fc704c 100644 --- a/scripts/seed.js +++ b/scripts/seed.js @@ -67,6 +67,18 @@ async function uploadAutoEmoji(guild, name, filename) { console.log("✅ Database is ready...") + // ensure appservice bot user is registered... + try { + await api.register(reg.sender_localpart) + } catch (e) { + if (e.data?.error !== "Internal server error") throw e // "Internal server error" is the only OK error because Synapse says this if you try to register the same username twice. + } + + // upload initial images... + const avatarUrl = await file.uploadDiscordFileToMxc("https://cadence.moe/friends/out_of_your_element.png") + + console.log("✅ Matrix appservice login works...") + // upload the L1 L2 emojis to some guild const emojis = db.prepare("SELECT name FROM auto_emoji WHERE name = 'L1' OR name = 'L2'").pluck().all() if (emojis.length !== 2) { @@ -104,11 +116,6 @@ async function uploadAutoEmoji(guild, name, filename) { } console.log("✅ Emojis are ready...") - // ensure homeserver well-known is valid and returns reg.ooye.server_name... - - // upload initial images... - const avatarUrl = await file.uploadDiscordFileToMxc("https://cadence.moe/friends/out_of_your_element.png") - // set profile data on discord... const avatarImageBuffer = await fetch("https://cadence.moe/friends/out_of_your_element.png").then(res => res.arrayBuffer()) await discord.snow.user.updateSelf({avatar: "data:image/png;base64," + Buffer.from(avatarImageBuffer).toString("base64")}) From afbbe0da3d9bf561e3f5394c0fb6009a5d9f6711 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Sat, 28 Oct 2023 00:24:42 +1300 Subject: [PATCH 016/464] Fix more edge-case embed formatting --- .../message-to-event.embeds.test.js | 68 ++++++-- d2m/converters/message-to-event.js | 149 ++++++++++++------ m2d/converters/emoji.js | 1 + m2d/converters/event-to-message.js | 18 +++ m2d/converters/event-to-message.test.js | 43 ++++- m2d/converters/utils.js | 65 ++++++++ matrix/matrix-command-handler.js | 53 +------ package-lock.json | 23 ++- package.json | 4 +- readme.md | 8 +- test/data.js | 108 +++++++++++++ test/ooye-test-data.sql | 15 +- 12 files changed, 428 insertions(+), 127 deletions(-) diff --git a/d2m/converters/message-to-event.embeds.test.js b/d2m/converters/message-to-event.embeds.test.js index 93d189c..173e016 100644 --- a/d2m/converters/message-to-event.embeds.test.js +++ b/d2m/converters/message-to-event.embeds.test.js @@ -35,14 +35,14 @@ test("message2event embeds: nothing but a field", async t => { $type: "m.room.message", "m.mentions": {}, msgtype: "m.notice", - body: "> **Amanda 🎵#2192 :online:" - + "\n> willow tree, branch 0**" + body: "> ### Amanda 🎵#2192 :online:" + + "\n> willow tree, branch 0" + "\n> **❯ Uptime:**\n> 3m 55s\n> **❯ Memory:**\n> 64.45MB", format: "org.matrix.custom.html", - formatted_body: '
Amanda 🎵#2192 \":online:\"' + formatted_body: '

Amanda 🎵#2192 \":online:\"' + '
willow tree, branch 0
' + '
❯ Uptime:
3m 55s' - + '
❯ Memory:
64.45MB

' + + '
❯ Memory:
64.45MB

' }]) }) @@ -52,19 +52,19 @@ test("message2event embeds: reply with just an embed", async t => { $type: "m.room.message", msgtype: "m.notice", "m.mentions": {}, - body: "> [**⏺️ dynastic (@dynastic)**](https://twitter.com/i/user/719631291747078145)" - + "\n> \n> **https://twitter.com/i/status/1707484191963648161**" + body: "> ## ⏺️ dynastic (@dynastic) https://twitter.com/i/user/719631291747078145" + + "\n> \n> ## https://twitter.com/i/status/1707484191963648161" + "\n> \n> does anyone know where to find that one video of the really mysterious yam-like object being held up to a bunch of random objects, like clocks, and they have unexplained impossible reactions to it?" - + "\n> \n> **Retweets**" + + "\n> \n> ### Retweets" + "\n> 119" - + "\n> \n> **Likes**" + + "\n> \n> ### Likes" + "\n> 5581" - + "\n> \n> — Twitter", + + "\n> — Twitter", format: "org.matrix.custom.html", - formatted_body: '
⏺️ dynastic (@dynastic)' - + '

https://twitter.com/i/status/1707484191963648161' - + '

does anyone know where to find that one video of the really mysterious yam-like object being held up to a bunch of random objects, like clocks, and they have unexplained impossible reactions to it?' - + '

Retweets
119

Likes
5581

— Twitter
' + formatted_body: '

⏺️ dynastic (@dynastic)

' + + '

https://twitter.com/i/status/1707484191963648161' + + '

does anyone know where to find that one video of the really mysterious yam-like object being held up to a bunch of random objects, like clocks, and they have unexplained impossible reactions to it?' + + '

Retweets
119

Likes
5581

— Twitter
' }]) }) @@ -99,3 +99,45 @@ test("message2event embeds: image embed and attachment", async t => { "m.mentions": {} }]) }) + +test("message2event embeds: blockquote in embed", async t => { + const events = await messageToEvent(data.message_with_embeds.blockquote_in_embed, data.guild.general) + t.deepEqual(events, [{ + $type: "m.room.message", + msgtype: "m.text", + body: ":emoji: **4 |** #wonderland", + format: "org.matrix.custom.html", + formatted_body: `\":emoji:\" 4 | #wonderland`, + "m.mentions": {} + }, { + $type: "m.room.message", + msgtype: "m.notice", + body: "> ## ⏺️ minimus https://matrix.to/#/!qzDBLKlildpzrrOnFZ:cadence.moe/$dVCLyj6kxb3DaAWDtjcv2kdSny8JMMHdDhCMz8mDxVo\n> \n> reply draft\n> > The following is a message composed via consensus of the Stinker Council.\n> > \n> > For those who are not currently aware of our existence, we represent the organization known as Wonderland. Our previous mission centered around the assortment and study of puzzling objects, entities and other assorted phenomena. This mission was the focus of our organization for more than 28 years.\n> > \n> > Due to circumstances outside of our control, this directive has now changed. Our new mission will be the extermination of the stinker race.\n> > \n> > There will be no further communication.\n> \n> [Go to Message](https://matrix.to/#/!qzDBLKlildpzrrOnFZ:cadence.moe/$dVCLyj6kxb3DaAWDtjcv2kdSny8JMMHdDhCMz8mDxVo)", + format: "org.matrix.custom.html", + formatted_body: "

⏺️ minimus

reply draft

The following is a message composed via consensus of the Stinker Council.

For those who are not currently aware of our existence, we represent the organization known as Wonderland. Our previous mission centered around the assortment and study of puzzling objects, entities and other assorted phenomena. This mission was the focus of our organization for more than 28 years.

Due to circumstances outside of our control, this directive has now changed. Our new mission will be the extermination of the stinker race.

There will be no further communication.

Go to Message

", + "m.mentions": {} + }]) +}) + +test("message2event embeds: crazy html is all escaped", async t => { + const events = await messageToEvent(data.message_with_embeds.escaping_crazy_html_tags, data.guild.general) + t.deepEqual(events, [{ + $type: "m.room.message", + msgtype: "m.notice", + body: "> ## ⏺️ [Hey