From 0acf59bf489f417ec572cab125549c111a5e0e5f Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Thu, 7 Sep 2023 23:20:48 +1200 Subject: [PATCH 1/3] forcing space sync will unbridge deleted channels --- d2m/actions/create-room.js | 10 +++++++--- d2m/actions/create-space.js | 6 +++++- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/d2m/actions/create-room.js b/d2m/actions/create-room.js index e8284df..d8bab51 100644 --- a/d2m/actions/create-room.js +++ b/d2m/actions/create-room.js @@ -303,9 +303,13 @@ async function _unbridgeRoom(channelID) { /** @ts-ignore @type {DiscordTypes.APIGuildChannel} */ const channel = discord.channels.get(channelID) assert.ok(channel) + return unbridgeDeletedChannel(channel.id, channel.guild_id) +} + +async function unbridgeDeletedChannel(channelID, guildID) { const roomID = db.prepare("SELECT room_id from channel_room WHERE channel_id = ?").pluck().get(channelID) assert.ok(roomID) - const spaceID = db.prepare("SELECT space_id FROM guild_space WHERE guild_id = ?").pluck().get(channel.guild_id) + const spaceID = db.prepare("SELECT space_id FROM guild_space WHERE guild_id = ?").pluck().get(guildID) assert.ok(spaceID) // remove room from being a space member @@ -313,7 +317,7 @@ async function _unbridgeRoom(channelID) { await api.sendState(spaceID, "m.space.child", roomID, {}) // remove declaration that the room is bridged - await api.sendState(roomID, "uk.half-shot.bridge", `moe.cadence.ooye://discord/${channel.guild_id}/${channel.id}`, {}) + await api.sendState(roomID, "uk.half-shot.bridge", `moe.cadence.ooye://discord/${guildID}/${channelID}`, {}) // send a notification in the room await api.sendEvent(roomID, "m.room.message", { @@ -329,7 +333,6 @@ async function _unbridgeRoom(channelID) { assert.equal(changes, 1) } - /** * Async because it gets all space state from the homeserver, then if necessary sends one state event back. * @param {DiscordTypes.APIGuildTextChannel} channel @@ -377,3 +380,4 @@ module.exports.applyKStateDiffToRoom = applyKStateDiffToRoom module.exports.postApplyPowerLevels = postApplyPowerLevels module.exports._convertNameAndTopic = convertNameAndTopic module.exports._unbridgeRoom = _unbridgeRoom +module.exports.unbridgeDeletedChannel = unbridgeDeletedChannel diff --git a/d2m/actions/create-space.js b/d2m/actions/create-space.js index 0bdf1d1..34cf88a 100644 --- a/d2m/actions/create-space.js +++ b/d2m/actions/create-space.js @@ -145,7 +145,11 @@ async function syncSpaceFully(guildID) { for (const roomID of childRooms) { const channelID = db.prepare("SELECT channel_id FROM channel_room WHERE room_id = ?").pluck().get(roomID) if (!channelID) continue - await createRoom.syncRoom(channelID) + if (discord.channels.has(channelID)) { + await createRoom.syncRoom(channelID) + } else { + await createRoom.unbridgeDeletedChannel(channelID, guildID) + } } return spaceID From e4e28daf083a6d8b330ac436c032490895ea8be0 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Thu, 7 Sep 2023 23:48:44 +1200 Subject: [PATCH 2/3] m->d replies to file should use emoji indicator --- m2d/converters/event-to-message.js | 24 +++++++--- m2d/converters/event-to-message.test.js | 58 +++++++++++++++++++++++++ 2 files changed, 76 insertions(+), 6 deletions(-) diff --git a/m2d/converters/event-to-message.js b/m2d/converters/event-to-message.js index 1e66c7a..91dea5e 100644 --- a/m2d/converters/event-to-message.js +++ b/m2d/converters/event-to-message.js @@ -239,14 +239,26 @@ async function eventToMessage(event, guild, di) { const senderName = sender.match(/@([^:]*)/)?.[1] || sender const authorID = db.prepare("SELECT discord_id FROM sim WHERE mxid = ?").pluck().get(repliedToEvent.sender) if (authorID) { - replyLine += `<@${authorID}>:` + replyLine += `<@${authorID}>` } else { - replyLine += `Ⓜ️**${senderName}**:` + replyLine += `Ⓜ️**${senderName}**` } - const repliedToContent = repliedToEvent.content.formatted_body || repliedToEvent.content.body - const contentPreviewChunks = chunk(repliedToContent.replace(/.*<\/mx-reply>/, "").replace(/.*?<\/blockquote>/, "").replace(/(?:\n|
)+/g, " ").replace(/<[^>]+>/g, ""), 50) - const contentPreview = contentPreviewChunks.length > 1 ? contentPreviewChunks[0] + "..." : contentPreviewChunks[0] - replyLine = `> ${replyLine}\n> ${contentPreview}\n` + let contentPreview + const fileReplyContentAlternative = + ( repliedToEvent.content.msgtype === "m.image" ? "🖼️" + : repliedToEvent.content.msgtype === "m.video" ? "🎞️" + : repliedToEvent.content.msgtype === "m.audio" ? "🎶" + : repliedToEvent.content.msgtype === "m.file" ? "📄" + : null) + if (fileReplyContentAlternative) { + contentPreview = " " + fileReplyContentAlternative + } else { + const repliedToContent = repliedToEvent.content.formatted_body || repliedToEvent.content.body + const contentPreviewChunks = chunk(repliedToContent.replace(/.*<\/mx-reply>/, "").replace(/.*?<\/blockquote>/, "").replace(/(?:\n|
)+/g, " ").replace(/<[^>]+>/g, ""), 50) + contentPreview = ":\n> " + contentPreview += contentPreviewChunks.length > 1 ? contentPreviewChunks[0] + "..." : contentPreviewChunks[0] + } + replyLine = `> ${replyLine}${contentPreview}\n` })() if (event.content.format === "org.matrix.custom.html" && event.content.formatted_body) { diff --git a/m2d/converters/event-to-message.test.js b/m2d/converters/event-to-message.test.js index 40a4daa..4d85092 100644 --- a/m2d/converters/event-to-message.test.js +++ b/m2d/converters/event-to-message.test.js @@ -992,6 +992,64 @@ test("event2message: rich reply to a matrix user's long message with formatting" ) }) +test("event2message: rich reply to an image", async t => { + t.deepEqual( + await eventToMessage({ + "type": "m.room.message", + "sender": "@cadence:cadence.moe", + "content": { + "msgtype": "m.text", + "body": "> <@cadence:cadence.moe> sent an image.\n\nCaught in 8K UHD VR QLED Epic Edition", + "format": "org.matrix.custom.html", + "formatted_body": "
In reply to @cadence:cadence.moe
sent an image.
Caught in 8K UHD VR QLED Epic Edition", + "m.relates_to": { + "m.in_reply_to": { + "event_id": "$Fxy8SMoJuTduwReVkHZ1uHif9EuvNx36Hg79cltiA04" + } + } + }, + "origin_server_ts": 1693037401693, + "unsigned": { + "age": 381, + "transaction_id": "m1693037401592.521" + }, + "event_id": "$v_Gtr-bzv9IVlSLBO5DstzwmiDd-GSFaNfHX66IupV8", + "room_id": "!fGgIymcYWOqjbSRUdV:cadence.moe" + }, data.guild.general, { + api: { + getEvent: mockGetEvent(t, "!fGgIymcYWOqjbSRUdV:cadence.moe", "$Fxy8SMoJuTduwReVkHZ1uHif9EuvNx36Hg79cltiA04", { + type: "m.room.message", + sender: "@_ooye_kyuugryphon:cadence.moe", + content: { + "m.mentions": {}, + msgtype: "m.image", + url: "mxc://cadence.moe/ABfYgGdcIECnraZLGpRnoArG", + external_url: "https://cdn.discordapp.com/attachments/1100319550446252084/1149300251648339998/arcafeappx2.png", + body: "arcafeappx2.png", + filename: "arcafeappx2.png", + info: { + mimetype: "image/png", + w: 512, + h: 512, + size: 43990 + } + } + }) + } + }), + { + messagesToDelete: [], + messagesToEdit: [], + messagesToSend: [{ + username: "cadence [they]", + content: "> <:L1:1144820033948762203><:L2:1144820084079087647>https://discord.com/channels/112760669178241024/687028734322147344/1144865310588014633 <@111604486476181504> 🖼️" + + "\nCaught in 8K UHD VR QLED Epic Edition", + avatar_url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/azCAhThKTojXSZJRoWwZmhvU" + }] + } + ) +}) + test("event2message: with layered rich replies, the preview should only be the real text", async t => { t.deepEqual( await eventToMessage({ From 8b7c47e3698d4707c6ef06eaf050d3607750ae2c Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Fri, 8 Sep 2023 00:13:25 +1200 Subject: [PATCH 3/3] m->d replies: support replying to edits & spoilers --- d2m/converters/message-to-event.test.js | 2 +- m2d/converters/event-to-message.js | 14 +- m2d/converters/event-to-message.test.js | 174 ++++++++++++++++++++---- 3 files changed, 163 insertions(+), 27 deletions(-) diff --git a/d2m/converters/message-to-event.test.js b/d2m/converters/message-to-event.test.js index 5524543..94b67ff 100644 --- a/d2m/converters/message-to-event.test.js +++ b/d2m/converters/message-to-event.test.js @@ -363,7 +363,7 @@ test("message2event: thread start message reference", async t => { api: { getEvent: mockGetEvent(t, "!PnyBKvUBOhjuCucEfk:cadence.moe", "$FchUVylsOfmmbj-VwEs5Z9kY49_dt2zd0vWfylzy5Yo", { "type": "m.room.message", - "sender": "@_ooye_cadence:cadence.moe", + "sender": "@_ooye_kyuugryphon:cadence.moe", "content": { "m.mentions": {}, "msgtype": "m.text", diff --git a/m2d/converters/event-to-message.js b/m2d/converters/event-to-message.js index 91dea5e..75e04f9 100644 --- a/m2d/converters/event-to-message.js +++ b/m2d/converters/event-to-message.js @@ -227,7 +227,7 @@ async function eventToMessage(event, guild, di) { await (async () => { const repliedToEventId = event.content["m.relates_to"]?.["m.in_reply_to"]?.event_id if (!repliedToEventId) return - const repliedToEvent = await di.api.getEvent(event.room_id, repliedToEventId) + let repliedToEvent = await di.api.getEvent(event.room_id, repliedToEventId) if (!repliedToEvent) return const row = db.prepare("SELECT channel_id, message_id FROM event_message INNER JOIN message_channel USING (message_id) WHERE event_id = ? ORDER BY part").get(repliedToEventId) if (row) { @@ -243,6 +243,11 @@ async function eventToMessage(event, guild, di) { } else { replyLine += `Ⓜ️**${senderName}**` } + // If the event has been edited, the homeserver will include the relation in `unsigned`. + if (repliedToEvent.unsigned?.["m.relations"]?.["m.replace"]?.content?.["m.new_content"]) { + repliedToEvent = repliedToEvent.unsigned["m.relations"]["m.replace"] // Note: this changes which event_id is in repliedToEvent. + repliedToEvent.content = repliedToEvent.content["m.new_content"] + } let contentPreview const fileReplyContentAlternative = ( repliedToEvent.content.msgtype === "m.image" ? "🖼️" @@ -254,7 +259,12 @@ async function eventToMessage(event, guild, di) { contentPreview = " " + fileReplyContentAlternative } else { const repliedToContent = repliedToEvent.content.formatted_body || repliedToEvent.content.body - const contentPreviewChunks = chunk(repliedToContent.replace(/.*<\/mx-reply>/, "").replace(/.*?<\/blockquote>/, "").replace(/(?:\n|
)+/g, " ").replace(/<[^>]+>/g, ""), 50) + 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. 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 4d85092..e7d2314 100644 --- a/m2d/converters/event-to-message.test.js +++ b/m2d/converters/event-to-message.test.js @@ -595,6 +595,82 @@ test("event2message: rich reply to a sim user", async t => { ) }) +test("event2message: rich reply to an already-edited message will quote the new message content", async t => { + t.deepEqual( + await eventToMessage({ + "type": "m.room.message", + "sender": "@cadence:cadence.moe", + "content": { + "msgtype": "m.text", + "body": "> <@_ooye_kyuugryphon:cadence.moe> this is the new content. heya!\n\nhiiiii....", + "format": "org.matrix.custom.html", + "formatted_body": "
In reply to @_ooye_kyuugryphon:cadence.moe
this is the new content. heya!
hiiiii....", + "m.relates_to": { + "m.in_reply_to": { + "event_id": "$DSQvWxOBB2DYaei6b83-fb33dQGYt5LJd_s8Nl2a43Q" + } + } + }, + "origin_server_ts": 1693029683016, + "unsigned": { + "age": 91, + "transaction_id": "m1693029682894.510" + }, + "event_id": "$v_Gtr-bzv9IVlSLBO5DstzwmiDd-GSFaNfHX66IupV8", + "room_id": "!fGgIymcYWOqjbSRUdV:cadence.moe" + }, data.guild.general, { + api: { + getEvent: mockGetEvent(t, "!fGgIymcYWOqjbSRUdV:cadence.moe", "$DSQvWxOBB2DYaei6b83-fb33dQGYt5LJd_s8Nl2a43Q", { + type: "m.room.message", + room_id: "!fGgIymcYWOqjbSRUdV:cadence.moe", + sender: "@_ooye_kyuugryphon:cadence.moe", + content: { + "m.mentions": {}, + msgtype: "m.text", + body: "this is the old content. don't use this!" + }, + unsigned: { + "m.relations": { + "m.replace": { + type: "m.room.message", + room_id: "!fGgIymcYWOqjbSRUdV:cadence.moe", + sender: "@_ooye_kyuugryphon:cadence.moe", + content: { + "m.mentions": {}, + msgtype: "m.text", + body: "* this is the new content. heya!", + "m.new_content": { + "m.mentions": {}, + msgtype: "m.text", + body: "this is the new content. heya!" + }, + "m.relates_to": { + rel_type: "m.replace", + event_id: "$DSQvWxOBB2DYaei6b83-fb33dQGYt5LJd_s8Nl2a43Q" + } + }, + event_id: "$JOrl8ycWpo7NIAxZ4u-VJmANVrZFBF41LXyp30y8VvU", + user_id: "@_ooye_kyuugryphon:cadence.moe", + } + } + } + }) + } + }), + { + messagesToDelete: [], + messagesToEdit: [], + messagesToSend: [{ + username: "cadence [they]", + content: "> <:L1:1144820033948762203><:L2:1144820084079087647><@111604486476181504>:" + + "\n> this is the new content. heya!" + + "\nhiiiii....", + avatar_url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/azCAhThKTojXSZJRoWwZmhvU" + }] + } + ) +}) + test("event2message: should avoid using blockquote contents as reply preview in rich reply to a sim user", async t => { t.deepEqual( await eventToMessage({ @@ -642,8 +718,6 @@ test("event2message: should avoid using blockquote contents as reply preview in ) }) - - test("event2message: editing a rich reply to a sim user", async t => { const eventsFetched = [] t.deepEqual( @@ -947,20 +1021,20 @@ test("event2message: rich reply to a matrix user's long message with formatting" "type": "m.room.message", "sender": "@cadence:cadence.moe", "content": { - "msgtype": "m.text", - "body": "> <@cadence:cadence.moe> ```\n> i should have a little happy test\n> ```\n> * list **bold** _em_ ~~strike~~\n> # heading 1\n> ## heading 2\n> ### heading 3\n> https://cadence.moe\n> [legit website](https://cadence.moe)\n\nno you can't!!!", - "format": "org.matrix.custom.html", - "formatted_body": "
In reply to @cadence:cadence.moe
i should have a little happy test\n
\n
    \n
  • list bold em ~~strike~~
  • \n
\n

heading 1

\n

heading 2

\n

heading 3

\n

https://cadence.moe
legit website

\n
no you can't!!!", - "m.relates_to": { - "m.in_reply_to": { - "event_id": "$Fxy8SMoJuTduwReVkHZ1uHif9EuvNx36Hg79cltiA04" - } - } + "msgtype": "m.text", + "body": "> <@cadence:cadence.moe> ```\n> i should have a little happy test\n> ```\n> * list **bold** _em_ ~~strike~~\n> # heading 1\n> ## heading 2\n> ### heading 3\n> https://cadence.moe\n> [legit website](https://cadence.moe)\n\nno you can't!!!", + "format": "org.matrix.custom.html", + "formatted_body": "
In reply to @cadence:cadence.moe
i should have a little happy test\n
\n
    \n
  • list bold em ~~strike~~
  • \n
\n

heading 1

\n

heading 2

\n

heading 3

\n

https://cadence.moe
legit website

\n
no you can't!!!", + "m.relates_to": { + "m.in_reply_to": { + "event_id": "$Fxy8SMoJuTduwReVkHZ1uHif9EuvNx36Hg79cltiA04" + } + } }, "origin_server_ts": 1693037401693, "unsigned": { - "age": 381, - "transaction_id": "m1693037401592.521" + "age": 381, + "transaction_id": "m1693037401592.521" }, "event_id": "$v_Gtr-bzv9IVlSLBO5DstzwmiDd-GSFaNfHX66IupV8", "room_id": "!fGgIymcYWOqjbSRUdV:cadence.moe" @@ -998,20 +1072,20 @@ test("event2message: rich reply to an image", async t => { "type": "m.room.message", "sender": "@cadence:cadence.moe", "content": { - "msgtype": "m.text", - "body": "> <@cadence:cadence.moe> sent an image.\n\nCaught in 8K UHD VR QLED Epic Edition", - "format": "org.matrix.custom.html", - "formatted_body": "
In reply to @cadence:cadence.moe
sent an image.
Caught in 8K UHD VR QLED Epic Edition", - "m.relates_to": { - "m.in_reply_to": { - "event_id": "$Fxy8SMoJuTduwReVkHZ1uHif9EuvNx36Hg79cltiA04" - } - } + "msgtype": "m.text", + "body": "> <@cadence:cadence.moe> sent an image.\n\nCaught in 8K UHD VR QLED Epic Edition", + "format": "org.matrix.custom.html", + "formatted_body": "
In reply to @cadence:cadence.moe
sent an image.
Caught in 8K UHD VR QLED Epic Edition", + "m.relates_to": { + "m.in_reply_to": { + "event_id": "$Fxy8SMoJuTduwReVkHZ1uHif9EuvNx36Hg79cltiA04" + } + } }, "origin_server_ts": 1693037401693, "unsigned": { - "age": 381, - "transaction_id": "m1693037401592.521" + "age": 381, + "transaction_id": "m1693037401592.521" }, "event_id": "$v_Gtr-bzv9IVlSLBO5DstzwmiDd-GSFaNfHX66IupV8", "room_id": "!fGgIymcYWOqjbSRUdV:cadence.moe" @@ -1050,6 +1124,58 @@ test("event2message: rich reply to an image", async t => { ) }) +test("event2message: rich reply to a spoiler should ensure the spoiler is hidden", async t => { + t.deepEqual( + await eventToMessage({ + "type": "m.room.message", + "sender": "@cadence:cadence.moe", + "content": { + "msgtype": "m.text", + "body": "> <@cadence:cadence.moe> ||zoe kills a 5 letter noun at the end. don't tell anybody|| cw crossword spoilers you'll never believe\n\nomg NO WAY!!", + "format": "org.matrix.custom.html", + "formatted_body": "
In reply to @cadence:cadence.moe
zoe kills a 5 letter noun at the end. don't tell anybody cw crossword spoilers you'll never believe
omg NO WAY!!", + "m.relates_to": { + "m.in_reply_to": { + "event_id": "$Fxy8SMoJuTduwReVkHZ1uHif9EuvNx36Hg79cltiA04" + } + } + }, + "origin_server_ts": 1693037401693, + "unsigned": { + "age": 381, + "transaction_id": "m1693037401592.521" + }, + "event_id": "$v_Gtr-bzv9IVlSLBO5DstzwmiDd-GSFaNfHX66IupV8", + "room_id": "!fGgIymcYWOqjbSRUdV:cadence.moe" + }, data.guild.general, { + api: { + getEvent: mockGetEvent(t, "!fGgIymcYWOqjbSRUdV:cadence.moe", "$Fxy8SMoJuTduwReVkHZ1uHif9EuvNx36Hg79cltiA04", { + type: "m.room.message", + sender: "@_ooye_kyuugryphon:cadence.moe", + content: { + "m.mentions": {}, + msgtype: "m.text", + body: "||zoe kills a 5 letter noun at the end. don't tell anybody|| cw crossword spoilers you'll never believe", + format: "org.matrix.custom.html", + formatted_body: `zoe kills a 5 letter noun at the end. don't tell anybody cw crossword spoilers you'll never believe` + } + }) + } + }), + { + messagesToDelete: [], + messagesToEdit: [], + messagesToSend: [{ + username: "cadence [they]", + content: "> <:L1:1144820033948762203><:L2:1144820084079087647>https://discord.com/channels/112760669178241024/687028734322147344/1144865310588014633 <@111604486476181504>:" + + "\n> [spoiler] cw crossword spoilers you'll never..." + + "\nomg NO WAY!!", + avatar_url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/azCAhThKTojXSZJRoWwZmhvU" + }] + } + ) +}) + test("event2message: with layered rich replies, the preview should only be the real text", async t => { t.deepEqual( await eventToMessage({