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/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/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/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 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'),