diff --git a/db/data-for-test.sql b/db/data-for-test.sql index 7148b19..b8da73c 100644 --- a/db/data-for-test.sql +++ b/db/data-for-test.sql @@ -67,7 +67,8 @@ INSERT INTO sim (discord_id, sim_name, localpart, mxid) VALUES ('820865262526005258', 'crunch_god', '_ooye_crunch_god', '@_ooye_crunch_god:cadence.moe'), ('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'); +('114147806469554185', 'extremity', '_ooye_extremity', '@_ooye_extremity:cadence.moe'), +('111604486476181504', 'kyuugryphon', '_ooye_kyuugryphon', '@_ooye_kyuugryphon:cadence.moe');; INSERT INTO sim_member (mxid, room_id, profile_event_content_hash) VALUES ('@_ooye_bojack_horseman:cadence.moe', '!uCtjHhfGlYbVnPVlkG:cadence.moe', NULL); @@ -86,7 +87,8 @@ INSERT INTO event_message (event_id, event_type, event_subtype, message_id, chan ('$FchUVylsOfmmbj-VwEs5Z9kY49_dt2zd0vWfylzy5Yo', 'm.room.message', 'm.text', '1143121514925928541', '1100319550446252084', 0, 1), ('$lnAF9IosAECTnlv9p2e18FG8rHn-JgYKHEHIh5qdFv4', 'm.room.message', 'm.text', '1106366167788044450', '122155380120748034', 0, 1), ('$Ijf1MFCD39ktrNHxrA-i2aKoRWNYdAV2ZXYQeiZIgEU', 'm.room.message', 'm.image', '1106366167788044450', '122155380120748034', 0, 0), -('$f9cjKiacXI9qPF_nUAckzbiKnJEi0LM399kOkhdd8f8', 'm.sticker', NULL, '1106366167788044450', '122155380120748034', 0, 0); +('$f9cjKiacXI9qPF_nUAckzbiKnJEi0LM399kOkhdd8f8', 'm.sticker', NULL, '1106366167788044450', '122155380120748034', 0, 0), +('$Fxy8SMoJuTduwReVkHZ1uHif9EuvNx36Hg79cltiA04', 'm.room.message', 'm.text', '1144865310588014633', '687028734322147344', 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/m2d/actions/send-event.js b/m2d/actions/send-event.js index 39eed22..016768e 100644 --- a/m2d/actions/send-event.js +++ b/m2d/actions/send-event.js @@ -9,6 +9,8 @@ const {sync, discord, db} = passthrough const channelWebhook = sync.require("./channel-webhook") /** @type {import("../converters/event-to-message")} */ const eventToMessage = sync.require("../converters/event-to-message") +/** @type {import("../../matrix/api")}) */ +const api = sync.require("../../matrix/api") /** @param {import("../../types").Event.Outer} event */ async function sendEvent(event) { @@ -20,10 +22,14 @@ async function sendEvent(event) { threadID = channelID channelID = row.thread_parent // it's the thread's parent... get with the times... } + // @ts-ignore + const guildID = discord.channels.get(channelID).guild_id + const guild = discord.guilds.get(guildID) + assert(guild) // no need to sync the matrix member to the other side. but if I did need to, this is where I'd do it - const messages = eventToMessage.eventToMessage(event) + const messages = await eventToMessage.eventToMessage(event, guild, {api}) assert(Array.isArray(messages)) // sanity /** @type {DiscordTypes.APIMessage[]} */ diff --git a/m2d/converters/event-to-message.js b/m2d/converters/event-to-message.js index f4382a3..cf34705 100644 --- a/m2d/converters/event-to-message.js +++ b/m2d/converters/event-to-message.js @@ -26,6 +26,8 @@ const turndownService = new TurndownService({ codeBlockStyle: "fenced" }) +turndownService.remove("mx-reply") + turndownService.addRule("strikethrough", { filter: ["del", "s", "strike"], replacement: function (content) { @@ -69,13 +71,16 @@ turndownService.addRule("fencedCodeBlock", { /** * @param {Ty.Event.Outer} event + * @param {import("discord-api-types/v10").APIGuild} guild + * @param {{api: import("../../matrix/api")}} di simple-as-nails dependency injection for the matrix API */ -function eventToMessage(event) { +async function eventToMessage(event, guild, di) { /** @type {(DiscordTypes.RESTPostAPIWebhookWithTokenJSONBody & {files?: {name: string, file: Buffer}[]})[]} */ let messages = [] let displayName = event.sender let avatarURL = undefined + let replyLine = "" const match = event.sender.match(/^@(.*?):/) if (match) { displayName = match[1] @@ -95,7 +100,33 @@ function eventToMessage(event) { // input = input.replace(/ /g, " ") // There is also a corresponding test to uncomment, named "event2message: whitespace is retained" - // Element adds a bunch of
before but doesn't render them. I can't figure out how this works, so let's just delete those. + // Handling replies. We'll look up the data of the replied-to event from the Matrix homeserver. + 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) + if (!repliedToEvent) return + const row = db.prepare("SELECT channel_id, message_id FROM event_message WHERE event_id = ? ORDER BY part").get(repliedToEventId) + if (row) { + replyLine = `<:L1:1144820033948762203><:L2:1144820084079087647>https://discord.com/channels/${guild.id}/${row.channel_id}/${row.message_id} ` + } else { + replyLine = `<:L1:1144820033948762203><:L2:1144820084079087647>` + } + const sender = repliedToEvent.sender + 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}>: ` + } else { + replyLine += `Ⓜ️**${senderName}**: ` + } + const repliedToContent = repliedToEvent.content.formatted_body || repliedToEvent.content.body + const contentPreviewChunks = chunk(repliedToContent.replace(/.*<\/mx-reply>/, "").replace(/(?:\n|
)+/g, " ").replace(/<[^>]+>/g, ""), 24) + const contentPreview = contentPreviewChunks.length > 1 ? contentPreviewChunks[0] + "..." : contentPreviewChunks[0] + replyLine += contentPreview + "\n" + })() + + // Element adds a bunch of
before but doesn't render them. I can't figure out how this even works in the browser, so let's just delete those. input = input.replace(/(?:\n|
\s*)*<\/blockquote>/g, "") // The matrix spec hasn't decided whether \n counts as a newline or not, but I'm going to count it, because if it's in the data it's there for a reason. @@ -127,6 +158,8 @@ function eventToMessage(event) { content = content.replace(/([*_~`#])/g, `\\$1`) } + content = replyLine + content + // Split into 2000 character chunks const chunks = chunk(content, 2000) messages = messages.concat(chunks.map(content => ({ diff --git a/m2d/converters/event-to-message.test.js b/m2d/converters/event-to-message.test.js index afa40de..33c4d71 100644 --- a/m2d/converters/event-to-message.test.js +++ b/m2d/converters/event-to-message.test.js @@ -1,18 +1,42 @@ -// @ts-check - const {test} = require("supertape") const {eventToMessage} = require("./event-to-message") const data = require("../../test/data") +/** + * @param {string} roomID + * @param {string} eventID + * @returns {(roomID: string, eventID: string) => Promise>} + */ +function mockGetEvent(t, roomID_in, eventID_in, outer) { + return async function(roomID, eventID) { + t.equal(roomID, roomID_in) + t.equal(eventID, eventID_in) + return new Promise(resolve => { + setTimeout(() => { + resolve({ + event_id: eventID_in, + room_id: roomID_in, + origin_server_ts: 1680000000000, + unsigned: { + age: 2245, + transaction_id: "$local.whatever" + }, + ...outer + }) + }) + }) + } +} + function sameFirstContentAndWhitespace(t, a, b) { const a2 = JSON.stringify(a[0].content) const b2 = JSON.stringify(b[0].content) t.equal(a2, b2) } -test("event2message: body is used when there is no formatted_body", t => { +test("event2message: body is used when there is no formatted_body", async t => { t.deepEqual( - eventToMessage({ + await eventToMessage({ content: { body: "testing plaintext", msgtype: "m.text" @@ -34,9 +58,9 @@ test("event2message: body is used when there is no formatted_body", t => { ) }) -test("event2message: any markdown in body is escaped", t => { +test("event2message: any markdown in body is escaped", async t => { t.deepEqual( - eventToMessage({ + await eventToMessage({ content: { body: "testing **special** ~~things~~ which _should_ *not* `trigger` @any ", msgtype: "m.text" @@ -58,9 +82,9 @@ test("event2message: any markdown in body is escaped", t => { ) }) -test("event2message: basic html is converted to markdown", t => { +test("event2message: basic html is converted to markdown", async t => { t.deepEqual( - eventToMessage({ + await eventToMessage({ content: { msgtype: "m.text", body: "wrong body", @@ -84,9 +108,9 @@ test("event2message: basic html is converted to markdown", t => { ) }) -test("event2message: markdown syntax is escaped", t => { +test("event2message: markdown syntax is escaped", async t => { t.deepEqual( - eventToMessage({ + await eventToMessage({ content: { msgtype: "m.text", body: "wrong body", @@ -110,9 +134,9 @@ test("event2message: markdown syntax is escaped", t => { ) }) -test("event2message: html lines are bridged correctly", t => { +test("event2message: html lines are bridged correctly", async t => { t.deepEqual( - eventToMessage({ + await eventToMessage({ content: { msgtype: "m.text", body: "wrong body", @@ -136,9 +160,9 @@ test("event2message: html lines are bridged correctly", t => { ) }) -/*test("event2message: whitespace is retained", t => { +/*test("event2message: whitespace is retained", async t => { t.deepEqual( - eventToMessage({ + await eventToMessage({ content: { msgtype: "m.text", body: "wrong body", @@ -162,10 +186,10 @@ test("event2message: html lines are bridged correctly", t => { ) })*/ -test("event2message: whitespace is collapsed", t => { +test("event2message: whitespace is collapsed", async t => { sameFirstContentAndWhitespace( t, - eventToMessage({ + await eventToMessage({ content: { msgtype: "m.text", body: "wrong body", @@ -189,10 +213,10 @@ test("event2message: whitespace is collapsed", t => { ) }) -test("event2message: lists are bridged correctly", t => { +test("event2message: lists are bridged correctly", async t => { sameFirstContentAndWhitespace( t, - eventToMessage({ + await eventToMessage({ "type": "m.room.message", "sender": "@cadence:cadence.moe", "content": { @@ -217,9 +241,9 @@ test("event2message: lists are bridged correctly", t => { ) }) -test("event2message: long messages are split", t => { +test("event2message: long messages are split", async t => { t.deepEqual( - eventToMessage({ + await eventToMessage({ content: { body: ("a".repeat(130) + " ").repeat(19), msgtype: "m.text" @@ -245,9 +269,9 @@ test("event2message: long messages are split", t => { ) }) -test("event2message: code blocks work", t => { +test("event2message: code blocks work", async t => { t.deepEqual( - eventToMessage({ + await eventToMessage({ content: { msgtype: "m.text", body: "wrong body", @@ -271,9 +295,9 @@ test("event2message: code blocks work", t => { ) }) -test("event2message: code block contents are formatted correctly and not escaped", t => { +test("event2message: code block contents are formatted correctly and not escaped", async t => { t.deepEqual( - eventToMessage({ + await eventToMessage({ "type": "m.room.message", "sender": "@cadence:cadence.moe", "content": { @@ -298,9 +322,9 @@ test("event2message: code block contents are formatted correctly and not escaped ) }) -test("event2message: quotes have an appropriate amount of whitespace", t => { +test("event2message: quotes have an appropriate amount of whitespace", async t => { t.deepEqual( - eventToMessage({ + await eventToMessage({ content: { msgtype: "m.text", body: "wrong body", @@ -324,9 +348,9 @@ test("event2message: quotes have an appropriate amount of whitespace", t => { ) }) -test("event2message: m.emote markdown syntax is escaped", t => { +test("event2message: m.emote markdown syntax is escaped", async t => { t.deepEqual( - eventToMessage({ + await eventToMessage({ content: { msgtype: "m.emote", body: "wrong body", @@ -349,3 +373,136 @@ test("event2message: m.emote markdown syntax is escaped", t => { }] ) }) + +test("event2message: rich reply to a sim user", async t => { + t.deepEqual( + await eventToMessage({ + "type": "m.room.message", + "sender": "@cadence:cadence.moe", + "content": { + "msgtype": "m.text", + "body": "> <@_ooye_kyuugryphon:cadence.moe> Slow news day.\n\nTesting this reply, ignore", + "format": "org.matrix.custom.html", + "formatted_body": "
In reply to @_ooye_kyuugryphon:cadence.moe
Slow news day.
Testing this reply, ignore", + "m.relates_to": { + "m.in_reply_to": { + "event_id": "$Fxy8SMoJuTduwReVkHZ1uHif9EuvNx36Hg79cltiA04" + } + } + }, + "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", "$Fxy8SMoJuTduwReVkHZ1uHif9EuvNx36Hg79cltiA04", { + type: "m.room.message", + content: { + msgtype: "m.text", + body: "Slow news day." + }, + sender: "@_ooye_kyuugryphon:cadence.moe" + }) + } + }), + [{ + username: "cadence", + content: "<:L1:1144820033948762203><:L2:1144820084079087647>https://discord.com/channels/112760669178241024/687028734322147344/1144865310588014633 <@111604486476181504>: Slow news day.\nTesting this reply, ignore", + avatar_url: undefined + }] + ) +}) + +test("event2message: rich reply to a matrix user's long message with formatting", async t => { + t.deepEqual( + await eventToMessage({ + "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" + } + } + }, + "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": "@cadence:cadence.moe", + "content": { + "msgtype": "m.text", + "body": "```\ni should have a little happy test\n```\n* list **bold** _em_ ~~strike~~\n# heading 1\n## heading 2\n### heading 3\nhttps://cadence.moe\n[legit website](https://cadence.moe)", + "format": "org.matrix.custom.html", + "formatted_body": "
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" + } + }) + } + }), + [{ + username: "cadence", + content: "<:L1:1144820033948762203><:L2:1144820084079087647>https://discord.com/channels/112760669178241024/687028734322147344/1144865310588014633 Ⓜ️**cadence**: i should have a little...\n**no you can't!!!**", + avatar_url: undefined + }] + ) +}) + +test("event2message: with layered rich replies, the preview should only be the real text", async t => { + t.deepEqual( + await eventToMessage({ + type: "m.room.message", + sender: "@cadence:cadence.moe", + content: { + msgtype: "m.text", + body: "> <@cadence:cadence.moe> two\n\nthree", + format: "org.matrix.custom.html", + formatted_body: "
In reply to @cadence:cadence.moe
two
three", + "m.relates_to": { + "m.in_reply_to": { + event_id: "$Fxy8SMoJuTduwReVkHZ1uHif9EuvNx36Hg79cltiA04" + } + } + }, + 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": "@cadence:cadence.moe", + "content": { + "msgtype": "m.text", + "body": "> <@cadence:cadence.moe> one\n\ntwo", + "format": "org.matrix.custom.html", + "formatted_body": "
In reply to @cadence:cadence.moe
one
two", + "m.relates_to": { + "m.in_reply_to": { + "event_id": "$5UtboIC30EFlAYD_Oh0pSYVW8JqOp6GsDIJZHtT0Wls" + } + } + } + }) + } + }), + [{ + username: "cadence", + content: "<:L1:1144820033948762203><:L2:1144820084079087647>https://discord.com/channels/112760669178241024/687028734322147344/1144865310588014633 Ⓜ️**cadence**: two\nthree", + avatar_url: undefined + }] + ) +})