From 5a853249a295ecc2e6a0ce9a7f0df87811872bc1 Mon Sep 17 00:00:00 2001 From: Guzio Date: Thu, 19 Feb 2026 16:21:27 +0000 Subject: [PATCH 01/43] I prefer 4 spaces --- .vscode/settings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 9f1e183..ea571e8 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,5 @@ { "editor.insertSpaces": false, "editor.detectIndentation": false, - "editor.tabSize": 3 + "editor.tabSize": 4 } From 8676a736204b8cdf8831aad4ca32f7ebf2f7a8be Mon Sep 17 00:00:00 2001 From: Guzio Date: Thu, 19 Feb 2026 17:53:56 +0000 Subject: [PATCH 02/43] Testing BEGINS! --- src/m2d/event-dispatcher.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/m2d/event-dispatcher.js b/src/m2d/event-dispatcher.js index 2091f7d..4385f71 100644 --- a/src/m2d/event-dispatcher.js +++ b/src/m2d/event-dispatcher.js @@ -211,6 +211,9 @@ async event => { if (utils.eventSenderIsFromDiscord(event.sender)) return const messageResponses = await sendEvent.sendEvent(event) if (!messageResponses.length) return + if (event.content["m.relates_to"]?.rel_type === "m.thread"){ + console.log("thread event spotted") + } if (event.type === "m.room.message" && event.content.msgtype === "m.text") { // @ts-ignore await matrixCommandHandler.execute(event) From dca53752bb74da6df4b597c9ecf9c96a74fc1074 Mon Sep 17 00:00:00 2001 From: Guzio Date: Thu, 19 Feb 2026 18:34:30 +0000 Subject: [PATCH 03/43] Reverse-engineering the docs --- src/d2m/actions/send-message.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/d2m/actions/send-message.js b/src/d2m/actions/send-message.js index eb919bb..8b86d34 100644 --- a/src/d2m/actions/send-message.js +++ b/src/d2m/actions/send-message.js @@ -86,6 +86,7 @@ async function sendMessage(message, channel, guild, row) { const useTimestamp = message["backfill"] ? new Date(message.timestamp).getTime() : undefined const eventID = await api.sendEvent(roomID, eventType, eventWithoutType, senderMxid, useTimestamp) + console.log(eventWithoutType) eventIDs.push(eventID) try { From 01b82e7b682e50d7316ad4cc6210d858907ac985 Mon Sep 17 00:00:00 2001 From: Guzio Date: Thu, 19 Feb 2026 18:48:24 +0000 Subject: [PATCH 04/43] I think I got SOMETHING up and running! --- src/d2m/actions/send-message.js | 1 - src/m2d/event-dispatcher.js | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/d2m/actions/send-message.js b/src/d2m/actions/send-message.js index 8b86d34..eb919bb 100644 --- a/src/d2m/actions/send-message.js +++ b/src/d2m/actions/send-message.js @@ -86,7 +86,6 @@ async function sendMessage(message, channel, guild, row) { const useTimestamp = message["backfill"] ? new Date(message.timestamp).getTime() : undefined const eventID = await api.sendEvent(roomID, eventType, eventWithoutType, senderMxid, useTimestamp) - console.log(eventWithoutType) eventIDs.push(eventID) try { diff --git a/src/m2d/event-dispatcher.js b/src/m2d/event-dispatcher.js index 4385f71..ef93e3c 100644 --- a/src/m2d/event-dispatcher.js +++ b/src/m2d/event-dispatcher.js @@ -212,7 +212,7 @@ async event => { const messageResponses = await sendEvent.sendEvent(event) if (!messageResponses.length) return if (event.content["m.relates_to"]?.rel_type === "m.thread"){ - console.log("thread event spotted") + api.sendEvent(event.room_id, "m.room.message", {body:"Thread spotted!"}) } if (event.type === "m.room.message" && event.content.msgtype === "m.text") { // @ts-ignore From 486959be0b489d44bb8fae0a3cd1c813f00bc7b0 Mon Sep 17 00:00:00 2001 From: Guzio Date: Thu, 19 Feb 2026 19:05:25 +0000 Subject: [PATCH 05/43] forgor --- src/m2d/event-dispatcher.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/m2d/event-dispatcher.js b/src/m2d/event-dispatcher.js index ef93e3c..b89261e 100644 --- a/src/m2d/event-dispatcher.js +++ b/src/m2d/event-dispatcher.js @@ -212,7 +212,7 @@ async event => { const messageResponses = await sendEvent.sendEvent(event) if (!messageResponses.length) return if (event.content["m.relates_to"]?.rel_type === "m.thread"){ - api.sendEvent(event.room_id, "m.room.message", {body:"Thread spotted!"}) + api.sendEvent(event.room_id, "m.room.message", {body:"Thread spotted!", msgtype:"m.text"}) } if (event.type === "m.room.message" && event.content.msgtype === "m.text") { // @ts-ignore From aaf8dea10484e8ed1097cafefeaf4b19fdb15ce0 Mon Sep 17 00:00:00 2001 From: Guzio Date: Thu, 19 Feb 2026 19:27:13 +0000 Subject: [PATCH 06/43] Reply-related metadata --- src/m2d/event-dispatcher.js | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/m2d/event-dispatcher.js b/src/m2d/event-dispatcher.js index b89261e..86421ff 100644 --- a/src/m2d/event-dispatcher.js +++ b/src/m2d/event-dispatcher.js @@ -212,7 +212,19 @@ async event => { const messageResponses = await sendEvent.sendEvent(event) if (!messageResponses.length) return if (event.content["m.relates_to"]?.rel_type === "m.thread"){ - api.sendEvent(event.room_id, "m.room.message", {body:"Thread spotted!", msgtype:"m.text"}) + api.sendEvent(event.room_id, "m.room.message", { + "body": "It seems like you sent this message inside a thread.", + "format": "org.matrix.custom.html", + "formatted_body": "It seems like you sent this message inside a thread.", + "m.mentions": { "user_ids": [event.sender]}, + "m.relates_to": { + "event_id": event.content["m.relates_to"].event_id, + "is_falling_back": false, + "m.in_reply_to": { "event_id": event.event_id }, + "rel_type": "m.thread" + }, + "msgtype": "m.text" + }) } if (event.type === "m.room.message" && event.content.msgtype === "m.text") { // @ts-ignore From abe42aaa925a7a1afca0d419875fa353d550064c Mon Sep 17 00:00:00 2001 From: Guzio Date: Thu, 19 Feb 2026 21:26:15 +0000 Subject: [PATCH 07/43] Updated the message to its final form. At least final-until-we-make-it-so-that-new-rooms-are-autogenerated-when-a-thread-is-opened. Then we'd need to include the link to it instead of a command-help. --- src/m2d/event-dispatcher.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/m2d/event-dispatcher.js b/src/m2d/event-dispatcher.js index 86421ff..ded162f 100644 --- a/src/m2d/event-dispatcher.js +++ b/src/m2d/event-dispatcher.js @@ -213,9 +213,9 @@ async event => { if (!messageResponses.length) return if (event.content["m.relates_to"]?.rel_type === "m.thread"){ api.sendEvent(event.room_id, "m.room.message", { - "body": "It seems like you sent this message inside a thread.", + "body": "⚠️ **This message may not have bridged to Discord in the way you thought it would!**\n\nIt seems like you sent this message inside a Matrix thread. Matrix threads don't work like Discord threads - they are effectively just „fancy replies”, not independent rooms/channels (any „thread-like appearance” is handled purely client-side - and even then, most Matrix clients don't handle it particularly well), and as such, they are bridged as replies to Discord. _Discord users will not be aware that you sent this message inside a thread - it will go directly onto the main channel. If the thread you sent this message in is old, such a random reply may be distracting to Discord users!_\n\nFor the sake of Discord parity (as well as better support in numerous Matrix clients - as stated above, most Matrix clients don't handle threads particularly well, and they just render in-thread messages as fancy replies), it is recommended to send threaded messages inside a separate Matrix room that gets bridged to Discord. If you sent this message in a pre-existing Matrix thread, please look around to see if anyone has created such a room for it. If this is the first message (ie. the thread was just created) or noone's made a thread-room for this thread, it is recommended you create one with `/thread ` (on Matrix) and continue the conversation there.\n\n*You can read more about the rationale behind this design choice [here](https://gitdab.com/cadence/out-of-your-element/src/branch/main/docs/threads-as-rooms.md).*", "format": "org.matrix.custom.html", - "formatted_body": "It seems like you sent this message inside a thread.", + "formatted_body": "⚠️ This message may not have bridged to Discord in the way you thought it would!

It seems like you sent this message inside a Matrix thread. Matrix threads don't work like Discord threads - they are effectively just „fancy replies”, not independent rooms/channels (any „thread-like appearance” is handled purely client-side - and even then, most Matrix clients don't handle it particularly well), and as such, they are bridged as replies to Discord. Discord users will not be aware that you sent this message inside a thread - it will go directly onto the main channel. If the thread you sent this message in is old, such a random reply may be distracting to Discord users!

For the sake of Discord parity (as well as better support in numerous Matrix clients - as stated above, most Matrix clients don't handle threads particularly well, and they just render in-thread messages as fancy replies), it is recommended to send threaded messages inside a separate Matrix room that gets bridged to Discord. If you sent this message in a pre-existing Matrix thread, please look around to see if anyone has created such a room for it. If this is the first message (ie. the thread was just created) or noone's made a thread-room for this thread, it is recommended you create one with /thread <Thread Name> (on Matrix) and continue the conversation there.

You can read more about the rationale behind this design choice here.", "m.mentions": { "user_ids": [event.sender]}, "m.relates_to": { "event_id": event.content["m.relates_to"].event_id, From 7afcbfaa06c7e7ada0cdb2fde0b8780354a353e2 Mon Sep 17 00:00:00 2001 From: Guzio Date: Thu, 19 Feb 2026 22:44:20 +0000 Subject: [PATCH 08/43] I have no idea if this works; just throwing random ideas together. It's 23:42; I'm going just purely based on vibes at this point. vibecoding but no AI, just eepy --- src/d2m/converters/thread-to-announcement.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/d2m/converters/thread-to-announcement.js b/src/d2m/converters/thread-to-announcement.js index 575b3c5..601e61a 100644 --- a/src/d2m/converters/thread-to-announcement.js +++ b/src/d2m/converters/thread-to-announcement.js @@ -19,19 +19,21 @@ const userRegex = reg.namespaces.users.map(u => new RegExp(u.regex)) */ async function threadToAnnouncement(parentRoomID, threadRoomID, creatorMxid, thread, di) { const branchedFromEventID = select("event_message", "event_id", {message_id: thread.id}).pluck().get() - /** @type {{"m.mentions"?: any, "m.in_reply_to"?: any}} */ + /** @type {{"m.mentions"?: any, "m.relates_to"?: {event_id?: string, is_falling_back?: boolean, "m.in_reply_to"?: {event_id: string}, rel_type?: "m.replace"|"m.thread"}}} */ const context = {} + let suffix = ""; if (branchedFromEventID) { // Need to figure out who sent that event... const event = await di.api.getEvent(parentRoomID, branchedFromEventID) - context["m.relates_to"] = {"m.in_reply_to": {event_id: event.event_id}} + suffix = "\n\n*[Note: You should talk there, rather than in this newly-created Matrix thread. Any messages sent here will be bridged as replies, which is probably not what you want.]*"; + context["m.relates_to"] = {"m.in_reply_to": {event_id: event.event_id}, is_falling_back:false, event_id: event.event_id, rel_type: "m.thread"} if (event.sender && !userRegex.some(rx => event.sender.match(rx))) context["m.mentions"] = {user_ids: [event.sender]} } const msgtype = creatorMxid ? "m.emote" : "m.text" const template = creatorMxid ? "started a thread:" : "Thread started:" const via = await mxUtils.getViaServersQuery(threadRoomID, di.api) - let body = `${template} ${thread.name} https://matrix.to/#/${threadRoomID}?${via.toString()}` + let body = `${template} ${thread.name} https://matrix.to/#/${threadRoomID}?${via.toString()}${suffix}` return { msgtype, From ac421e6c743c93a00d0a304f9a8135cb7d27102c Mon Sep 17 00:00:00 2001 From: Guzio Date: Fri, 20 Feb 2026 00:00:42 +0000 Subject: [PATCH 09/43] this looks better --- src/d2m/converters/thread-to-announcement.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/d2m/converters/thread-to-announcement.js b/src/d2m/converters/thread-to-announcement.js index 601e61a..ca20af6 100644 --- a/src/d2m/converters/thread-to-announcement.js +++ b/src/d2m/converters/thread-to-announcement.js @@ -25,7 +25,7 @@ async function threadToAnnouncement(parentRoomID, threadRoomID, creatorMxid, thr if (branchedFromEventID) { // Need to figure out who sent that event... const event = await di.api.getEvent(parentRoomID, branchedFromEventID) - suffix = "\n\n*[Note: You should talk there, rather than in this newly-created Matrix thread. Any messages sent here will be bridged as replies, which is probably not what you want.]*"; + suffix = "\n\n[Note: You should talk there, rather than in this newly-created Matrix thread. Any messages sent here will be bridged as replies, which is probably not what you want.]"; context["m.relates_to"] = {"m.in_reply_to": {event_id: event.event_id}, is_falling_back:false, event_id: event.event_id, rel_type: "m.thread"} if (event.sender && !userRegex.some(rx => event.sender.match(rx))) context["m.mentions"] = {user_ids: [event.sender]} } From b542a81ee14f894090e6632ce4dddc4f78b80253 Mon Sep 17 00:00:00 2001 From: Guzio Date: Fri, 20 Feb 2026 02:21:52 +0000 Subject: [PATCH 10/43] first time actually interacting with the DB --- src/matrix/matrix-command-handler.js | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/matrix/matrix-command-handler.js b/src/matrix/matrix-command-handler.js index e382a32..863996c 100644 --- a/src/matrix/matrix-command-handler.js +++ b/src/matrix/matrix-command-handler.js @@ -255,11 +255,20 @@ const commands = [{ 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." + 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." // NOTE: Currently, this assumes that all Matrix users have no Discord roles, and makes that „If you were a Discord user...” claim based on that assumption. If Discord permission emulation is gonna happen in the future, this is certainly one among many places that will need to be changed. }) } + + const relation = event.content["m.relates_to"] + let attachedToEvent = relation?.["m.in_reply_to"]?.event_id // By default, attempt to attach the thread to the message to which /thread was replying. + if (relation?.rel_type === "m.thread" && relation.is_falling_back) attachedToEvent = event.content["m.relates_to"]?.event_id // If /thread was sent inside a Matrix thread, attempt to attach the Discord thread to the message around which that Matrix thread was based on. But only if we're falling back, ie. the message sent in that thread was a „normal” one, not a reply to some other message inside the thread. If it WAS reply, we preserve the original behavior (attach the thread to the message to which /thread was replying). One slight caveat is that is_falling_back will also be false if it's the 1st message of that thread. In such case, however, m.in_reply_to should (if sent from a spec-compliant client) point towards the message around which that Matrix thread was based on, so the intended behavior is preserved. + if (!attachedToEvent) attachedToEvent = event.event_id // If /thread wasn't replying to anything (ie. attachedToEvent was undefined at initial assignment), or if the event was somehow malformed (in such a way that that - one way or another - attachedToEvent ended up being undefined, even if according to the spec it shouldn't), attach the thread to the /thread command-message that created it. + + assert(attachedToEvent) + const attachedToMessage = select("event_message", "message_id", {event_id: attachedToEvent}).pluck().get() - await discord.snow.channel.createThreadWithoutMessage(channelID, {type: 11, name: words.slice(1).join(" ")}) + if (attachedToMessage) await discord.snow.channel.createThreadWithMessage(channelID, attachedToMessage, {name: words.slice(1).join(" ")}) + else await discord.snow.channel.createThreadWithoutMessage(channelID, {type: 11, name: words.slice(1).join(" ")}) } ) }] From f734b0619f614133de4ca4054b8b497d0dd361ba Mon Sep 17 00:00:00 2001 From: Guzio Date: Fri, 20 Feb 2026 03:10:09 +0000 Subject: [PATCH 11/43] Don't warn the user that they should use /thread if they literally just did it. --- src/m2d/event-dispatcher.js | 16 +++++++++++----- src/matrix/matrix-command-handler.js | 10 +++++++--- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/src/m2d/event-dispatcher.js b/src/m2d/event-dispatcher.js index ded162f..914845c 100644 --- a/src/m2d/event-dispatcher.js +++ b/src/m2d/event-dispatcher.js @@ -211,7 +211,16 @@ async event => { if (utils.eventSenderIsFromDiscord(event.sender)) return const messageResponses = await sendEvent.sendEvent(event) if (!messageResponses.length) return - if (event.content["m.relates_to"]?.rel_type === "m.thread"){ + + /** @type {string|undefined} */ + let executedCommand + if (event.type === "m.room.message" && event.content.msgtype === "m.text") { + executedCommand = await matrixCommandHandler.parseAndExecute( + // @ts-ignore - TypeScript doesn't know that the event.content.msgtype === "m.text" ensures that event isn't of type Ty.Event.Outer_M_Room_Message_File (which, indeed, wouldn't fit here) + event + ) + } + if (event.content["m.relates_to"]?.rel_type === "m.thread" && executedCommand !== "thread"){ api.sendEvent(event.room_id, "m.room.message", { "body": "⚠️ **This message may not have bridged to Discord in the way you thought it would!**\n\nIt seems like you sent this message inside a Matrix thread. Matrix threads don't work like Discord threads - they are effectively just „fancy replies”, not independent rooms/channels (any „thread-like appearance” is handled purely client-side - and even then, most Matrix clients don't handle it particularly well), and as such, they are bridged as replies to Discord. _Discord users will not be aware that you sent this message inside a thread - it will go directly onto the main channel. If the thread you sent this message in is old, such a random reply may be distracting to Discord users!_\n\nFor the sake of Discord parity (as well as better support in numerous Matrix clients - as stated above, most Matrix clients don't handle threads particularly well, and they just render in-thread messages as fancy replies), it is recommended to send threaded messages inside a separate Matrix room that gets bridged to Discord. If you sent this message in a pre-existing Matrix thread, please look around to see if anyone has created such a room for it. If this is the first message (ie. the thread was just created) or noone's made a thread-room for this thread, it is recommended you create one with `/thread ` (on Matrix) and continue the conversation there.\n\n*You can read more about the rationale behind this design choice [here](https://gitdab.com/cadence/out-of-your-element/src/branch/main/docs/threads-as-rooms.md).*", "format": "org.matrix.custom.html", @@ -226,10 +235,7 @@ async event => { "msgtype": "m.text" }) } - if (event.type === "m.room.message" && event.content.msgtype === "m.text") { - // @ts-ignore - await matrixCommandHandler.execute(event) - } + retrigger.messageFinishedBridging(event.event_id) await api.ackEvent(event) })) diff --git a/src/matrix/matrix-command-handler.js b/src/matrix/matrix-command-handler.js index 863996c..2d6deca 100644 --- a/src/matrix/matrix-command-handler.js +++ b/src/matrix/matrix-command-handler.js @@ -274,8 +274,11 @@ const commands = [{ }] -/** @type {CommandExecute} */ -async function execute(event) { +/** + * @param {Ty.Event.Outer_M_Room_Message} event + * @returns {Promise} the executed command's name or undefined if no command execution was performed +*/ +async function parseAndExecute(event) { let realBody = event.content.body while (realBody.startsWith("> ")) { const i = realBody.indexOf("\n") @@ -296,7 +299,8 @@ async function execute(event) { if (!command) return await command.execute(event, realBody, words) + return words[0] } -module.exports.execute = execute +module.exports.parseAndExecute = parseAndExecute module.exports.onReactionAdd = onReactionAdd From e44f1041b65de8628a5e36e5477e6237358c09b0 Mon Sep 17 00:00:00 2001 From: Guzio Date: Fri, 20 Feb 2026 03:50:06 +0000 Subject: [PATCH 12/43] Turns out that creating a thread-in-thread (which is what the stuff I was doing in matrix-command-handler effectively amounted to) KINDA breaks Element. Whoops! MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Also, that message in thread-to-announcement was misleading, as now there's no guarantee that a thread was newly created (it could be very old, but freshly /thread-ed). So I changed that, too. Also, while updating messages, I decided to slightly alter the „may not have been bridged to Discord in the way you thought it was gonna be”-warning in event-dispatcher. --- src/d2m/converters/thread-to-announcement.js | 6 +++--- src/m2d/event-dispatcher.js | 4 ++-- src/matrix/matrix-command-handler.js | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/d2m/converters/thread-to-announcement.js b/src/d2m/converters/thread-to-announcement.js index ca20af6..d35bb80 100644 --- a/src/d2m/converters/thread-to-announcement.js +++ b/src/d2m/converters/thread-to-announcement.js @@ -25,15 +25,15 @@ async function threadToAnnouncement(parentRoomID, threadRoomID, creatorMxid, thr if (branchedFromEventID) { // Need to figure out who sent that event... const event = await di.api.getEvent(parentRoomID, branchedFromEventID) - suffix = "\n\n[Note: You should talk there, rather than in this newly-created Matrix thread. Any messages sent here will be bridged as replies, which is probably not what you want.]"; + suffix = "\n[Note: You should continue the conversation in that room, rather than in this thread. Any messages sent in Matrix threads will be bridged to Discord as replies, not in-thread messages, which is probably not what you want.]"; context["m.relates_to"] = {"m.in_reply_to": {event_id: event.event_id}, is_falling_back:false, event_id: event.event_id, rel_type: "m.thread"} if (event.sender && !userRegex.some(rx => event.sender.match(rx))) context["m.mentions"] = {user_ids: [event.sender]} } const msgtype = creatorMxid ? "m.emote" : "m.text" - const template = creatorMxid ? "started a thread:" : "Thread started:" + const template = creatorMxid ? "started a thread" : "New thread started: " const via = await mxUtils.getViaServersQuery(threadRoomID, di.api) - let body = `${template} ${thread.name} https://matrix.to/#/${threadRoomID}?${via.toString()}${suffix}` + let body = `${template} „${thread.name}” in room: https://matrix.to/#/${threadRoomID}?${via.toString()}${suffix}` return { msgtype, diff --git a/src/m2d/event-dispatcher.js b/src/m2d/event-dispatcher.js index 914845c..7342f0a 100644 --- a/src/m2d/event-dispatcher.js +++ b/src/m2d/event-dispatcher.js @@ -222,9 +222,9 @@ async event => { } if (event.content["m.relates_to"]?.rel_type === "m.thread" && executedCommand !== "thread"){ api.sendEvent(event.room_id, "m.room.message", { - "body": "⚠️ **This message may not have bridged to Discord in the way you thought it would!**\n\nIt seems like you sent this message inside a Matrix thread. Matrix threads don't work like Discord threads - they are effectively just „fancy replies”, not independent rooms/channels (any „thread-like appearance” is handled purely client-side - and even then, most Matrix clients don't handle it particularly well), and as such, they are bridged as replies to Discord. _Discord users will not be aware that you sent this message inside a thread - it will go directly onto the main channel. If the thread you sent this message in is old, such a random reply may be distracting to Discord users!_\n\nFor the sake of Discord parity (as well as better support in numerous Matrix clients - as stated above, most Matrix clients don't handle threads particularly well, and they just render in-thread messages as fancy replies), it is recommended to send threaded messages inside a separate Matrix room that gets bridged to Discord. If you sent this message in a pre-existing Matrix thread, please look around to see if anyone has created such a room for it. If this is the first message (ie. the thread was just created) or noone's made a thread-room for this thread, it is recommended you create one with `/thread ` (on Matrix) and continue the conversation there.\n\n*You can read more about the rationale behind this design choice [here](https://gitdab.com/cadence/out-of-your-element/src/branch/main/docs/threads-as-rooms.md).*", + "body": "⚠️ **This message may not have been bridged to Discord in the way you thought it was gonna be!**\n\nIt seems like you sent this message inside a Matrix thread. Matrix threads don't work like Discord threads - they are effectively just „fancy replies”, not independent rooms/channels (any „thread-like appearance” is handled purely client-side - and even then, most Matrix clients don't handle it particularly well), and as such, they are bridged as replies to Discord. *Discord users will not be aware that you sent this message inside a thread - it will go directly onto the main channel. If the thread you sent this message in is old, such a random reply may be distracting to Discord users!*\n\nFor the sake of Discord parity (and for better support in numerous Matrix clients - as stated above, most Matrix clients don't handle threads particularly well, and they just render in-thread messages as fancy replies), it is recommended to send threaded messages inside a separate Matrix room that gets bridged to Discord. If you sent this message in a per-existing Matrix thread, please look around to see if anyone has created such a room for it. If this is the first message (ie. the thread was just created) or nobody has made a thread-room for this thread yet, it is recommended you create one with `/thread ` (on Matrix) and continue the conversation there. You can even run this command directly in this very Matrix thread, and the new Discord thread will be automatically attached to the same message as this one (alternatively, run that command outside a thread and as a reply to a different message to attach the thread to that message, or stand-alone to create a stand-alone thread).\n\n*You can read more about the rationale behind this design choice [here](https://gitdab.com/cadence/out-of-your-element/src/branch/main/docs/threads-as-rooms.md).*", "format": "org.matrix.custom.html", - "formatted_body": "⚠️ This message may not have bridged to Discord in the way you thought it would!

It seems like you sent this message inside a Matrix thread. Matrix threads don't work like Discord threads - they are effectively just „fancy replies”, not independent rooms/channels (any „thread-like appearance” is handled purely client-side - and even then, most Matrix clients don't handle it particularly well), and as such, they are bridged as replies to Discord. Discord users will not be aware that you sent this message inside a thread - it will go directly onto the main channel. If the thread you sent this message in is old, such a random reply may be distracting to Discord users!

For the sake of Discord parity (as well as better support in numerous Matrix clients - as stated above, most Matrix clients don't handle threads particularly well, and they just render in-thread messages as fancy replies), it is recommended to send threaded messages inside a separate Matrix room that gets bridged to Discord. If you sent this message in a pre-existing Matrix thread, please look around to see if anyone has created such a room for it. If this is the first message (ie. the thread was just created) or noone's made a thread-room for this thread, it is recommended you create one with /thread <Thread Name> (on Matrix) and continue the conversation there.

You can read more about the rationale behind this design choice here.", + "formatted_body": "⚠️ This message may not have been bridged to Discord in the way you thought it was gonna be!

It seems like you sent this message inside a Matrix thread. Matrix threads don't work like Discord threads - they are effectively just „fancy replies”, not independent rooms/channels (any „thread-like appearance” is handled purely client-side - and even then, most Matrix clients don't handle it particularly well), and as such, they are bridged as replies to Discord. Discord users will not be aware that you sent this message inside a thread - it will go directly onto the main channel. If the thread you sent this message in is old, such a random reply may be distracting to Discord users!

For the sake of Discord parity (and for better support in numerous Matrix clients - as stated above, most Matrix clients don't handle threads particularly well, and they just render in-thread messages as fancy replies), it is recommended to send threaded messages inside a separate Matrix room that gets bridged to Discord. If you sent this message in a per-existing Matrix thread, please look around to see if anyone has created such a room for it. If this is the first message (ie. the thread was just created) or nobody has made a thread-room for this thread yet, it is recommended you create one with /thread <Thread Name> (on Matrix) and continue the conversation there. You can even run this command directly in this very Matrix thread, and the new Discord thread will be automatically attached to the same message as this one (alternatively, run that command outside a thread and as a reply to a different message to attach the thread to that message, or stand-alone to create a stand-alone thread).

You can read more about the rationale behind this design choice here.", "m.mentions": { "user_ids": [event.sender]}, "m.relates_to": { "event_id": event.content["m.relates_to"].event_id, diff --git a/src/matrix/matrix-command-handler.js b/src/matrix/matrix-command-handler.js index 2d6deca..6f3c5f9 100644 --- a/src/matrix/matrix-command-handler.js +++ b/src/matrix/matrix-command-handler.js @@ -261,7 +261,7 @@ const commands = [{ const relation = event.content["m.relates_to"] let attachedToEvent = relation?.["m.in_reply_to"]?.event_id // By default, attempt to attach the thread to the message to which /thread was replying. - if (relation?.rel_type === "m.thread" && relation.is_falling_back) attachedToEvent = event.content["m.relates_to"]?.event_id // If /thread was sent inside a Matrix thread, attempt to attach the Discord thread to the message around which that Matrix thread was based on. But only if we're falling back, ie. the message sent in that thread was a „normal” one, not a reply to some other message inside the thread. If it WAS reply, we preserve the original behavior (attach the thread to the message to which /thread was replying). One slight caveat is that is_falling_back will also be false if it's the 1st message of that thread. In such case, however, m.in_reply_to should (if sent from a spec-compliant client) point towards the message around which that Matrix thread was based on, so the intended behavior is preserved. + if (relation?.rel_type === "m.thread") attachedToEvent = event.content["m.relates_to"]?.event_id // If /thread was sent inside a Matrix thread, attempt to attach the Discord thread to the message around which that Matrix thread was based on. if (!attachedToEvent) attachedToEvent = event.event_id // If /thread wasn't replying to anything (ie. attachedToEvent was undefined at initial assignment), or if the event was somehow malformed (in such a way that that - one way or another - attachedToEvent ended up being undefined, even if according to the spec it shouldn't), attach the thread to the /thread command-message that created it. assert(attachedToEvent) From f9e303f018ffb5474dc400ceb9d2de123e20571d Mon Sep 17 00:00:00 2001 From: Guzio Date: Fri, 20 Feb 2026 03:52:03 +0000 Subject: [PATCH 13/43] stray whitespace --- src/d2m/converters/thread-to-announcement.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/d2m/converters/thread-to-announcement.js b/src/d2m/converters/thread-to-announcement.js index d35bb80..179a559 100644 --- a/src/d2m/converters/thread-to-announcement.js +++ b/src/d2m/converters/thread-to-announcement.js @@ -31,7 +31,7 @@ async function threadToAnnouncement(parentRoomID, threadRoomID, creatorMxid, thr } const msgtype = creatorMxid ? "m.emote" : "m.text" - const template = creatorMxid ? "started a thread" : "New thread started: " + const template = creatorMxid ? "started a thread" : "New thread started:" const via = await mxUtils.getViaServersQuery(threadRoomID, di.api) let body = `${template} „${thread.name}” in room: https://matrix.to/#/${threadRoomID}?${via.toString()}${suffix}` From d7aadc30793403be6f487b6ec5697a202877d80d Mon Sep 17 00:00:00 2001 From: Guzio Date: Fri, 20 Feb 2026 04:02:01 +0000 Subject: [PATCH 14/43] AAAAAAAAAAAAAAAAAAAA --- src/m2d/event-dispatcher.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/m2d/event-dispatcher.js b/src/m2d/event-dispatcher.js index 7342f0a..ec5fa89 100644 --- a/src/m2d/event-dispatcher.js +++ b/src/m2d/event-dispatcher.js @@ -222,9 +222,9 @@ async event => { } if (event.content["m.relates_to"]?.rel_type === "m.thread" && executedCommand !== "thread"){ api.sendEvent(event.room_id, "m.room.message", { - "body": "⚠️ **This message may not have been bridged to Discord in the way you thought it was gonna be!**\n\nIt seems like you sent this message inside a Matrix thread. Matrix threads don't work like Discord threads - they are effectively just „fancy replies”, not independent rooms/channels (any „thread-like appearance” is handled purely client-side - and even then, most Matrix clients don't handle it particularly well), and as such, they are bridged as replies to Discord. *Discord users will not be aware that you sent this message inside a thread - it will go directly onto the main channel. If the thread you sent this message in is old, such a random reply may be distracting to Discord users!*\n\nFor the sake of Discord parity (and for better support in numerous Matrix clients - as stated above, most Matrix clients don't handle threads particularly well, and they just render in-thread messages as fancy replies), it is recommended to send threaded messages inside a separate Matrix room that gets bridged to Discord. If you sent this message in a per-existing Matrix thread, please look around to see if anyone has created such a room for it. If this is the first message (ie. the thread was just created) or nobody has made a thread-room for this thread yet, it is recommended you create one with `/thread ` (on Matrix) and continue the conversation there. You can even run this command directly in this very Matrix thread, and the new Discord thread will be automatically attached to the same message as this one (alternatively, run that command outside a thread and as a reply to a different message to attach the thread to that message, or stand-alone to create a stand-alone thread).\n\n*You can read more about the rationale behind this design choice [here](https://gitdab.com/cadence/out-of-your-element/src/branch/main/docs/threads-as-rooms.md).*", + "body": "⚠️ **This message may not have been bridged to Discord in the way you thought it was gonna be!**\n\nIt seems like you sent this message inside a Matrix thread. Matrix threads don't work like Discord threads - they are effectively just „fancy replies”, not independent rooms/channels (any „thread-like appearance” is handled purely client-side - and even then, most Matrix clients don't handle it particularly well), and as such, they are bridged as replies to Discord. *Discord users will not be aware that you sent this message inside a thread - it will go directly onto the main channel. If the thread you sent this message in is old, such a random reply may be distracting to Discord users!*\n\nFor the sake of Discord parity (and for better support in numerous Matrix clients - as stated above, most Matrix clients don't handle threads particularly well, and they just render in-thread messages as fancy replies), it is recommended to send threaded messages inside a separate Matrix room that gets bridged to Discord. If you sent this message in a pre-existing Matrix thread, please look around to see if anyone has created such a room for it. If this is the first message (ie. the thread was just created) or nobody has made a thread-room for this thread yet, it is recommended you create one with `/thread ` and continue the conversation there. You can even run this command directly in this very Matrix thread, and the new Discord thread will be automatically attached to the same message as this one (alternatively, run that command outside a thread, and as a reply to a different message to attach the thread to that message or stand-alone to create a stand-alone thread).\n\n*You can read more about the rationale behind this design choice [here](https://gitdab.com/cadence/out-of-your-element/src/branch/main/docs/threads-as-rooms.md).*", "format": "org.matrix.custom.html", - "formatted_body": "⚠️ This message may not have been bridged to Discord in the way you thought it was gonna be!

It seems like you sent this message inside a Matrix thread. Matrix threads don't work like Discord threads - they are effectively just „fancy replies”, not independent rooms/channels (any „thread-like appearance” is handled purely client-side - and even then, most Matrix clients don't handle it particularly well), and as such, they are bridged as replies to Discord. Discord users will not be aware that you sent this message inside a thread - it will go directly onto the main channel. If the thread you sent this message in is old, such a random reply may be distracting to Discord users!

For the sake of Discord parity (and for better support in numerous Matrix clients - as stated above, most Matrix clients don't handle threads particularly well, and they just render in-thread messages as fancy replies), it is recommended to send threaded messages inside a separate Matrix room that gets bridged to Discord. If you sent this message in a per-existing Matrix thread, please look around to see if anyone has created such a room for it. If this is the first message (ie. the thread was just created) or nobody has made a thread-room for this thread yet, it is recommended you create one with /thread <Thread Name> (on Matrix) and continue the conversation there. You can even run this command directly in this very Matrix thread, and the new Discord thread will be automatically attached to the same message as this one (alternatively, run that command outside a thread and as a reply to a different message to attach the thread to that message, or stand-alone to create a stand-alone thread).

You can read more about the rationale behind this design choice here.", + "formatted_body": "⚠️ This message may not have been bridged to Discord in the way you thought it was gonna be!

It seems like you sent this message inside a Matrix thread. Matrix threads don't work like Discord threads - they are effectively just „fancy replies”, not independent rooms/channels (any „thread-like appearance” is handled purely client-side - and even then, most Matrix clients don't handle it particularly well), and as such, they are bridged as replies to Discord. Discord users will not be aware that you sent this message inside a thread - it will go directly onto the main channel. If the thread you sent this message in is old, such a random reply may be distracting to Discord users!

For the sake of Discord parity (and for better support in numerous Matrix clients - as stated above, most Matrix clients don't handle threads particularly well, and they just render in-thread messages as fancy replies), it is recommended to send threaded messages inside a separate Matrix room that gets bridged to Discord. If you sent this message in a pre-existing Matrix thread, please look around to see if anyone has created such a room for it. If this is the first message (ie. the thread was just created) or nobody has made a thread-room for this thread yet, it is recommended you create one with /thread <Thread Name> and continue the conversation there. You can even run this command directly in this very Matrix thread, and the new Discord thread will be automatically attached to the same message as this one (alternatively, run that command outside a thread, and as a reply to a different message to attach the thread to that message or stand-alone to create a stand-alone thread).

You can read more about the rationale behind this design choice here.", "m.mentions": { "user_ids": [event.sender]}, "m.relates_to": { "event_id": event.content["m.relates_to"].event_id, From b53b2f56b6e67cd4932bda2fcdc578d235c82a37 Mon Sep 17 00:00:00 2001 From: Guzio Date: Tue, 24 Feb 2026 19:26:34 +0000 Subject: [PATCH 15/43] Improved error handling --- src/m2d/event-dispatcher.js | 4 +- src/matrix/matrix-command-handler.js | 74 +++++++++++++++++++++++++--- 2 files changed, 70 insertions(+), 8 deletions(-) diff --git a/src/m2d/event-dispatcher.js b/src/m2d/event-dispatcher.js index ec5fa89..b0cf640 100644 --- a/src/m2d/event-dispatcher.js +++ b/src/m2d/event-dispatcher.js @@ -222,9 +222,9 @@ async event => { } if (event.content["m.relates_to"]?.rel_type === "m.thread" && executedCommand !== "thread"){ api.sendEvent(event.room_id, "m.room.message", { - "body": "⚠️ **This message may not have been bridged to Discord in the way you thought it was gonna be!**\n\nIt seems like you sent this message inside a Matrix thread. Matrix threads don't work like Discord threads - they are effectively just „fancy replies”, not independent rooms/channels (any „thread-like appearance” is handled purely client-side - and even then, most Matrix clients don't handle it particularly well), and as such, they are bridged as replies to Discord. *Discord users will not be aware that you sent this message inside a thread - it will go directly onto the main channel. If the thread you sent this message in is old, such a random reply may be distracting to Discord users!*\n\nFor the sake of Discord parity (and for better support in numerous Matrix clients - as stated above, most Matrix clients don't handle threads particularly well, and they just render in-thread messages as fancy replies), it is recommended to send threaded messages inside a separate Matrix room that gets bridged to Discord. If you sent this message in a pre-existing Matrix thread, please look around to see if anyone has created such a room for it. If this is the first message (ie. the thread was just created) or nobody has made a thread-room for this thread yet, it is recommended you create one with `/thread ` and continue the conversation there. You can even run this command directly in this very Matrix thread, and the new Discord thread will be automatically attached to the same message as this one (alternatively, run that command outside a thread, and as a reply to a different message to attach the thread to that message or stand-alone to create a stand-alone thread).\n\n*You can read more about the rationale behind this design choice [here](https://gitdab.com/cadence/out-of-your-element/src/branch/main/docs/threads-as-rooms.md).*", + "body": "⚠️ **This message may not have been bridged to Discord in the way you thought it was gonna be!**\n\nIt seems like you sent this message inside a Matrix thread. Matrix threads don't work like Discord threads - they are effectively just „fancy replies”, not independent rooms/channels (any „thread-like appearance” is handled purely client-side - and even then, most Matrix clients don't handle it particularly well), and as such, they are bridged as replies to Discord. *Discord users will not be aware that you sent this message inside a thread - it will go directly onto the main channel. If the thread you sent this message in is old, such a random reply may be distracting to Discord users!*\n\nFor the sake of Discord parity (and for better support in numerous Matrix clients - as stated above, most Matrix clients don't handle threads particularly well, and they just render in-thread messages as fancy replies), it is recommended to send threaded messages inside a separate Matrix room that gets bridged to Discord. If you sent this message in a pre-existing Matrix thread, please look around to see if anyone has created such a room for it. If this is the first message (ie. the thread was just created) or nobody has made a thread-room for this thread yet, it is recommended you create one with `/thread ` and continue the conversation there. You can even run the command directly in this thread to attach the created Discord thread to the same message as this Matrix one (run `/thread` (with no arguments) to see other options for attachment).\n\n*You can read more about the rationale behind this design choice [here](https://gitdab.com/cadence/out-of-your-element/src/branch/main/docs/threads-as-rooms.md).*", "format": "org.matrix.custom.html", - "formatted_body": "⚠️ This message may not have been bridged to Discord in the way you thought it was gonna be!

It seems like you sent this message inside a Matrix thread. Matrix threads don't work like Discord threads - they are effectively just „fancy replies”, not independent rooms/channels (any „thread-like appearance” is handled purely client-side - and even then, most Matrix clients don't handle it particularly well), and as such, they are bridged as replies to Discord. Discord users will not be aware that you sent this message inside a thread - it will go directly onto the main channel. If the thread you sent this message in is old, such a random reply may be distracting to Discord users!

For the sake of Discord parity (and for better support in numerous Matrix clients - as stated above, most Matrix clients don't handle threads particularly well, and they just render in-thread messages as fancy replies), it is recommended to send threaded messages inside a separate Matrix room that gets bridged to Discord. If you sent this message in a pre-existing Matrix thread, please look around to see if anyone has created such a room for it. If this is the first message (ie. the thread was just created) or nobody has made a thread-room for this thread yet, it is recommended you create one with /thread <Thread Name> and continue the conversation there. You can even run this command directly in this very Matrix thread, and the new Discord thread will be automatically attached to the same message as this one (alternatively, run that command outside a thread, and as a reply to a different message to attach the thread to that message or stand-alone to create a stand-alone thread).

You can read more about the rationale behind this design choice here.", + "formatted_body": "⚠️ This message may not have been bridged to Discord in the way you thought it was gonna be!

It seems like you sent this message inside a Matrix thread. Matrix threads don't work like Discord threads - they are effectively just „fancy replies”, not independent rooms/channels (any „thread-like appearance” is handled purely client-side - and even then, most Matrix clients don't handle it particularly well), and as such, they are bridged as replies to Discord. Discord users will not be aware that you sent this message inside a thread - it will go directly onto the main channel. If the thread you sent this message in is old, such a random reply may be distracting to Discord users!

For the sake of Discord parity (and for better support in numerous Matrix clients - as stated above, most Matrix clients don't handle threads particularly well, and they just render in-thread messages as fancy replies), it is recommended to send threaded messages inside a separate Matrix room that gets bridged to Discord. If you sent this message in a pre-existing Matrix thread, please look around to see if anyone has created such a room for it. If this is the first message (ie. the thread was just created) or nobody has made a thread-room for this thread yet, it is recommended you create one with /thread and continue the conversation there. You can even run the command directly in this thread to attach the created Discord thread to the same message as this Matrix one (run /thread (with no arguments) to see other options for attachment).

You can read more about the rationale behind this design choice here.", "m.mentions": { "user_ids": [event.sender]}, "m.relates_to": { "event_id": event.content["m.relates_to"].event_id, diff --git a/src/matrix/matrix-command-handler.js b/src/matrix/matrix-command-handler.js index 6f3c5f9..fd4ae4a 100644 --- a/src/matrix/matrix-command-handler.js +++ b/src/matrix/matrix-command-handler.js @@ -236,6 +236,19 @@ const commands = [{ aliases: ["thread"], execute: replyctx( async (event, realBody, words, ctx) => { + if (words.length < 2){ + return api.sendEvent(event.room_id, "m.room.message", { + ...ctx, + msgtype: "m.text", + "m.relates_to":{ + "m.in_reply_to": event.event_id + }, + "body": "**`/thread` usage:**\nRun this command as `/thread ` to create a thread on Discord (and optionally Matrix, if one doesn't exist already). The message under which said thread will be attached, is chosen based on the following rules:\n* If ran stand-alone (not as a reply, nor in a Matrix thread), the created thread gets attached to the command-message itself.\n* If sent as a reply (outside a Matrix thread), the thread will be attached to the message to which you replied.\n* If ran inside an existing Matrix thread (regardless of whether it's a reply or not), the Discord thread will be attached to the same message as the Matrix thread already is.", + "format": "org.matrix.custom.html", + "formatted_body": "/thread usage:
Run this command as /thread <Thread Name> to create a thread on Discord (and optionally Matrix, if one doesn't exist already). The message under which said thread will be attached, is chosen based on the following rules:
  • If ran stand-alone (not as a reply, nor in a Matrix thread), the created thread gets attached to the command-message itself.
  • If sent as a reply (outside a Matrix thread), the thread will be attached to the message to which you replied.
  • If ran inside an existing Matrix thread (regardless of whether it's a reply or not), the Discord thread will be attached to the same message as the Matrix thread already is.
" + }) + } + // Guard /** @type {string} */ // @ts-ignore const channelID = select("channel_room", "channel_id", {room_id: event.room_id}).pluck().get() @@ -244,6 +257,9 @@ const commands = [{ return api.sendEvent(event.room_id, "m.room.message", { ...ctx, msgtype: "m.text", + "m.relates_to":{ + "m.in_reply_to": event.event_id + }, body: "This room isn't bridged to the other side." }) } @@ -255,7 +271,10 @@ const commands = [{ 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." // NOTE: Currently, this assumes that all Matrix users have no Discord roles, and makes that „If you were a Discord user...” claim based on that assumption. If Discord permission emulation is gonna happen in the future, this is certainly one among many places that will need to be changed. + "m.relates_to":{ + "m.in_reply_to": event.event_id + }, + 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." // NOTE: Currently, this assumes that all Matrix users have no Discord roles (see: empty [] in the getPermissions call above), and makes that „If you were a Discord user...” claim based on that assumption. If Discord permission emulation is gonna happen in the future, this is certainly one among many places that will need to be changed. }) } @@ -263,12 +282,55 @@ const commands = [{ let attachedToEvent = relation?.["m.in_reply_to"]?.event_id // By default, attempt to attach the thread to the message to which /thread was replying. if (relation?.rel_type === "m.thread") attachedToEvent = event.content["m.relates_to"]?.event_id // If /thread was sent inside a Matrix thread, attempt to attach the Discord thread to the message around which that Matrix thread was based on. if (!attachedToEvent) attachedToEvent = event.event_id // If /thread wasn't replying to anything (ie. attachedToEvent was undefined at initial assignment), or if the event was somehow malformed (in such a way that that - one way or another - attachedToEvent ended up being undefined, even if according to the spec it shouldn't), attach the thread to the /thread command-message that created it. - - assert(attachedToEvent) const attachedToMessage = select("event_message", "message_id", {event_id: attachedToEvent}).pluck().get() - - if (attachedToMessage) await discord.snow.channel.createThreadWithMessage(channelID, attachedToMessage, {name: words.slice(1).join(" ")}) - else await discord.snow.channel.createThreadWithoutMessage(channelID, {type: 11, name: words.slice(1).join(" ")}) + + try { + if (attachedToMessage) await discord.snow.channel.createThreadWithMessage(channelID, attachedToMessage, {name: words.slice(1).join(" ")}) + else throw "NO_ATTACH_TARGET"; + } + catch (e){ + if (e === "NO_ATTACH_TARGET") { + return api.sendEvent(event.room_id, "m.room.message", { + ...ctx, + msgtype: "m.text", + "m.relates_to":{ + "m.in_reply_to": event.event_id + }, + body: "Couldn't find a Discord representation of the message under which you're trying to attach this thread. Either you ran this command on an unbridged message (one sent by this bot or one that failed to bridge due to a previous error), or this is an error on our side and should be reported." + }) + } + else if (e.code === 160004) { // see: https://docs.discord.com/developers/topics/opcodes-and-status-codes + return api.sendEvent(event.room_id, "m.room.message", { + ...ctx, + msgtype: "m.text", + "m.relates_to":{ + "m.in_reply_to": event.event_id + }, + body: "There already exist a thread for the message you ran this command on (please run `/thread` (with no arguments) for details on attachment rules, if you're unsure what „ran this command on” refers to); cannot create a new one. Please scroll up and see where the link could be." + }) + } + else if (e.code === 50024) { + return api.sendEvent(event.room_id, "m.room.message", { + ...ctx, + msgtype: "m.text", + "m.relates_to":{ + "m.in_reply_to": event.event_id + }, + body: "You cannot create a new thread in this type of channel. Did you try to create a thread inside a thread?" + }) + } + else { + await api.sendEvent(event.room_id, "m.room.message", { + ...ctx, + msgtype: "m.text", + "m.relates_to":{ + "m.in_reply_to": event.event_id + }, + body: "Unknown error occurred during thread creation. See error message below (or on the main channel, if the command was ran inside a thread) for details." + }) + throw e; + } + } } ) }] From 23cdf549828dab27e201f543a71a5a22347d2b57 Mon Sep 17 00:00:00 2001 From: Guzio Date: Wed, 25 Feb 2026 09:25:02 +0000 Subject: [PATCH 16/43] AEUGH it turns out that replying was already handled. In other news: Made /thread work without args (in SOME cases). I'm pretty sure this is the final patch before we go PR. --- src/m2d/event-dispatcher.js | 4 +- src/matrix/matrix-command-handler.js | 64 +++++++++++----------------- 2 files changed, 27 insertions(+), 41 deletions(-) diff --git a/src/m2d/event-dispatcher.js b/src/m2d/event-dispatcher.js index b0cf640..5521012 100644 --- a/src/m2d/event-dispatcher.js +++ b/src/m2d/event-dispatcher.js @@ -222,9 +222,9 @@ async event => { } if (event.content["m.relates_to"]?.rel_type === "m.thread" && executedCommand !== "thread"){ api.sendEvent(event.room_id, "m.room.message", { - "body": "⚠️ **This message may not have been bridged to Discord in the way you thought it was gonna be!**\n\nIt seems like you sent this message inside a Matrix thread. Matrix threads don't work like Discord threads - they are effectively just „fancy replies”, not independent rooms/channels (any „thread-like appearance” is handled purely client-side - and even then, most Matrix clients don't handle it particularly well), and as such, they are bridged as replies to Discord. *Discord users will not be aware that you sent this message inside a thread - it will go directly onto the main channel. If the thread you sent this message in is old, such a random reply may be distracting to Discord users!*\n\nFor the sake of Discord parity (and for better support in numerous Matrix clients - as stated above, most Matrix clients don't handle threads particularly well, and they just render in-thread messages as fancy replies), it is recommended to send threaded messages inside a separate Matrix room that gets bridged to Discord. If you sent this message in a pre-existing Matrix thread, please look around to see if anyone has created such a room for it. If this is the first message (ie. the thread was just created) or nobody has made a thread-room for this thread yet, it is recommended you create one with `/thread ` and continue the conversation there. You can even run the command directly in this thread to attach the created Discord thread to the same message as this Matrix one (run `/thread` (with no arguments) to see other options for attachment).\n\n*You can read more about the rationale behind this design choice [here](https://gitdab.com/cadence/out-of-your-element/src/branch/main/docs/threads-as-rooms.md).*", + "body": "⚠️ **This message may not have been bridged to Discord in the way you thought it was gonna be!**\n\nIt seems like you sent this message inside a Matrix thread. Matrix threads don't work like Discord threads - they are effectively just „fancy replies”, not independent rooms/channels (any „thread-like appearance” is handled purely client-side - and even then, most Matrix clients don't handle it particularly well, with Element being the only one known to actually render threads as threads), and as such, they are bridged as replies to Discord. *In other words: __Discord users will not be aware that you sent this message inside a thread - the reply will go directly onto the main channel.__ If the thread you sent this message in is old, such a random reply may be distracting to Discord users!*\n\nFor the sake of Discord parity (and for better support in numerous Matrix clients - as stated above, most Matrix clients don't handle threads particularly well, and they just render in-thread messages as fancy replies), it is recommended to send threaded messages inside a separate Matrix room that gets bridged to Discord. If you sent this message in a pre-existing Matrix thread, please look around to see if anyone has created such a room for it. If this is the first message (ie. the thread was just created) or nobody has made a thread-room for this thread yet, it is recommended you create one with `/thread` and continue the conversation there. You can run that command directly in this thread to attach the created Discord thread to the same message as this Matrix one, or run it outside any threads to see other options for attachment.\n\n*You can read more about the rationale behind this design choice [here](https://gitdab.com/cadence/out-of-your-element/src/branch/main/docs/threads-as-rooms.md).*", "format": "org.matrix.custom.html", - "formatted_body": "⚠️ This message may not have been bridged to Discord in the way you thought it was gonna be!

It seems like you sent this message inside a Matrix thread. Matrix threads don't work like Discord threads - they are effectively just „fancy replies”, not independent rooms/channels (any „thread-like appearance” is handled purely client-side - and even then, most Matrix clients don't handle it particularly well), and as such, they are bridged as replies to Discord. Discord users will not be aware that you sent this message inside a thread - it will go directly onto the main channel. If the thread you sent this message in is old, such a random reply may be distracting to Discord users!

For the sake of Discord parity (and for better support in numerous Matrix clients - as stated above, most Matrix clients don't handle threads particularly well, and they just render in-thread messages as fancy replies), it is recommended to send threaded messages inside a separate Matrix room that gets bridged to Discord. If you sent this message in a pre-existing Matrix thread, please look around to see if anyone has created such a room for it. If this is the first message (ie. the thread was just created) or nobody has made a thread-room for this thread yet, it is recommended you create one with /thread and continue the conversation there. You can even run the command directly in this thread to attach the created Discord thread to the same message as this Matrix one (run /thread (with no arguments) to see other options for attachment).

You can read more about the rationale behind this design choice here.", + "formatted_body": "⚠️ This message may not have been bridged to Discord in the way you thought it was gonna be!

It seems like you sent this message inside a Matrix thread. Matrix threads don't work like Discord threads - they are effectively just „fancy replies”, not independent rooms/channels (any „thread-like appearance” is handled purely client-side - and even then, most Matrix clients don't handle it particularly well, with Element being the only one known to actually render threads as threads), and as such, they are bridged as replies to Discord. In other words: Discord users will not be aware that you sent this message inside a thread - the reply will go directly onto the main channel. If the thread you sent this message in is old, such a random reply may be distracting to Discord users!

For the sake of Discord parity (and for better support in numerous Matrix clients - as stated above, most Matrix clients don't handle threads particularly well, and they just render in-thread messages as fancy replies), it is recommended to send threaded messages inside a separate Matrix room that gets bridged to Discord. If you sent this message in a pre-existing Matrix thread, please look around to see if anyone has created such a room for it. If this is the first message (ie. the thread was just created) or nobody has made a thread-room for this thread yet, it is recommended you create one with /thread and continue the conversation there. You can run that command directly in this thread to attach the created Discord thread to the same message as this Matrix one, or run it outside any threads to see other options for attachment.

You can read more about the rationale behind this design choice here.", "m.mentions": { "user_ids": [event.sender]}, "m.relates_to": { "event_id": event.content["m.relates_to"].event_id, diff --git a/src/matrix/matrix-command-handler.js b/src/matrix/matrix-command-handler.js index fd4ae4a..6ef4b5f 100644 --- a/src/matrix/matrix-command-handler.js +++ b/src/matrix/matrix-command-handler.js @@ -236,19 +236,6 @@ const commands = [{ aliases: ["thread"], execute: replyctx( async (event, realBody, words, ctx) => { - if (words.length < 2){ - return api.sendEvent(event.room_id, "m.room.message", { - ...ctx, - msgtype: "m.text", - "m.relates_to":{ - "m.in_reply_to": event.event_id - }, - "body": "**`/thread` usage:**\nRun this command as `/thread ` to create a thread on Discord (and optionally Matrix, if one doesn't exist already). The message under which said thread will be attached, is chosen based on the following rules:\n* If ran stand-alone (not as a reply, nor in a Matrix thread), the created thread gets attached to the command-message itself.\n* If sent as a reply (outside a Matrix thread), the thread will be attached to the message to which you replied.\n* If ran inside an existing Matrix thread (regardless of whether it's a reply or not), the Discord thread will be attached to the same message as the Matrix thread already is.", - "format": "org.matrix.custom.html", - "formatted_body": "/thread usage:
Run this command as /thread <Thread Name> to create a thread on Discord (and optionally Matrix, if one doesn't exist already). The message under which said thread will be attached, is chosen based on the following rules:
  • If ran stand-alone (not as a reply, nor in a Matrix thread), the created thread gets attached to the command-message itself.
  • If sent as a reply (outside a Matrix thread), the thread will be attached to the message to which you replied.
  • If ran inside an existing Matrix thread (regardless of whether it's a reply or not), the Discord thread will be attached to the same message as the Matrix thread already is.
" - }) - } - // Guard /** @type {string} */ // @ts-ignore const channelID = select("channel_room", "channel_id", {room_id: event.room_id}).pluck().get() @@ -257,9 +244,6 @@ const commands = [{ return api.sendEvent(event.room_id, "m.room.message", { ...ctx, msgtype: "m.text", - "m.relates_to":{ - "m.in_reply_to": event.event_id - }, body: "This room isn't bridged to the other side." }) } @@ -271,61 +255,63 @@ const commands = [{ return api.sendEvent(event.room_id, "m.room.message", { ...ctx, msgtype: "m.text", - "m.relates_to":{ - "m.in_reply_to": event.event_id - }, 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." // NOTE: Currently, this assumes that all Matrix users have no Discord roles (see: empty [] in the getPermissions call above), and makes that „If you were a Discord user...” claim based on that assumption. If Discord permission emulation is gonna happen in the future, this is certainly one among many places that will need to be changed. }) } const relation = event.content["m.relates_to"] + let isFallingBack = false; let attachedToEvent = relation?.["m.in_reply_to"]?.event_id // By default, attempt to attach the thread to the message to which /thread was replying. - if (relation?.rel_type === "m.thread") attachedToEvent = event.content["m.relates_to"]?.event_id // If /thread was sent inside a Matrix thread, attempt to attach the Discord thread to the message around which that Matrix thread was based on. - if (!attachedToEvent) attachedToEvent = event.event_id // If /thread wasn't replying to anything (ie. attachedToEvent was undefined at initial assignment), or if the event was somehow malformed (in such a way that that - one way or another - attachedToEvent ended up being undefined, even if according to the spec it shouldn't), attach the thread to the /thread command-message that created it. + if (relation?.rel_type === "m.thread") attachedToEvent = relation?.event_id // If /thread was sent inside a Matrix thread, attempt to attach the Discord thread to the message around which that Matrix thread was based on. + if (!attachedToEvent){ + attachedToEvent = event.event_id // If /thread wasn't replying to anything (ie. attachedToEvent was undefined at initial assignment), or if the event was somehow malformed (in such a way that that - one way or another - attachedToEvent ended up being undefined, even if according to the spec it shouldn't), attach the thread to the /thread command-message that created it. + isFallingBack = true; + } const attachedToMessage = select("event_message", "message_id", {event_id: attachedToEvent}).pluck().get() + + if (words.length < 2){ + words[1] = (await api.getEvent(event.room_id, attachedToEvent)).content.body + if (isFallingBack) return api.sendEvent(event.room_id, "m.room.message", { + ...ctx, + msgtype: "m.text", + "body": "**`/thread` usage:**\nRun this command as `/thread [Thread Name]` to create a thread on Discord (and optionally Matrix, if one doesn't exist already). The message under which said thread will be attached, is chosen based on the following rules:\n* If ran stand-alone (not as a reply, nor in a Matrix thread), the created thread gets attached to the command-message itself. The `Thread Name` argument must be provided in this case, otherwise you get this help message.\n* If sent as a reply (outside a Matrix thread), the thread will be attached to the message to which you replied.\n* If ran inside an existing Matrix thread (regardless of whether it's a reply or not), the Discord thread will be attached to the same message as the Matrix thread already is.", + "format": "org.matrix.custom.html", + "formatted_body": "/thread usage:
Run this command as /thread [Thread Name] to create a thread on Discord (and optionally Matrix, if one doesn't exist already). The message under which said thread will be attached, is chosen based on the following rules:
  • If ran stand-alone (not as a reply, nor in a Matrix thread), the created thread gets attached to the command-message itself. The Thread Name argument must be provided in this case, otherwise you get this help message.
  • If sent as a reply (outside a Matrix thread), the thread will be attached to the message to which you replied.
  • If ran inside an existing Matrix thread (regardless of whether it's a reply or not), the Discord thread will be attached to the same message as the Matrix thread already is.
" + }) + } try { - if (attachedToMessage) await discord.snow.channel.createThreadWithMessage(channelID, attachedToMessage, {name: words.slice(1).join(" ")}) - else throw "NO_ATTACH_TARGET"; + if (attachedToMessage) return discord.snow.channel.createThreadWithMessage(channelID, attachedToMessage, {name: words.slice(1).join(" ")}) + else throw {code: "NO_ATTACH_TARGET", was_supposed_to_be: attachedToEvent}; } catch (e){ - if (e === "NO_ATTACH_TARGET") { + if (e.code === "NO_ATTACH_TARGET") { return api.sendEvent(event.room_id, "m.room.message", { ...ctx, msgtype: "m.text", - "m.relates_to":{ - "m.in_reply_to": event.event_id - }, - body: "Couldn't find a Discord representation of the message under which you're trying to attach this thread. Either you ran this command on an unbridged message (one sent by this bot or one that failed to bridge due to a previous error), or this is an error on our side and should be reported." + body: "Couldn't find a Discord representation of the message under which you're trying to attach this thread (event "+e.was_supposed_to_be+" on Matrix). Either you ran this command on an unbridged message (one sent by this bot or one that failed to bridge due to a previous error), or this is an error on our side and should be reported." }) } else if (e.code === 160004) { // see: https://docs.discord.com/developers/topics/opcodes-and-status-codes return api.sendEvent(event.room_id, "m.room.message", { ...ctx, msgtype: "m.text", - "m.relates_to":{ - "m.in_reply_to": event.event_id - }, - body: "There already exist a thread for the message you ran this command on (please run `/thread` (with no arguments) for details on attachment rules, if you're unsure what „ran this command on” refers to); cannot create a new one. Please scroll up and see where the link could be." + "body": "There already exist a thread for the message you ran this command on (please run `/thread` (with no arguments) for details on attachment rules, if you're unsure what „ran this command on” refers to); cannot create a new one. Please scroll up and see where the link could be.", + "format": "org.matrix.custom.html", + "formatted_body": "There already exist a thread for the message you ran this command on (please run /thread (with no arguments) for details on attachment rules, if you're unsure what „ran this command on” refers to); cannot create a new one. Please scroll up and see where the link could be.", }) } else if (e.code === 50024) { return api.sendEvent(event.room_id, "m.room.message", { ...ctx, msgtype: "m.text", - "m.relates_to":{ - "m.in_reply_to": event.event_id - }, - body: "You cannot create a new thread in this type of channel. Did you try to create a thread inside a thread?" + body: "You cannot create a thread in a Discord channel of the type, to which this Matrix room is bridged to. Did you try to create a thread inside a thread?" }) } else { await api.sendEvent(event.room_id, "m.room.message", { ...ctx, msgtype: "m.text", - "m.relates_to":{ - "m.in_reply_to": event.event_id - }, body: "Unknown error occurred during thread creation. See error message below (or on the main channel, if the command was ran inside a thread) for details." }) throw e; From 69d07c1a7b3c5ae17a24d7134e2a436928578eb6 Mon Sep 17 00:00:00 2001 From: Guzio Date: Wed, 25 Feb 2026 09:48:22 +0000 Subject: [PATCH 17/43] I lied, that's the final patch (my C# past got the better of me lol) --- src/types.d.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/types.d.ts b/src/types.d.ts index e7ef318..e36241f 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -191,7 +191,7 @@ export namespace Event { formatted_body?: string, "m.relates_to"?: { event_id?: string - is_falling_back?: bool + is_falling_back?: boolean "m.in_reply_to"?: { event_id: string } @@ -212,7 +212,7 @@ export namespace Event { "page.codeberg.everypizza.msc4193.spoiler"?: boolean "m.relates_to"?: { event_id?: string - is_falling_back?: bool + is_falling_back?: boolean "m.in_reply_to"?: { event_id: string } @@ -249,7 +249,7 @@ export namespace Event { info?: any "m.relates_to"?: { event_id?: string - is_falling_back?: bool + is_falling_back?: boolean "m.in_reply_to"?: { event_id: string } From f0515ceecf8e1726ee94439374e29707bf125c01 Mon Sep 17 00:00:00 2001 From: Guzio Date: Wed, 25 Feb 2026 10:11:18 +0000 Subject: [PATCH 18/43] UX testing revealed that og messages looked awkward --- src/matrix/matrix-command-handler.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/matrix/matrix-command-handler.js b/src/matrix/matrix-command-handler.js index 6ef4b5f..7fb8fe0 100644 --- a/src/matrix/matrix-command-handler.js +++ b/src/matrix/matrix-command-handler.js @@ -289,16 +289,18 @@ const commands = [{ return api.sendEvent(event.room_id, "m.room.message", { ...ctx, msgtype: "m.text", - body: "Couldn't find a Discord representation of the message under which you're trying to attach this thread (event "+e.was_supposed_to_be+" on Matrix). Either you ran this command on an unbridged message (one sent by this bot or one that failed to bridge due to a previous error), or this is an error on our side and should be reported." + "body": "⚠️ Couldn't find a Discord representation of the message under which you're trying to attach this thread (event ID `"+e.was_supposed_to_be+"` on Matrix), so it wasn't created. Either you ran this command on an unbridged message (one sent by this bot or one that failed to bridge due to a previous error), or this is an error on our side and should be reported.", + "format": "org.matrix.custom.html", + "formatted_body": "⚠️ Couldn't find a Discord representation of the message under which you're trying to attach this thread (event ID "+e.was_supposed_to_be+" on Matrix), so it wasn't created. Either you ran this command on an unbridged message (one sent by this bot or one that failed to bridge due to a previous error), or this is an error on our side and should be reported." }) } else if (e.code === 160004) { // see: https://docs.discord.com/developers/topics/opcodes-and-status-codes return api.sendEvent(event.room_id, "m.room.message", { ...ctx, msgtype: "m.text", - "body": "There already exist a thread for the message you ran this command on (please run `/thread` (with no arguments) for details on attachment rules, if you're unsure what „ran this command on” refers to); cannot create a new one. Please scroll up and see where the link could be.", + "body": "There already exist a thread for the message you ran this command on (please run `/thread` (with no arguments and outside any threads) for details on attachment rules, if you're unsure what „ran this command on” refers to); cannot create a new one. Scroll up and see where the link could be.", "format": "org.matrix.custom.html", - "formatted_body": "There already exist a thread for the message you ran this command on (please run /thread (with no arguments) for details on attachment rules, if you're unsure what „ran this command on” refers to); cannot create a new one. Please scroll up and see where the link could be.", + "formatted_body": "There already exist a thread for the message you ran this command on (please run /thread (with no arguments and outside any threads) for details on attachment rules, if you're unsure what „ran this command on” refers to); cannot create a new one. Scroll up and see where the link could be.", }) } else if (e.code === 50024) { From 4a260013829aaa485ce7d0f5f364afbac6db9904 Mon Sep 17 00:00:00 2001 From: Guzio Date: Wed, 25 Feb 2026 10:23:13 +0000 Subject: [PATCH 19/43] Debug-slop begins! --- src/matrix/matrix-command-handler.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/matrix/matrix-command-handler.js b/src/matrix/matrix-command-handler.js index 7fb8fe0..d82ce3e 100644 --- a/src/matrix/matrix-command-handler.js +++ b/src/matrix/matrix-command-handler.js @@ -285,6 +285,7 @@ const commands = [{ else throw {code: "NO_ATTACH_TARGET", was_supposed_to_be: attachedToEvent}; } catch (e){ + console.log(e) if (e.code === "NO_ATTACH_TARGET") { return api.sendEvent(event.room_id, "m.room.message", { ...ctx, From 0ad4b41ae96824383455da54392a777c596a4cb0 Mon Sep 17 00:00:00 2001 From: Guzio Date: Wed, 25 Feb 2026 10:28:09 +0000 Subject: [PATCH 20/43] Iiiiiii........... I have no idea what was I trying to accomplish here... --- src/matrix/matrix-command-handler.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/matrix/matrix-command-handler.js b/src/matrix/matrix-command-handler.js index d82ce3e..786df5c 100644 --- a/src/matrix/matrix-command-handler.js +++ b/src/matrix/matrix-command-handler.js @@ -281,7 +281,7 @@ const commands = [{ } try { - if (attachedToMessage) return discord.snow.channel.createThreadWithMessage(channelID, attachedToMessage, {name: words.slice(1).join(" ")}) + if (attachedToMessage) await discord.snow.channel.createThreadWithMessage(channelID, attachedToMessage, {name: words.slice(1).join(" ")}) else throw {code: "NO_ATTACH_TARGET", was_supposed_to_be: attachedToEvent}; } catch (e){ From c283528d720a9093db84b4b452206c9525f15534 Mon Sep 17 00:00:00 2001 From: Guzio Date: Wed, 25 Feb 2026 10:32:37 +0000 Subject: [PATCH 21/43] Is this LITERALLY just a String????? --- src/matrix/matrix-command-handler.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/matrix/matrix-command-handler.js b/src/matrix/matrix-command-handler.js index 786df5c..4ccba82 100644 --- a/src/matrix/matrix-command-handler.js +++ b/src/matrix/matrix-command-handler.js @@ -285,7 +285,7 @@ const commands = [{ else throw {code: "NO_ATTACH_TARGET", was_supposed_to_be: attachedToEvent}; } catch (e){ - console.log(e) + console.log(typeof e) if (e.code === "NO_ATTACH_TARGET") { return api.sendEvent(event.room_id, "m.room.message", { ...ctx, From bea0b9370d413fed28c70a92748982f378573400 Mon Sep 17 00:00:00 2001 From: Guzio Date: Wed, 25 Feb 2026 10:39:56 +0000 Subject: [PATCH 22/43] Type of e is its own content. Apparently. What the fuck? --- src/matrix/matrix-command-handler.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/matrix/matrix-command-handler.js b/src/matrix/matrix-command-handler.js index 4ccba82..b31a937 100644 --- a/src/matrix/matrix-command-handler.js +++ b/src/matrix/matrix-command-handler.js @@ -285,7 +285,8 @@ const commands = [{ else throw {code: "NO_ATTACH_TARGET", was_supposed_to_be: attachedToEvent}; } catch (e){ - console.log(typeof e) + const what_how = typeof e + console.log("Type of "+e+" is "+what_how) if (e.code === "NO_ATTACH_TARGET") { return api.sendEvent(event.room_id, "m.room.message", { ...ctx, From fa916699a787068f4b8e32dbcce1f41c5e14b4b0 Mon Sep 17 00:00:00 2001 From: Guzio Date: Wed, 25 Feb 2026 10:48:12 +0000 Subject: [PATCH 23/43] Nope, it's an object. But, like... A weird one. It doesn't seem to behave like objects normally do. It it a wrapper around stuff? --- src/matrix/matrix-command-handler.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/matrix/matrix-command-handler.js b/src/matrix/matrix-command-handler.js index b31a937..7b18e71 100644 --- a/src/matrix/matrix-command-handler.js +++ b/src/matrix/matrix-command-handler.js @@ -286,7 +286,7 @@ const commands = [{ } catch (e){ const what_how = typeof e - console.log("Type of "+e+" is "+what_how) + console.log("Type of "+e+" is "+what_how+" with keys: "+Object.keys(e)) if (e.code === "NO_ATTACH_TARGET") { return api.sendEvent(event.room_id, "m.room.message", { ...ctx, From 9bf6e50ae9adaded912921cba6e58f6c07e8940f Mon Sep 17 00:00:00 2001 From: Guzio Date: Wed, 25 Feb 2026 10:56:51 +0000 Subject: [PATCH 24/43] SO WHAT DO YOU WANT? TELL ME WHAT'S YOUR POINT, JS! So the code-key DOES exist. HuuuHhhhhh??? --- src/matrix/matrix-command-handler.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/matrix/matrix-command-handler.js b/src/matrix/matrix-command-handler.js index 7b18e71..734582e 100644 --- a/src/matrix/matrix-command-handler.js +++ b/src/matrix/matrix-command-handler.js @@ -286,7 +286,8 @@ const commands = [{ } catch (e){ const what_how = typeof e - console.log("Type of "+e+" is "+what_how+" with keys: "+Object.keys(e)) + const but_im_done_with_you = typeof e.code + console.log("Type of "+e+" is "+what_how+" with keys "+Object.keys(e) + " and the value of that „code” key is "+e.code+" of type "+but_im_done_with_you) if (e.code === "NO_ATTACH_TARGET") { return api.sendEvent(event.room_id, "m.room.message", { ...ctx, From 9424b5e517d0cc6d5919bcffa156d8d327d09131 Mon Sep 17 00:00:00 2001 From: Guzio Date: Wed, 25 Feb 2026 11:04:55 +0000 Subject: [PATCH 25/43] =?UTF-8?q?Apparenly,=20what=20I=20completley=20miss?= =?UTF-8?q?ed,=20is=20that=20=E2=80=9Ecode=E2=80=9D=20is=20overriden=20by?= =?UTF-8?q?=20something=20later=20in=20the=20error=20stack.=20Trying=20out?= =?UTF-8?q?=20other=20keys......?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/matrix/matrix-command-handler.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/matrix/matrix-command-handler.js b/src/matrix/matrix-command-handler.js index 734582e..07246cd 100644 --- a/src/matrix/matrix-command-handler.js +++ b/src/matrix/matrix-command-handler.js @@ -288,6 +288,8 @@ const commands = [{ const what_how = typeof e const but_im_done_with_you = typeof e.code console.log("Type of "+e+" is "+what_how+" with keys "+Object.keys(e) + " and the value of that „code” key is "+e.code+" of type "+but_im_done_with_you) + console.log("MSG: "+e.message) + console.log("NAM: "+e.name) if (e.code === "NO_ATTACH_TARGET") { return api.sendEvent(event.room_id, "m.room.message", { ...ctx, From 06962c217e46da6e0e4094f957b3b6b491c612ec Mon Sep 17 00:00:00 2001 From: Guzio Date: Thu, 26 Feb 2026 12:23:09 +0000 Subject: [PATCH 26/43] unspecified horsing around --- src/m2d/event-dispatcher.js | 5 +++-- src/matrix/matrix-command-handler.js | 17 +++++++++++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/src/m2d/event-dispatcher.js b/src/m2d/event-dispatcher.js index 5521012..0cd1b0c 100644 --- a/src/m2d/event-dispatcher.js +++ b/src/m2d/event-dispatcher.js @@ -158,11 +158,12 @@ async function sendError(roomID, source, type, e, payload) { /** * @param {string} type - * @param {(event: any, ...args: any)=>any} fn + * @param {(event: Ty.Event.Outer & {type: any, redacts:any, state_key:any}, ...args: any)=>any} fn */ function guard(type, fn) { return async function(/** @type {Ty.Event.Outer} */ event, /** @type {any} */ ...args) { try { + // @ts-ignore return await fn(event, ...args) } catch (e) { await sendError(event.room_id, "Matrix", type, e, event) @@ -216,7 +217,7 @@ async event => { let executedCommand if (event.type === "m.room.message" && event.content.msgtype === "m.text") { executedCommand = await matrixCommandHandler.parseAndExecute( - // @ts-ignore - TypeScript doesn't know that the event.content.msgtype === "m.text" ensures that event isn't of type Ty.Event.Outer_M_Room_Message_File (which, indeed, wouldn't fit here) + // @ts-ignore - TypeScript doesn't know that the event.content.msgtype === "m.text" check ensures that event isn't of type Ty.Event.Outer_M_Room_Message_File (which, indeed, wouldn't fit here) event ) } diff --git a/src/matrix/matrix-command-handler.js b/src/matrix/matrix-command-handler.js index 07246cd..b8c3b20 100644 --- a/src/matrix/matrix-command-handler.js +++ b/src/matrix/matrix-command-handler.js @@ -96,6 +96,22 @@ function replyctx(execute) { } } +/** + * @param {Error & {code: string|number}} e + * @returns {e} +*/ +function unmarshallDiscordError(e) { + if (e.name === "DiscordAPIError"){ + try{ + + JSON.parse(e.message) + } catch{ + + } + } + return e; +} + /** @type {Command[]} */ const commands = [{ aliases: ["emoji"], @@ -289,6 +305,7 @@ const commands = [{ const but_im_done_with_you = typeof e.code console.log("Type of "+e+" is "+what_how+" with keys "+Object.keys(e) + " and the value of that „code” key is "+e.code+" of type "+but_im_done_with_you) console.log("MSG: "+e.message) + console.log("MST: "+typeof e.message) console.log("NAM: "+e.name) if (e.code === "NO_ATTACH_TARGET") { return api.sendEvent(event.room_id, "m.room.message", { From 266f46563b3ac3e7b5e3cc8a8a166fae024bcc5e Mon Sep 17 00:00:00 2001 From: Guzio Date: Thu, 26 Feb 2026 12:49:21 +0000 Subject: [PATCH 27/43] Possibly? fixed error handling??? and yea, ofc it was a string...... --- src/matrix/matrix-command-handler.js | 34 ++++++++++++++++------------ 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/src/matrix/matrix-command-handler.js b/src/matrix/matrix-command-handler.js index b8c3b20..bd9ecfe 100644 --- a/src/matrix/matrix-command-handler.js +++ b/src/matrix/matrix-command-handler.js @@ -97,16 +97,27 @@ function replyctx(execute) { } /** - * @param {Error & {code: string|number}} e + * @param {Error & {code?: string|number}} e * @returns {e} */ function unmarshallDiscordError(e) { if (e.name === "DiscordAPIError"){ try{ - - JSON.parse(e.message) - } catch{ - + const unmarshaled = JSON.parse(e.message) + return { + ...e, + ...unmarshaled + } + } catch (err) { + return { + ...err, + code: "JSON_PARSE_FAILED", + message: JSON.stringify({ + original_error_where_message_failed_to_parse: e, + json_parser_error_message: err.message, + json_parser_error_code: err.code, + }) + } } } return e; @@ -300,20 +311,15 @@ const commands = [{ if (attachedToMessage) await discord.snow.channel.createThreadWithMessage(channelID, attachedToMessage, {name: words.slice(1).join(" ")}) else throw {code: "NO_ATTACH_TARGET", was_supposed_to_be: attachedToEvent}; } - catch (e){ - const what_how = typeof e - const but_im_done_with_you = typeof e.code - console.log("Type of "+e+" is "+what_how+" with keys "+Object.keys(e) + " and the value of that „code” key is "+e.code+" of type "+but_im_done_with_you) - console.log("MSG: "+e.message) - console.log("MST: "+typeof e.message) - console.log("NAM: "+e.name) + catch (e_raw){ + const e = unmarshallDiscordError(e_raw) if (e.code === "NO_ATTACH_TARGET") { return api.sendEvent(event.room_id, "m.room.message", { ...ctx, msgtype: "m.text", - "body": "⚠️ Couldn't find a Discord representation of the message under which you're trying to attach this thread (event ID `"+e.was_supposed_to_be+"` on Matrix), so it wasn't created. Either you ran this command on an unbridged message (one sent by this bot or one that failed to bridge due to a previous error), or this is an error on our side and should be reported.", + "body": "⚠️ Couldn't find a Discord representation of the message under which you're trying to attach this thread (event ID `"+e_raw.was_supposed_to_be+"` on Matrix), so it wasn't created. Either you ran this command on an unbridged message (one sent by this bot or one that failed to bridge due to a previous error), or this is an error on our side and should be reported.", "format": "org.matrix.custom.html", - "formatted_body": "⚠️ Couldn't find a Discord representation of the message under which you're trying to attach this thread (event ID "+e.was_supposed_to_be+" on Matrix), so it wasn't created. Either you ran this command on an unbridged message (one sent by this bot or one that failed to bridge due to a previous error), or this is an error on our side and should be reported." + "formatted_body": "⚠️ Couldn't find a Discord representation of the message under which you're trying to attach this thread (event ID "+e_raw.was_supposed_to_be+" on Matrix), so it wasn't created. Either you ran this command on an unbridged message (one sent by this bot or one that failed to bridge due to a previous error), or this is an error on our side and should be reported." }) } else if (e.code === 160004) { // see: https://docs.discord.com/developers/topics/opcodes-and-status-codes From 3f7a7aa10f875d6324cc8eb151614faacb990c38 Mon Sep 17 00:00:00 2001 From: Guzio Date: Thu, 26 Feb 2026 15:37:17 +0000 Subject: [PATCH 28/43] handled overyapping --- src/matrix/matrix-command-handler.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/matrix/matrix-command-handler.js b/src/matrix/matrix-command-handler.js index bd9ecfe..761d299 100644 --- a/src/matrix/matrix-command-handler.js +++ b/src/matrix/matrix-command-handler.js @@ -298,6 +298,7 @@ const commands = [{ if (words.length < 2){ words[1] = (await api.getEvent(event.room_id, attachedToEvent)).content.body + words[1] = words[1].length < 100 ? words[1] : words[1].slice(0, 96) + "..." if (isFallingBack) return api.sendEvent(event.room_id, "m.room.message", { ...ctx, msgtype: "m.text", @@ -335,7 +336,14 @@ const commands = [{ return api.sendEvent(event.room_id, "m.room.message", { ...ctx, msgtype: "m.text", - body: "You cannot create a thread in a Discord channel of the type, to which this Matrix room is bridged to. Did you try to create a thread inside a thread?" + body: "You cannot create threads in a Discord channel of the type, to which this Matrix room is bridged to. Did you try to create a thread inside a thread?" + }) + } + else if (e.code === 50035) { + return api.sendEvent(event.room_id, "m.room.message", { + ...ctx, + msgtype: "m.text", + body: "Specified thread name is too long - thread creation failed. Please yap a bit less in the title, thread body is for that. ;)" }) } else { From 22ff10222c7b32d9f183a02e6d24bd8a1841918b Mon Sep 17 00:00:00 2001 From: Guzio Date: Thu, 26 Feb 2026 15:38:20 +0000 Subject: [PATCH 29/43] fixed error details showing up as [object Object] --- src/matrix/matrix-command-handler.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/matrix/matrix-command-handler.js b/src/matrix/matrix-command-handler.js index 761d299..e6086ba 100644 --- a/src/matrix/matrix-command-handler.js +++ b/src/matrix/matrix-command-handler.js @@ -352,7 +352,7 @@ const commands = [{ msgtype: "m.text", body: "Unknown error occurred during thread creation. See error message below (or on the main channel, if the command was ran inside a thread) for details." }) - throw e; + throw e_raw; } } } From 3e42616065de6a9f87da61992a7230285c7b432e Mon Sep 17 00:00:00 2001 From: Guzio Date: Thu, 26 Feb 2026 15:55:18 +0000 Subject: [PATCH 30/43] =?UTF-8?q?It=20just=20occurred=20to=20me=20that=20I?= =?UTF-8?q?=20have=20no=20way=20of=20testing=20the=20=E2=80=9Efallback=20c?= =?UTF-8?q?ase=E2=80=9D=20now?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/matrix/matrix-command-handler.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/matrix/matrix-command-handler.js b/src/matrix/matrix-command-handler.js index e6086ba..07ae178 100644 --- a/src/matrix/matrix-command-handler.js +++ b/src/matrix/matrix-command-handler.js @@ -339,13 +339,13 @@ const commands = [{ body: "You cannot create threads in a Discord channel of the type, to which this Matrix room is bridged to. Did you try to create a thread inside a thread?" }) } - else if (e.code === 50035) { + /*else if (e.code === 50035) { return api.sendEvent(event.room_id, "m.room.message", { ...ctx, msgtype: "m.text", - body: "Specified thread name is too long - thread creation failed. Please yap a bit less in the title, thread body is for that. ;)" + body: "Specified thread name is too long - thread creation failed. Please yap a bit less in the title, the thread body is for that. ;)" }) - } + }*/ else { await api.sendEvent(event.room_id, "m.room.message", { ...ctx, From 0557c7b143112aa5331cef03ccd07ab71a94f5fc Mon Sep 17 00:00:00 2001 From: Guzio Date: Thu, 26 Feb 2026 16:00:22 +0000 Subject: [PATCH 31/43] works yay --- src/matrix/matrix-command-handler.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/matrix/matrix-command-handler.js b/src/matrix/matrix-command-handler.js index 07ae178..7c8667e 100644 --- a/src/matrix/matrix-command-handler.js +++ b/src/matrix/matrix-command-handler.js @@ -339,13 +339,13 @@ const commands = [{ body: "You cannot create threads in a Discord channel of the type, to which this Matrix room is bridged to. Did you try to create a thread inside a thread?" }) } - /*else if (e.code === 50035) { + else if (e.code === 50035) { return api.sendEvent(event.room_id, "m.room.message", { ...ctx, msgtype: "m.text", body: "Specified thread name is too long - thread creation failed. Please yap a bit less in the title, the thread body is for that. ;)" }) - }*/ + } else { await api.sendEvent(event.room_id, "m.room.message", { ...ctx, From ffed434c6ab11e584b4d1ab6ca3e6e871d589814 Mon Sep 17 00:00:00 2001 From: Guzio Date: Thu, 26 Feb 2026 16:05:53 +0000 Subject: [PATCH 32/43] rewrote the f#cker as a switch statement --- src/matrix/matrix-command-handler.js | 44 ++++++++++++---------------- 1 file changed, 19 insertions(+), 25 deletions(-) diff --git a/src/matrix/matrix-command-handler.js b/src/matrix/matrix-command-handler.js index 7c8667e..8c0c3c2 100644 --- a/src/matrix/matrix-command-handler.js +++ b/src/matrix/matrix-command-handler.js @@ -314,45 +314,39 @@ const commands = [{ } catch (e_raw){ const e = unmarshallDiscordError(e_raw) - if (e.code === "NO_ATTACH_TARGET") { - return api.sendEvent(event.room_id, "m.room.message", { + switch (e.code) { + case "NO_ATTACH_TARGET": return api.sendEvent(event.room_id, "m.room.message", { ...ctx, msgtype: "m.text", "body": "⚠️ Couldn't find a Discord representation of the message under which you're trying to attach this thread (event ID `"+e_raw.was_supposed_to_be+"` on Matrix), so it wasn't created. Either you ran this command on an unbridged message (one sent by this bot or one that failed to bridge due to a previous error), or this is an error on our side and should be reported.", "format": "org.matrix.custom.html", "formatted_body": "⚠️ Couldn't find a Discord representation of the message under which you're trying to attach this thread (event ID "+e_raw.was_supposed_to_be+" on Matrix), so it wasn't created. Either you ran this command on an unbridged message (one sent by this bot or one that failed to bridge due to a previous error), or this is an error on our side and should be reported." }) - } - else if (e.code === 160004) { // see: https://docs.discord.com/developers/topics/opcodes-and-status-codes - return api.sendEvent(event.room_id, "m.room.message", { - ...ctx, - msgtype: "m.text", - "body": "There already exist a thread for the message you ran this command on (please run `/thread` (with no arguments and outside any threads) for details on attachment rules, if you're unsure what „ran this command on” refers to); cannot create a new one. Scroll up and see where the link could be.", - "format": "org.matrix.custom.html", - "formatted_body": "There already exist a thread for the message you ran this command on (please run /thread (with no arguments and outside any threads) for details on attachment rules, if you're unsure what „ran this command on” refers to); cannot create a new one. Scroll up and see where the link could be.", - }) - } - else if (e.code === 50024) { - return api.sendEvent(event.room_id, "m.room.message", { + case (160004): // see: https://docs.discord.com/developers/topics/opcodes-and-status-codes + return api.sendEvent(event.room_id, "m.room.message", { + ...ctx, + msgtype: "m.text", + "body": "There already exist a thread for the message you ran this command on (please run `/thread` (with no arguments and outside any threads) for details on attachment rules, if you're unsure what „ran this command on” refers to); cannot create a new one. Scroll up and see where the link could be.", + "format": "org.matrix.custom.html", + "formatted_body": "There already exist a thread for the message you ran this command on (please run /thread (with no arguments and outside any threads) for details on attachment rules, if you're unsure what „ran this command on” refers to); cannot create a new one. Scroll up and see where the link could be.", + }) + case (50024): return api.sendEvent(event.room_id, "m.room.message", { ...ctx, msgtype: "m.text", body: "You cannot create threads in a Discord channel of the type, to which this Matrix room is bridged to. Did you try to create a thread inside a thread?" }) - } - else if (e.code === 50035) { - return api.sendEvent(event.room_id, "m.room.message", { + case (50035): return api.sendEvent(event.room_id, "m.room.message", { ...ctx, msgtype: "m.text", body: "Specified thread name is too long - thread creation failed. Please yap a bit less in the title, the thread body is for that. ;)" }) - } - else { - await api.sendEvent(event.room_id, "m.room.message", { - ...ctx, - msgtype: "m.text", - body: "Unknown error occurred during thread creation. See error message below (or on the main channel, if the command was ran inside a thread) for details." - }) - throw e_raw; + default: + await api.sendEvent(event.room_id, "m.room.message", { + ...ctx, + msgtype: "m.text", + body: "Unknown error occurred during thread creation. See error message below (or on the main channel, if the command was ran inside a thread) for details." + }) + throw e_raw; } } } From 42c32ba749cc27e899cee94cbc9240625a51cee6 Mon Sep 17 00:00:00 2001 From: Guzio Date: Fri, 27 Feb 2026 13:06:52 +0000 Subject: [PATCH 33/43] explained my technical decisions; made a function that'll help me later --- src/matrix/matrix-command-handler.js | 2 +- src/matrix/utils.js | 13 ++++++++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/matrix/matrix-command-handler.js b/src/matrix/matrix-command-handler.js index 8c0c3c2..a5124f9 100644 --- a/src/matrix/matrix-command-handler.js +++ b/src/matrix/matrix-command-handler.js @@ -346,7 +346,7 @@ const commands = [{ msgtype: "m.text", body: "Unknown error occurred during thread creation. See error message below (or on the main channel, if the command was ran inside a thread) for details." }) - throw e_raw; + throw e_raw; //Has to be e_raw, as functions get stripped from e as part of unmarshaling Discord error } } } diff --git a/src/matrix/utils.js b/src/matrix/utils.js index 9f5cb0f..dbdb6d8 100644 --- a/src/matrix/utils.js +++ b/src/matrix/utils.js @@ -4,7 +4,7 @@ const assert = require("assert").strict const Ty = require("../types") const {tag} = require("@cloudrac3r/html-template-tag") const passthrough = require("../passthrough") -const {db} = passthrough +const {db, select} = passthrough const {reg} = require("./read-registration") const userRegex = reg.namespaces.users.map(u => new RegExp(u.regex)) @@ -398,6 +398,16 @@ async function setUserPowerCascade(spaceID, mxid, power, api) { } } +/** + * Set a user's power level for a whole room hierarchy. + * @param {string} eventID + */ +function getThreadRoomFromThreadMessage(eventID){ + const threadID = select("event_message", "message_id", {event_id: eventID}).pluck().get() //Discord thread ID === its message ID + if (!threadID) return threadID; + return select("channel_room", "room_id", {channel_id: threadID}).pluck().get() +} + module.exports.bot = bot module.exports.BLOCK_ELEMENTS = BLOCK_ELEMENTS module.exports.eventSenderIsFromDiscord = eventSenderIsFromDiscord @@ -413,3 +423,4 @@ module.exports.removeCreatorsFromPowerLevels = removeCreatorsFromPowerLevels module.exports.getEffectivePower = getEffectivePower module.exports.setUserPower = setUserPower module.exports.setUserPowerCascade = setUserPowerCascade +module.exports.getThreadRoomFromThreadMessage = getThreadRoomFromThreadMessage From c9509bb9383603818be8f2fa5f8c0fb0f0c8f490 Mon Sep 17 00:00:00 2001 From: Guzio Date: Fri, 27 Feb 2026 20:42:16 +0000 Subject: [PATCH 34/43] figured out how tests work, yaaayyyy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit As a part of that: * rewrote the tests to support my changed behaviors * added a missing case * Made my „threads get attached to” wording more consistent with the test cases („threads branch from”), as I always felt that there was something off about my phrasing, but I couldn't quite tell what was it. OOYE's „branch” term seems much more fitting there * slightly cooked the testing data (changed the „Hey.” thread from „floating” to being a branch of a message, to accommodate...) * 3 NEW TESTS: of the function created in my previous commit (I'm not sure if this *REALLY* needed testing, given how braindead-simple that function is, but everything else in utils.js is covered, so I figured it's only fair to test this, too) EXTRA CHANGE: fixed that function's name (we're getting the thread from a (Matrix) Event, not a (Discord) Message) and description (I totally didn't copy-paste the JSDoc from above........) --- .../converters/thread-to-announcement.test.js | 65 +++++++++++++++---- src/matrix/matrix-command-handler.js | 30 ++++----- src/matrix/utils.js | 5 +- src/matrix/utils.test.js | 21 +++++- test/ooye-test-data.sql | 4 +- 5 files changed, 94 insertions(+), 31 deletions(-) diff --git a/src/d2m/converters/thread-to-announcement.test.js b/src/d2m/converters/thread-to-announcement.test.js index 3286f62..8af4d79 100644 --- a/src/d2m/converters/thread-to-announcement.test.js +++ b/src/d2m/converters/thread-to-announcement.test.js @@ -49,7 +49,7 @@ test("thread2announcement: no known creator, no branched from event", async t => }, {api: viaApi}) t.deepEqual(content, { msgtype: "m.text", - body: "Thread started: test thread https://matrix.to/#/!thread?via=cadence.moe&via=matrix.org", + body: "New thread started: „test thread” in room: https://matrix.to/#/!thread?via=cadence.moe&via=matrix.org", "m.mentions": {} }) }) @@ -61,7 +61,7 @@ test("thread2announcement: known creator, no branched from event", async t => { }, {api: viaApi}) t.deepEqual(content, { msgtype: "m.emote", - body: "started a thread: test thread https://matrix.to/#/!thread?via=cadence.moe&via=matrix.org", + body: "started a thread „test thread” in room: https://matrix.to/#/!thread?via=cadence.moe&via=matrix.org", "m.mentions": {} }) }) @@ -85,12 +85,15 @@ test("thread2announcement: no known creator, branched from discord event", async }) t.deepEqual(content, { msgtype: "m.text", - body: "Thread started: test thread https://matrix.to/#/!thread?via=cadence.moe&via=matrix.org", + body: "New thread started: „test thread” in room: https://matrix.to/#/!thread?via=cadence.moe&via=matrix.org\n[Note: You should continue the conversation in that room, rather than in this thread. Any messages sent in Matrix threads will be bridged to Discord as replies, not in-thread messages, which is probably not what you want.]", "m.mentions": {}, "m.relates_to": { + "event_id": "$X16nfVks1wsrhq4E9SSLiqrf2N8KD0erD0scZG7U5xg", + "is_falling_back": false, "m.in_reply_to": { - event_id: "$X16nfVks1wsrhq4E9SSLiqrf2N8KD0erD0scZG7U5xg" - } + "event_id": "$X16nfVks1wsrhq4E9SSLiqrf2N8KD0erD0scZG7U5xg", + }, + "rel_type": "m.thread", } }) }) @@ -114,12 +117,15 @@ test("thread2announcement: known creator, branched from discord event", async t }) t.deepEqual(content, { msgtype: "m.emote", - body: "started a thread: test thread https://matrix.to/#/!thread?via=cadence.moe&via=matrix.org", + body: "started a thread „test thread” in room: https://matrix.to/#/!thread?via=cadence.moe&via=matrix.org\n[Note: You should continue the conversation in that room, rather than in this thread. Any messages sent in Matrix threads will be bridged to Discord as replies, not in-thread messages, which is probably not what you want.]", "m.mentions": {}, "m.relates_to": { + "event_id": "$X16nfVks1wsrhq4E9SSLiqrf2N8KD0erD0scZG7U5xg", + "is_falling_back": false, "m.in_reply_to": { - event_id: "$X16nfVks1wsrhq4E9SSLiqrf2N8KD0erD0scZG7U5xg" - } + "event_id": "$X16nfVks1wsrhq4E9SSLiqrf2N8KD0erD0scZG7U5xg", + }, + "rel_type": "m.thread", } }) }) @@ -143,14 +149,51 @@ test("thread2announcement: no known creator, branched from matrix event", async }) t.deepEqual(content, { msgtype: "m.text", - body: "Thread started: test thread https://matrix.to/#/!thread?via=cadence.moe&via=matrix.org", + body: "New thread started: „test thread” in room: https://matrix.to/#/!thread?via=cadence.moe&via=matrix.org\n[Note: You should continue the conversation in that room, rather than in this thread. Any messages sent in Matrix threads will be bridged to Discord as replies, not in-thread messages, which is probably not what you want.]", "m.mentions": { user_ids: ["@cadence:cadence.moe"] }, "m.relates_to": { + "event_id": "$Ij3qo7NxMA4VPexlAiIx2CB9JbsiGhJeyt-2OvkAUe4", + "is_falling_back": false, "m.in_reply_to": { - event_id: "$Ij3qo7NxMA4VPexlAiIx2CB9JbsiGhJeyt-2OvkAUe4" - } + "event_id": "$Ij3qo7NxMA4VPexlAiIx2CB9JbsiGhJeyt-2OvkAUe4", + }, + "rel_type": "m.thread", + } + }) +}) + +test("thread2announcement: known creator, branched from matrix event", async t => { + const content = await threadToAnnouncement("!kLRqKKUQXcibIMtOpl:cadence.moe", "!thread", "@_ooye_crunch_god:cadence.moe", { + name: "test thread", + id: "1128118177155526666" + }, { + api: { + getEvent: mockGetEvent(t, "!kLRqKKUQXcibIMtOpl:cadence.moe", "$Ij3qo7NxMA4VPexlAiIx2CB9JbsiGhJeyt-2OvkAUe4", { + type: "m.room.message", + content: { + msgtype: "m.text", + body: "so can you reply to my webhook uwu" + }, + sender: "@cadence:cadence.moe" + }), + ...viaApi + } + }) + t.deepEqual(content, { + msgtype: "m.emote", + body: "started a thread „test thread” in room: https://matrix.to/#/!thread?via=cadence.moe&via=matrix.org\n[Note: You should continue the conversation in that room, rather than in this thread. Any messages sent in Matrix threads will be bridged to Discord as replies, not in-thread messages, which is probably not what you want.]", + "m.mentions": { + user_ids: ["@cadence:cadence.moe"] + }, + "m.relates_to": { + "event_id": "$Ij3qo7NxMA4VPexlAiIx2CB9JbsiGhJeyt-2OvkAUe4", + "is_falling_back": false, + "m.in_reply_to": { + "event_id": "$Ij3qo7NxMA4VPexlAiIx2CB9JbsiGhJeyt-2OvkAUe4", + }, + "rel_type": "m.thread", } }) }) diff --git a/src/matrix/matrix-command-handler.js b/src/matrix/matrix-command-handler.js index a5124f9..ec53d21 100644 --- a/src/matrix/matrix-command-handler.js +++ b/src/matrix/matrix-command-handler.js @@ -288,47 +288,47 @@ const commands = [{ const relation = event.content["m.relates_to"] let isFallingBack = false; - let attachedToEvent = relation?.["m.in_reply_to"]?.event_id // By default, attempt to attach the thread to the message to which /thread was replying. - if (relation?.rel_type === "m.thread") attachedToEvent = relation?.event_id // If /thread was sent inside a Matrix thread, attempt to attach the Discord thread to the message around which that Matrix thread was based on. - if (!attachedToEvent){ - attachedToEvent = event.event_id // If /thread wasn't replying to anything (ie. attachedToEvent was undefined at initial assignment), or if the event was somehow malformed (in such a way that that - one way or another - attachedToEvent ended up being undefined, even if according to the spec it shouldn't), attach the thread to the /thread command-message that created it. + let branchedFromMxEvent = relation?.["m.in_reply_to"]?.event_id // By default, attempt to branch the thread from the message to which /thread was replying. + if (relation?.rel_type === "m.thread") branchedFromMxEvent = relation?.event_id // If /thread was sent inside a Matrix thread, attempt to branch the Discord thread from the message, which that Matrix thread already is branching from. + if (!branchedFromMxEvent){ + branchedFromMxEvent = event.event_id // If /thread wasn't replying to anything (ie. branchedFromMxEvent was undefined at initial assignment), or if the event was somehow malformed (in such a way that that - one way or another - branchedFromMxEvent ended up being undefined, even if according to the spec it shouldn't), branch the thread from the /thread command-message that created it. isFallingBack = true; } - const attachedToMessage = select("event_message", "message_id", {event_id: attachedToEvent}).pluck().get() + const branchedFromDiscordMessage = select("event_message", "message_id", {event_id: branchedFromMxEvent}).pluck().get() if (words.length < 2){ - words[1] = (await api.getEvent(event.room_id, attachedToEvent)).content.body + words[1] = (await api.getEvent(event.room_id, branchedFromMxEvent)).content.body words[1] = words[1].length < 100 ? words[1] : words[1].slice(0, 96) + "..." if (isFallingBack) return api.sendEvent(event.room_id, "m.room.message", { ...ctx, msgtype: "m.text", - "body": "**`/thread` usage:**\nRun this command as `/thread [Thread Name]` to create a thread on Discord (and optionally Matrix, if one doesn't exist already). The message under which said thread will be attached, is chosen based on the following rules:\n* If ran stand-alone (not as a reply, nor in a Matrix thread), the created thread gets attached to the command-message itself. The `Thread Name` argument must be provided in this case, otherwise you get this help message.\n* If sent as a reply (outside a Matrix thread), the thread will be attached to the message to which you replied.\n* If ran inside an existing Matrix thread (regardless of whether it's a reply or not), the Discord thread will be attached to the same message as the Matrix thread already is.", + "body": "**`/thread` usage:**\nRun this command as `/thread [Thread Name]` to create a thread on Discord (and optionally Matrix, if one doesn't exist already). The message from which said thread will branch, is chosen based on the following rules:\n* If ran stand-alone (not as a reply, nor in a Matrix thread), the created thread will branch from the command-message itself. The `Thread Name` argument must be provided in this case, otherwise you get this help message.\n* If sent as a reply (outside a Matrix thread), the thread will branch from the message to which you replied.\n* If ran inside an existing Matrix thread (regardless of whether it's a reply or not), the created Discord thread will be branching from the same message as the Matrix thread already is.", "format": "org.matrix.custom.html", - "formatted_body": "/thread usage:
Run this command as /thread [Thread Name] to create a thread on Discord (and optionally Matrix, if one doesn't exist already). The message under which said thread will be attached, is chosen based on the following rules:
  • If ran stand-alone (not as a reply, nor in a Matrix thread), the created thread gets attached to the command-message itself. The Thread Name argument must be provided in this case, otherwise you get this help message.
  • If sent as a reply (outside a Matrix thread), the thread will be attached to the message to which you replied.
  • If ran inside an existing Matrix thread (regardless of whether it's a reply or not), the Discord thread will be attached to the same message as the Matrix thread already is.
" + "formatted_body": "/thread usage:
Run this command as /thread [Thread Name] to create a thread on Discord (and optionally Matrix, if one doesn't exist already). The message from which said thread will branch, is chosen based on the following rules:
  • If ran stand-alone (not as a reply, nor in a Matrix thread), the created thread will branch from the command-message itself. The Thread Name argument must be provided in this case, otherwise you get this help message.
  • If sent as a reply (outside a Matrix thread), the thread will branch from the message to which you replied.
  • If ran inside an existing Matrix thread (regardless of whether it's a reply or not), the created Discord thread will be branching from the same message as the Matrix thread already is.
" }) } try { - if (attachedToMessage) await discord.snow.channel.createThreadWithMessage(channelID, attachedToMessage, {name: words.slice(1).join(" ")}) - else throw {code: "NO_ATTACH_TARGET", was_supposed_to_be: attachedToEvent}; + if (branchedFromDiscordMessage) await discord.snow.channel.createThreadWithMessage(channelID, branchedFromDiscordMessage, {name: words.slice(1).join(" ")}) + else throw {code: "NO_BRANCH_SOURCE", was_supposed_to_be: branchedFromMxEvent}; } catch (e_raw){ const e = unmarshallDiscordError(e_raw) switch (e.code) { - case "NO_ATTACH_TARGET": return api.sendEvent(event.room_id, "m.room.message", { + case "NO_BRANCH_SOURCE": return api.sendEvent(event.room_id, "m.room.message", { ...ctx, msgtype: "m.text", - "body": "⚠️ Couldn't find a Discord representation of the message under which you're trying to attach this thread (event ID `"+e_raw.was_supposed_to_be+"` on Matrix), so it wasn't created. Either you ran this command on an unbridged message (one sent by this bot or one that failed to bridge due to a previous error), or this is an error on our side and should be reported.", + "body": "⚠️ Couldn't find a Discord representation of the message from which you're trying to branch this thread (event ID `"+e_raw.was_supposed_to_be+"` on Matrix), so it wasn't created. Either you ran this command on an unbridged message (one sent by this bot or one that failed to bridge due to a previous error), or this is an error on our side and should be reported.", "format": "org.matrix.custom.html", - "formatted_body": "⚠️ Couldn't find a Discord representation of the message under which you're trying to attach this thread (event ID "+e_raw.was_supposed_to_be+" on Matrix), so it wasn't created. Either you ran this command on an unbridged message (one sent by this bot or one that failed to bridge due to a previous error), or this is an error on our side and should be reported." + "formatted_body": "⚠️ Couldn't find a Discord representation of the message from which you're trying to branch this thread (event ID "+e_raw.was_supposed_to_be+" on Matrix), so it wasn't created. Either you ran this command on an unbridged message (one sent by this bot or one that failed to bridge due to a previous error), or this is an error on our side and should be reported." }) case (160004): // see: https://docs.discord.com/developers/topics/opcodes-and-status-codes return api.sendEvent(event.room_id, "m.room.message", { ...ctx, msgtype: "m.text", - "body": "There already exist a thread for the message you ran this command on (please run `/thread` (with no arguments and outside any threads) for details on attachment rules, if you're unsure what „ran this command on” refers to); cannot create a new one. Scroll up and see where the link could be.", + "body": "There already exist a thread for the message you ran this command on (please run `/thread` (with no arguments and outside any threads) for details on branching rules, if you're unsure what „ran this command on” refers to); cannot create a new one. Scroll up and see where the link could be.", "format": "org.matrix.custom.html", - "formatted_body": "There already exist a thread for the message you ran this command on (please run /thread (with no arguments and outside any threads) for details on attachment rules, if you're unsure what „ran this command on” refers to); cannot create a new one. Scroll up and see where the link could be.", + "formatted_body": "There already exist a thread for the message you ran this command on (please run /thread (with no arguments and outside any threads) for details on branching rules, if you're unsure what „ran this command on” refers to); cannot create a new one. Scroll up and see where the link could be.", }) case (50024): return api.sendEvent(event.room_id, "m.room.message", { ...ctx, diff --git a/src/matrix/utils.js b/src/matrix/utils.js index dbdb6d8..f9dbf3f 100644 --- a/src/matrix/utils.js +++ b/src/matrix/utils.js @@ -399,10 +399,9 @@ async function setUserPowerCascade(spaceID, mxid, power, api) { } /** - * Set a user's power level for a whole room hierarchy. * @param {string} eventID */ -function getThreadRoomFromThreadMessage(eventID){ +function getThreadRoomFromThreadEvent(eventID){ const threadID = select("event_message", "message_id", {event_id: eventID}).pluck().get() //Discord thread ID === its message ID if (!threadID) return threadID; return select("channel_room", "room_id", {channel_id: threadID}).pluck().get() @@ -423,4 +422,4 @@ module.exports.removeCreatorsFromPowerLevels = removeCreatorsFromPowerLevels module.exports.getEffectivePower = getEffectivePower module.exports.setUserPower = setUserPower module.exports.setUserPowerCascade = setUserPowerCascade -module.exports.getThreadRoomFromThreadMessage = getThreadRoomFromThreadMessage +module.exports.getThreadRoomFromThreadEvent = getThreadRoomFromThreadEvent diff --git a/src/matrix/utils.test.js b/src/matrix/utils.test.js index 842c513..f42ce5c 100644 --- a/src/matrix/utils.test.js +++ b/src/matrix/utils.test.js @@ -2,7 +2,7 @@ const {select} = require("../passthrough") const {test} = require("supertape") -const {eventSenderIsFromDiscord, getEventIDHash, MatrixStringBuilder, getViaServers, roomHasAtLeastVersion, removeCreatorsFromPowerLevels, setUserPower} = require("./utils") +const {eventSenderIsFromDiscord, getEventIDHash, MatrixStringBuilder, getViaServers, roomHasAtLeastVersion, removeCreatorsFromPowerLevels, setUserPower, getThreadRoomFromThreadEvent} = require("./utils") const util = require("util") /** @param {string[]} mxids */ @@ -417,4 +417,23 @@ test("set user power: privileged users must demote themselves", async t => { t.equal(called, 3) }) +test("getThreadRoomFromThreadEvent: real message, but without a thread", t => { + const room = getThreadRoomFromThreadEvent("$Ij3qo7NxMA4VPexlAiIx2CB9JbsiGhJeyt-2OvkAUe4") + const msg = "Expected null/undefined, got: "+room + if(room) t.fail(msg); + else t.pass(msg) +}) + +test("getThreadRoomFromThreadEvent: real message with a thread", t => { + const room = getThreadRoomFromThreadEvent("$fdD9o7NxMA4VPexlAiIx2CB9JbsiGhJeyJgnZG7U5xg") + t.equal(room, "!FuDZhlOAtqswlyxzeR:cadence.moe") +}) + +test("getThreadRoomFromThreadEvent: fake message", t => { + const room = getThreadRoomFromThreadEvent("$ThisEvent-IdDoesNotExistInTheDatabase4Sure") + const msg = "Expected null/undefined, got: "+room + if(room) t.fail(msg); + else t.pass(msg) +}) + module.exports.mockGetEffectivePower = mockGetEffectivePower diff --git a/test/ooye-test-data.sql b/test/ooye-test-data.sql index 1dd9dfe..a321d48 100644 --- a/test/ooye-test-data.sql +++ b/test/ooye-test-data.sql @@ -82,12 +82,14 @@ WITH a (message_id, channel_id) AS (VALUES ('1381212840957972480', '112760669178241024'), ('1401760355339862066', '112760669178241024'), ('1439351590262800565', '1438284564815548418'), -('1404133238414376971', '112760669178241024')) +('1404133238414376971', '112760669178241024'), +('1162005314908999790', '1100319550446252084')) SELECT message_id, max(historical_room_index) as historical_room_index FROM a INNER JOIN historical_channel_room ON historical_channel_room.reference_channel_id = a.channel_id GROUP BY message_id; 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), +('$fdD9o7NxMA4VPexlAiIx2CB9JbsiGhJeyJgnZG7U5xg', 'm.room.message', 'm.text', '1162005314908999790', 0, 0, 1), ('$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), From edfbdc567f50dae91cd1863f81d75dbf710634a6 Mon Sep 17 00:00:00 2001 From: Guzio Date: Fri, 27 Feb 2026 23:07:43 +0000 Subject: [PATCH 35/43] Used getThreadRoomFromThreadEvent in practice --- src/m2d/event-dispatcher.js | 5 +++-- src/matrix/matrix-command-handler.js | 16 +++++++--------- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/src/m2d/event-dispatcher.js b/src/m2d/event-dispatcher.js index 0cd1b0c..b247290 100644 --- a/src/m2d/event-dispatcher.js +++ b/src/m2d/event-dispatcher.js @@ -222,10 +222,11 @@ async event => { ) } if (event.content["m.relates_to"]?.rel_type === "m.thread" && executedCommand !== "thread"){ + const bridgedTo = utils.getThreadRoomFromThreadEvent(event.event_id) api.sendEvent(event.room_id, "m.room.message", { - "body": "⚠️ **This message may not have been bridged to Discord in the way you thought it was gonna be!**\n\nIt seems like you sent this message inside a Matrix thread. Matrix threads don't work like Discord threads - they are effectively just „fancy replies”, not independent rooms/channels (any „thread-like appearance” is handled purely client-side - and even then, most Matrix clients don't handle it particularly well, with Element being the only one known to actually render threads as threads), and as such, they are bridged as replies to Discord. *In other words: __Discord users will not be aware that you sent this message inside a thread - the reply will go directly onto the main channel.__ If the thread you sent this message in is old, such a random reply may be distracting to Discord users!*\n\nFor the sake of Discord parity (and for better support in numerous Matrix clients - as stated above, most Matrix clients don't handle threads particularly well, and they just render in-thread messages as fancy replies), it is recommended to send threaded messages inside a separate Matrix room that gets bridged to Discord. If you sent this message in a pre-existing Matrix thread, please look around to see if anyone has created such a room for it. If this is the first message (ie. the thread was just created) or nobody has made a thread-room for this thread yet, it is recommended you create one with `/thread` and continue the conversation there. You can run that command directly in this thread to attach the created Discord thread to the same message as this Matrix one, or run it outside any threads to see other options for attachment.\n\n*You can read more about the rationale behind this design choice [here](https://gitdab.com/cadence/out-of-your-element/src/branch/main/docs/threads-as-rooms.md).*", + "body": "⚠️ **This message may not have been bridged to Discord in the way you thought it was gonna be!**\n\nIt seems like you sent this message inside a Matrix thread. Matrix threads don't work like Discord threads - they are effectively just „fancy replies”, not independent rooms/channels (any „thread-like appearance” is handled purely client-side - and even then, most Matrix clients don't handle it particularly well, with Element being the only one known to actually render threads as threads), and as such, they are bridged as replies to Discord. *In other words: __Discord users will not be aware that you sent this message inside a thread - the reply will go directly onto the main channel.__ If the thread you sent this message in is old, such a random reply **may be distracting** to Discord users!*\n\nFor the sake of Discord parity (and for better support in numerous Matrix clients - as stated above, most Matrix clients don't handle threads particularly well, and they just render in-thread messages as fancy replies), it is recommended to send threaded messages inside a separate Matrix room that gets bridged to Discord. "+ (bridgedTo ? "Luckily for you, this thread already has one! You can access it on https://matrix.to/#/"+bridgedTo+"?"+(await utils.getViaServersQuery(bridgedTo, api)).toString() : "Please run `/thread [Optional: Thread Name]` to create such a room for this thread, or get a link to it if someone else has already done so. If you run `/thread` (without any arguments) outside any threads and not as a reply, you'll get more info about this command")+".\n\n*You can read more about the rationale behind this design choice [here](https://gitdab.com/cadence/out-of-your-element/src/branch/main/docs/threads-as-rooms.md).*", "format": "org.matrix.custom.html", - "formatted_body": "⚠️ This message may not have been bridged to Discord in the way you thought it was gonna be!

It seems like you sent this message inside a Matrix thread. Matrix threads don't work like Discord threads - they are effectively just „fancy replies”, not independent rooms/channels (any „thread-like appearance” is handled purely client-side - and even then, most Matrix clients don't handle it particularly well, with Element being the only one known to actually render threads as threads), and as such, they are bridged as replies to Discord. In other words: Discord users will not be aware that you sent this message inside a thread - the reply will go directly onto the main channel. If the thread you sent this message in is old, such a random reply may be distracting to Discord users!

For the sake of Discord parity (and for better support in numerous Matrix clients - as stated above, most Matrix clients don't handle threads particularly well, and they just render in-thread messages as fancy replies), it is recommended to send threaded messages inside a separate Matrix room that gets bridged to Discord. If you sent this message in a pre-existing Matrix thread, please look around to see if anyone has created such a room for it. If this is the first message (ie. the thread was just created) or nobody has made a thread-room for this thread yet, it is recommended you create one with /thread and continue the conversation there. You can run that command directly in this thread to attach the created Discord thread to the same message as this Matrix one, or run it outside any threads to see other options for attachment.

You can read more about the rationale behind this design choice here.", + "formatted_body": "⚠️ This message may not have been bridged to Discord in the way you thought it was gonna be!

It seems like you sent this message inside a Matrix thread. Matrix threads don't work like Discord threads - they are effectively just „fancy replies”, not independent rooms/channels (any „thread-like appearance” is handled purely client-side - and even then, most Matrix clients don't handle it particularly well, with Element being the only one known to actually render threads as threads), and as such, they are bridged as replies to Discord. In other words: Discord users will not be aware that you sent this message inside a thread - the reply will go directly onto the main channel. If the thread you sent this message in is old, such a random reply may be distracting to Discord users!

For the sake of Discord parity (and for better support in numerous Matrix clients - as stated above, most Matrix clients don't handle threads particularly well, and they just render in-thread messages as fancy replies), it is recommended to send threaded messages inside a separate Matrix room that gets bridged to Discord. "+ (bridgedTo ? "Luckily for you, this thread already has one! You can access it on https://matrix.to/#/"+bridgedTo+"?"+(await utils.getViaServersQuery(bridgedTo, api)).toString()+"" : "Please run /thread [Optional: Thread Name] to create such a room for this thread, or get a link to it if someone else has already done so. If you run /thread (without any arguments) outside any threads and not as a reply, you'll get more info about this command")+".

You can read more about the rationale behind this design choice here.", "m.mentions": { "user_ids": [event.sender]}, "m.relates_to": { "event_id": event.content["m.relates_to"].event_id, diff --git a/src/matrix/matrix-command-handler.js b/src/matrix/matrix-command-handler.js index ec53d21..ff46b9c 100644 --- a/src/matrix/matrix-command-handler.js +++ b/src/matrix/matrix-command-handler.js @@ -312,23 +312,21 @@ const commands = [{ if (branchedFromDiscordMessage) await discord.snow.channel.createThreadWithMessage(channelID, branchedFromDiscordMessage, {name: words.slice(1).join(" ")}) else throw {code: "NO_BRANCH_SOURCE", was_supposed_to_be: branchedFromMxEvent}; } - catch (e_raw){ - const e = unmarshallDiscordError(e_raw) - switch (e.code) { + catch (e){ + switch (unmarshallDiscordError(e).code) { case "NO_BRANCH_SOURCE": return api.sendEvent(event.room_id, "m.room.message", { ...ctx, msgtype: "m.text", - "body": "⚠️ Couldn't find a Discord representation of the message from which you're trying to branch this thread (event ID `"+e_raw.was_supposed_to_be+"` on Matrix), so it wasn't created. Either you ran this command on an unbridged message (one sent by this bot or one that failed to bridge due to a previous error), or this is an error on our side and should be reported.", + "body": "⚠️ Couldn't find a Discord representation of the message from which you're trying to branch this thread (event ID `"+e.was_supposed_to_be+"` on Matrix), so it wasn't created. Either you ran this command on an unbridged message (one sent by this bot or one that failed to bridge due to a previous error), or this is an error on our side and should be reported.", "format": "org.matrix.custom.html", - "formatted_body": "⚠️ Couldn't find a Discord representation of the message from which you're trying to branch this thread (event ID "+e_raw.was_supposed_to_be+" on Matrix), so it wasn't created. Either you ran this command on an unbridged message (one sent by this bot or one that failed to bridge due to a previous error), or this is an error on our side and should be reported." + "formatted_body": "⚠️ Couldn't find a Discord representation of the message from which you're trying to branch this thread (event ID "+e.was_supposed_to_be+" on Matrix), so it wasn't created. Either you ran this command on an unbridged message (one sent by this bot or one that failed to bridge due to a previous error), or this is an error on our side and should be reported." }) case (160004): // see: https://docs.discord.com/developers/topics/opcodes-and-status-codes + const thread = mxUtils.getThreadRoomFromThreadEvent(event.event_id) return api.sendEvent(event.room_id, "m.room.message", { ...ctx, msgtype: "m.text", - "body": "There already exist a thread for the message you ran this command on (please run `/thread` (with no arguments and outside any threads) for details on branching rules, if you're unsure what „ran this command on” refers to); cannot create a new one. Scroll up and see where the link could be.", - "format": "org.matrix.custom.html", - "formatted_body": "There already exist a thread for the message you ran this command on (please run /thread (with no arguments and outside any threads) for details on branching rules, if you're unsure what „ran this command on” refers to); cannot create a new one. Scroll up and see where the link could be.", + body: "There already exist a Discord thread for the message you ran this command on" + (thread ? " - you may join its bridged room here: https://matrix.to/#/"+thread+"?"+(await mxUtils.getViaServersQuery(thread, api)).toString() : ", so a new one cannot be crated. However, it seems like that thread isn't bridged to any Matrix rooms. Please ask the space/server admins to rectify this issue (or have someone send a message in that thread on Discord side to bridge it automatically, if that's enabled for this space/server).") }) case (50024): return api.sendEvent(event.room_id, "m.room.message", { ...ctx, @@ -346,7 +344,7 @@ const commands = [{ msgtype: "m.text", body: "Unknown error occurred during thread creation. See error message below (or on the main channel, if the command was ran inside a thread) for details." }) - throw e_raw; //Has to be e_raw, as functions get stripped from e as part of unmarshaling Discord error + throw e } } } From 10fbb9e6960e0be4874a6bb6a46a10d78b45211c Mon Sep 17 00:00:00 2001 From: Guzio Date: Fri, 27 Feb 2026 23:11:50 +0000 Subject: [PATCH 36/43] improved consistency --- src/m2d/event-dispatcher.js | 16 ++++++++-------- src/matrix/matrix-command-handler.js | 12 ++++++------ 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/m2d/event-dispatcher.js b/src/m2d/event-dispatcher.js index b247290..f3f6964 100644 --- a/src/m2d/event-dispatcher.js +++ b/src/m2d/event-dispatcher.js @@ -224,17 +224,17 @@ async event => { if (event.content["m.relates_to"]?.rel_type === "m.thread" && executedCommand !== "thread"){ const bridgedTo = utils.getThreadRoomFromThreadEvent(event.event_id) api.sendEvent(event.room_id, "m.room.message", { - "body": "⚠️ **This message may not have been bridged to Discord in the way you thought it was gonna be!**\n\nIt seems like you sent this message inside a Matrix thread. Matrix threads don't work like Discord threads - they are effectively just „fancy replies”, not independent rooms/channels (any „thread-like appearance” is handled purely client-side - and even then, most Matrix clients don't handle it particularly well, with Element being the only one known to actually render threads as threads), and as such, they are bridged as replies to Discord. *In other words: __Discord users will not be aware that you sent this message inside a thread - the reply will go directly onto the main channel.__ If the thread you sent this message in is old, such a random reply **may be distracting** to Discord users!*\n\nFor the sake of Discord parity (and for better support in numerous Matrix clients - as stated above, most Matrix clients don't handle threads particularly well, and they just render in-thread messages as fancy replies), it is recommended to send threaded messages inside a separate Matrix room that gets bridged to Discord. "+ (bridgedTo ? "Luckily for you, this thread already has one! You can access it on https://matrix.to/#/"+bridgedTo+"?"+(await utils.getViaServersQuery(bridgedTo, api)).toString() : "Please run `/thread [Optional: Thread Name]` to create such a room for this thread, or get a link to it if someone else has already done so. If you run `/thread` (without any arguments) outside any threads and not as a reply, you'll get more info about this command")+".\n\n*You can read more about the rationale behind this design choice [here](https://gitdab.com/cadence/out-of-your-element/src/branch/main/docs/threads-as-rooms.md).*", - "format": "org.matrix.custom.html", - "formatted_body": "⚠️ This message may not have been bridged to Discord in the way you thought it was gonna be!

It seems like you sent this message inside a Matrix thread. Matrix threads don't work like Discord threads - they are effectively just „fancy replies”, not independent rooms/channels (any „thread-like appearance” is handled purely client-side - and even then, most Matrix clients don't handle it particularly well, with Element being the only one known to actually render threads as threads), and as such, they are bridged as replies to Discord. In other words: Discord users will not be aware that you sent this message inside a thread - the reply will go directly onto the main channel. If the thread you sent this message in is old, such a random reply may be distracting to Discord users!

For the sake of Discord parity (and for better support in numerous Matrix clients - as stated above, most Matrix clients don't handle threads particularly well, and they just render in-thread messages as fancy replies), it is recommended to send threaded messages inside a separate Matrix room that gets bridged to Discord. "+ (bridgedTo ? "Luckily for you, this thread already has one! You can access it on https://matrix.to/#/"+bridgedTo+"?"+(await utils.getViaServersQuery(bridgedTo, api)).toString()+"" : "Please run /thread [Optional: Thread Name] to create such a room for this thread, or get a link to it if someone else has already done so. If you run /thread (without any arguments) outside any threads and not as a reply, you'll get more info about this command")+".

You can read more about the rationale behind this design choice here.", + body: "⚠️ **This message may not have been bridged to Discord in the way you thought it was gonna be!**\n\nIt seems like you sent this message inside a Matrix thread. Matrix threads don't work like Discord threads - they are effectively just „fancy replies”, not independent rooms/channels (any „thread-like appearance” is handled purely client-side - and even then, most Matrix clients don't handle it particularly well, with Element being the only one known to actually render threads as threads), and as such, they are bridged as replies to Discord. *In other words: __Discord users will not be aware that you sent this message inside a thread - the reply will go directly onto the main channel.__ If the thread you sent this message in is old, such a random reply **may be distracting** to Discord users!*\n\nFor the sake of Discord parity (and for better support in numerous Matrix clients - as stated above, most Matrix clients don't handle threads particularly well, and they just render in-thread messages as fancy replies), it is recommended to send threaded messages inside a separate Matrix room that gets bridged to Discord. "+ (bridgedTo ? "Luckily for you, this thread already has one! You can access it on https://matrix.to/#/"+bridgedTo+"?"+(await utils.getViaServersQuery(bridgedTo, api)).toString() : "Please run `/thread [Optional: Thread Name]` to create such a room for this thread, or get a link to it if someone else has already done so. If you run `/thread` (without any arguments) outside any threads and not as a reply, you'll get more info about this command")+".\n\n*You can read more about the rationale behind this design choice [here](https://gitdab.com/cadence/out-of-your-element/src/branch/main/docs/threads-as-rooms.md).*", + format: "org.matrix.custom.html", + formatted_body: "⚠️ This message may not have been bridged to Discord in the way you thought it was gonna be!

It seems like you sent this message inside a Matrix thread. Matrix threads don't work like Discord threads - they are effectively just „fancy replies”, not independent rooms/channels (any „thread-like appearance” is handled purely client-side - and even then, most Matrix clients don't handle it particularly well, with Element being the only one known to actually render threads as threads), and as such, they are bridged as replies to Discord. In other words: Discord users will not be aware that you sent this message inside a thread - the reply will go directly onto the main channel. If the thread you sent this message in is old, such a random reply may be distracting to Discord users!

For the sake of Discord parity (and for better support in numerous Matrix clients - as stated above, most Matrix clients don't handle threads particularly well, and they just render in-thread messages as fancy replies), it is recommended to send threaded messages inside a separate Matrix room that gets bridged to Discord. "+ (bridgedTo ? "Luckily for you, this thread already has one! You can access it on https://matrix.to/#/"+bridgedTo+"?"+(await utils.getViaServersQuery(bridgedTo, api)).toString()+"" : "Please run /thread [Optional: Thread Name] to create such a room for this thread, or get a link to it if someone else has already done so. If you run /thread (without any arguments) outside any threads and not as a reply, you'll get more info about this command")+".

You can read more about the rationale behind this design choice here.", "m.mentions": { "user_ids": [event.sender]}, "m.relates_to": { - "event_id": event.content["m.relates_to"].event_id, - "is_falling_back": false, - "m.in_reply_to": { "event_id": event.event_id }, - "rel_type": "m.thread" + event_id: event.content["m.relates_to"].event_id, + is_falling_back: false, + "m.in_reply_to": { event_id: event.event_id }, + rel_type: "m.thread" }, - "msgtype": "m.text" + msgtype: "m.text" }) } diff --git a/src/matrix/matrix-command-handler.js b/src/matrix/matrix-command-handler.js index ff46b9c..ffb6eb1 100644 --- a/src/matrix/matrix-command-handler.js +++ b/src/matrix/matrix-command-handler.js @@ -302,9 +302,9 @@ const commands = [{ if (isFallingBack) return api.sendEvent(event.room_id, "m.room.message", { ...ctx, msgtype: "m.text", - "body": "**`/thread` usage:**\nRun this command as `/thread [Thread Name]` to create a thread on Discord (and optionally Matrix, if one doesn't exist already). The message from which said thread will branch, is chosen based on the following rules:\n* If ran stand-alone (not as a reply, nor in a Matrix thread), the created thread will branch from the command-message itself. The `Thread Name` argument must be provided in this case, otherwise you get this help message.\n* If sent as a reply (outside a Matrix thread), the thread will branch from the message to which you replied.\n* If ran inside an existing Matrix thread (regardless of whether it's a reply or not), the created Discord thread will be branching from the same message as the Matrix thread already is.", - "format": "org.matrix.custom.html", - "formatted_body": "/thread usage:
Run this command as /thread [Thread Name] to create a thread on Discord (and optionally Matrix, if one doesn't exist already). The message from which said thread will branch, is chosen based on the following rules:
  • If ran stand-alone (not as a reply, nor in a Matrix thread), the created thread will branch from the command-message itself. The Thread Name argument must be provided in this case, otherwise you get this help message.
  • If sent as a reply (outside a Matrix thread), the thread will branch from the message to which you replied.
  • If ran inside an existing Matrix thread (regardless of whether it's a reply or not), the created Discord thread will be branching from the same message as the Matrix thread already is.
" + body: "**`/thread` usage:**\nRun this command as `/thread [Thread Name]` to create a thread on Discord (and optionally Matrix, if one doesn't exist already). The message from which said thread will branch, is chosen based on the following rules:\n* If ran stand-alone (not as a reply, nor in a Matrix thread), the created thread will branch from the command-message itself. The `Thread Name` argument must be provided in this case, otherwise you get this help message.\n* If sent as a reply (outside a Matrix thread), the thread will branch from the message to which you replied.\n* If ran inside an existing Matrix thread (regardless of whether it's a reply or not), the created Discord thread will be branching from the same message as the Matrix thread already is.", + format: "org.matrix.custom.html", + formatted_body: "/thread usage:
Run this command as /thread [Thread Name] to create a thread on Discord (and optionally Matrix, if one doesn't exist already). The message from which said thread will branch, is chosen based on the following rules:
  • If ran stand-alone (not as a reply, nor in a Matrix thread), the created thread will branch from the command-message itself. The Thread Name argument must be provided in this case, otherwise you get this help message.
  • If sent as a reply (outside a Matrix thread), the thread will branch from the message to which you replied.
  • If ran inside an existing Matrix thread (regardless of whether it's a reply or not), the created Discord thread will be branching from the same message as the Matrix thread already is.
" }) } @@ -317,9 +317,9 @@ const commands = [{ case "NO_BRANCH_SOURCE": return api.sendEvent(event.room_id, "m.room.message", { ...ctx, msgtype: "m.text", - "body": "⚠️ Couldn't find a Discord representation of the message from which you're trying to branch this thread (event ID `"+e.was_supposed_to_be+"` on Matrix), so it wasn't created. Either you ran this command on an unbridged message (one sent by this bot or one that failed to bridge due to a previous error), or this is an error on our side and should be reported.", - "format": "org.matrix.custom.html", - "formatted_body": "⚠️ Couldn't find a Discord representation of the message from which you're trying to branch this thread (event ID "+e.was_supposed_to_be+" on Matrix), so it wasn't created. Either you ran this command on an unbridged message (one sent by this bot or one that failed to bridge due to a previous error), or this is an error on our side and should be reported." + body: "⚠️ Couldn't find a Discord representation of the message from which you're trying to branch this thread (event ID `"+e.was_supposed_to_be+"` on Matrix), so it wasn't created. Either you ran this command on an unbridged message (one sent by this bot or one that failed to bridge due to a previous error), or this is an error on our side and should be reported.", + format: "org.matrix.custom.html", + formatted_body: "⚠️ Couldn't find a Discord representation of the message from which you're trying to branch this thread (event ID "+e.was_supposed_to_be+" on Matrix), so it wasn't created. Either you ran this command on an unbridged message (one sent by this bot or one that failed to bridge due to a previous error), or this is an error on our side and should be reported." }) case (160004): // see: https://docs.discord.com/developers/topics/opcodes-and-status-codes const thread = mxUtils.getThreadRoomFromThreadEvent(event.event_id) From 7895f89cc0d559acff7052370c2c3e311a0f171e Mon Sep 17 00:00:00 2001 From: Guzio Date: Sat, 28 Feb 2026 11:52:18 +0000 Subject: [PATCH 37/43] debug slop v2 --- src/matrix/utils.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/matrix/utils.js b/src/matrix/utils.js index f9dbf3f..001c26d 100644 --- a/src/matrix/utils.js +++ b/src/matrix/utils.js @@ -402,7 +402,9 @@ async function setUserPowerCascade(spaceID, mxid, power, api) { * @param {string} eventID */ function getThreadRoomFromThreadEvent(eventID){ + console.log("searching for: "+eventID) const threadID = select("event_message", "message_id", {event_id: eventID}).pluck().get() //Discord thread ID === its message ID + console.log("matched to: "+threadID) if (!threadID) return threadID; return select("channel_room", "room_id", {channel_id: threadID}).pluck().get() } From 69b128a5981791d1f9223cd89bda226973431807 Mon Sep 17 00:00:00 2001 From: Guzio Date: Sat, 28 Feb 2026 13:37:16 +0000 Subject: [PATCH 38/43] debug done; turns out that I'm just stupid MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I passed a completely wrong event ID and was confused as to why could it possibly be failing. (Btw, as part od fixing that - my new function from function is utils.js now supports blank values.) Also - and that's unrelated to the bug I was debugging - I put a guard clause in my if (words.length < 2) backwards. If->return should canonically be above the logic, even if it technically doesn't break said logic in this case (all it was doing was creating an extra step that says „yea, name the newly-created thread /thread, even if this name is very stupid, and also pointless because no thread creation is about to take place”. And while fixing that, I also did some minor changes to error handling. --- src/m2d/event-dispatcher.js | 2 +- src/matrix/matrix-command-handler.js | 22 +++++++++++++++++----- src/matrix/utils.js | 7 +++---- src/matrix/utils.test.js | 15 +++++++++++++++ 4 files changed, 36 insertions(+), 10 deletions(-) diff --git a/src/m2d/event-dispatcher.js b/src/m2d/event-dispatcher.js index f3f6964..ca8fbaf 100644 --- a/src/m2d/event-dispatcher.js +++ b/src/m2d/event-dispatcher.js @@ -222,7 +222,7 @@ async event => { ) } if (event.content["m.relates_to"]?.rel_type === "m.thread" && executedCommand !== "thread"){ - const bridgedTo = utils.getThreadRoomFromThreadEvent(event.event_id) + const bridgedTo = utils.getThreadRoomFromThreadEvent(event.content["m.relates_to"].event_id) api.sendEvent(event.room_id, "m.room.message", { body: "⚠️ **This message may not have been bridged to Discord in the way you thought it was gonna be!**\n\nIt seems like you sent this message inside a Matrix thread. Matrix threads don't work like Discord threads - they are effectively just „fancy replies”, not independent rooms/channels (any „thread-like appearance” is handled purely client-side - and even then, most Matrix clients don't handle it particularly well, with Element being the only one known to actually render threads as threads), and as such, they are bridged as replies to Discord. *In other words: __Discord users will not be aware that you sent this message inside a thread - the reply will go directly onto the main channel.__ If the thread you sent this message in is old, such a random reply **may be distracting** to Discord users!*\n\nFor the sake of Discord parity (and for better support in numerous Matrix clients - as stated above, most Matrix clients don't handle threads particularly well, and they just render in-thread messages as fancy replies), it is recommended to send threaded messages inside a separate Matrix room that gets bridged to Discord. "+ (bridgedTo ? "Luckily for you, this thread already has one! You can access it on https://matrix.to/#/"+bridgedTo+"?"+(await utils.getViaServersQuery(bridgedTo, api)).toString() : "Please run `/thread [Optional: Thread Name]` to create such a room for this thread, or get a link to it if someone else has already done so. If you run `/thread` (without any arguments) outside any threads and not as a reply, you'll get more info about this command")+".\n\n*You can read more about the rationale behind this design choice [here](https://gitdab.com/cadence/out-of-your-element/src/branch/main/docs/threads-as-rooms.md).*", format: "org.matrix.custom.html", diff --git a/src/matrix/matrix-command-handler.js b/src/matrix/matrix-command-handler.js index ffb6eb1..8fbdcf3 100644 --- a/src/matrix/matrix-command-handler.js +++ b/src/matrix/matrix-command-handler.js @@ -297,8 +297,6 @@ const commands = [{ const branchedFromDiscordMessage = select("event_message", "message_id", {event_id: branchedFromMxEvent}).pluck().get() if (words.length < 2){ - words[1] = (await api.getEvent(event.room_id, branchedFromMxEvent)).content.body - words[1] = words[1].length < 100 ? words[1] : words[1].slice(0, 96) + "..." if (isFallingBack) return api.sendEvent(event.room_id, "m.room.message", { ...ctx, msgtype: "m.text", @@ -306,6 +304,8 @@ const commands = [{ format: "org.matrix.custom.html", formatted_body: "/thread usage:
Run this command as /thread [Thread Name] to create a thread on Discord (and optionally Matrix, if one doesn't exist already). The message from which said thread will branch, is chosen based on the following rules:
  • If ran stand-alone (not as a reply, nor in a Matrix thread), the created thread will branch from the command-message itself. The Thread Name argument must be provided in this case, otherwise you get this help message.
  • If sent as a reply (outside a Matrix thread), the thread will branch from the message to which you replied.
  • If ran inside an existing Matrix thread (regardless of whether it's a reply or not), the created Discord thread will be branching from the same message as the Matrix thread already is.
" }) + words[1] = (await api.getEvent(event.room_id, branchedFromMxEvent)).content.body + words[1] = words[1].length < 100 ? words[1] : words[1].slice(0, 96) + "..." } try { @@ -321,28 +321,40 @@ const commands = [{ format: "org.matrix.custom.html", formatted_body: "⚠️ Couldn't find a Discord representation of the message from which you're trying to branch this thread (event ID "+e.was_supposed_to_be+" on Matrix), so it wasn't created. Either you ran this command on an unbridged message (one sent by this bot or one that failed to bridge due to a previous error), or this is an error on our side and should be reported." }) + case (160004): // see: https://docs.discord.com/developers/topics/opcodes-and-status-codes - const thread = mxUtils.getThreadRoomFromThreadEvent(event.event_id) + if (isFallingBack){ + await api.sendEvent(event.room_id, "m.room.message", { + ...ctx, + msgtype: "m.text", + body: "⚠️ Discord claims that there already exists a thread for the message you ran this command on, but that doesn't make logical sense, as it doesn't seem like you ran this command on any message. Either your Matrix client did something funny with reply/thread tags, or this is a logic error on OOYE's side. At any rate, this should be reported for further investigation. you should also attach the error message that's about to be sent below (or on the main room timeline, if the command was ran inside a thread).", + }) + throw e; + } + const thread = mxUtils.getThreadRoomFromThreadEvent(branchedFromMxEvent) return api.sendEvent(event.room_id, "m.room.message", { ...ctx, msgtype: "m.text", - body: "There already exist a Discord thread for the message you ran this command on" + (thread ? " - you may join its bridged room here: https://matrix.to/#/"+thread+"?"+(await mxUtils.getViaServersQuery(thread, api)).toString() : ", so a new one cannot be crated. However, it seems like that thread isn't bridged to any Matrix rooms. Please ask the space/server admins to rectify this issue (or have someone send a message in that thread on Discord side to bridge it automatically, if that's enabled for this space/server).") + body: "There already exists a Discord thread for the message you ran this command on" + (thread ? " - you may join its bridged room here: https://matrix.to/#/"+thread+"?"+(await mxUtils.getViaServersQuery(thread, api)).toString() : ", so a new one cannot be crated. However, it seems like that thread isn't bridged to any Matrix rooms. Please ask the space/server admins to rectify this issue (or have someone send a message in that thread on Discord side to bridge it automatically, if that's enabled for this space/server) by manually creating the bridge. (If you're said admin and you can see that said bridge already exists, but this message is still showing up, please report that as a bug.)") }) + case (50024): return api.sendEvent(event.room_id, "m.room.message", { ...ctx, msgtype: "m.text", body: "You cannot create threads in a Discord channel of the type, to which this Matrix room is bridged to. Did you try to create a thread inside a thread?" }) + case (50035): return api.sendEvent(event.room_id, "m.room.message", { ...ctx, msgtype: "m.text", body: "Specified thread name is too long - thread creation failed. Please yap a bit less in the title, the thread body is for that. ;)" }) + default: await api.sendEvent(event.room_id, "m.room.message", { ...ctx, msgtype: "m.text", - body: "Unknown error occurred during thread creation. See error message below (or on the main channel, if the command was ran inside a thread) for details." + body: "⚠️ Unknown error occurred during thread creation. See error message below (or on the main room timeline, if the command was ran inside a thread) for details." }) throw e } diff --git a/src/matrix/utils.js b/src/matrix/utils.js index 001c26d..6383c4d 100644 --- a/src/matrix/utils.js +++ b/src/matrix/utils.js @@ -399,12 +399,11 @@ async function setUserPowerCascade(spaceID, mxid, power, api) { } /** - * @param {string} eventID - */ + * @param {undefined|string?} eventID + */ //^For some reason, „?” doesn't include Undefined and it needs to be explicitly specified function getThreadRoomFromThreadEvent(eventID){ - console.log("searching for: "+eventID) + if (!eventID) return eventID; const threadID = select("event_message", "message_id", {event_id: eventID}).pluck().get() //Discord thread ID === its message ID - console.log("matched to: "+threadID) if (!threadID) return threadID; return select("channel_room", "room_id", {channel_id: threadID}).pluck().get() } diff --git a/src/matrix/utils.test.js b/src/matrix/utils.test.js index f42ce5c..8db998d 100644 --- a/src/matrix/utils.test.js +++ b/src/matrix/utils.test.js @@ -436,4 +436,19 @@ test("getThreadRoomFromThreadEvent: fake message", t => { else t.pass(msg) }) +test("getThreadRoomFromThreadEvent: null", t => { + const room = getThreadRoomFromThreadEvent(null) + t.equal(room, null) +}) + +test("getThreadRoomFromThreadEvent: undefined", t => { + const room = getThreadRoomFromThreadEvent(undefined) + t.equal(room, undefined) +}) + +test("getThreadRoomFromThreadEvent: no value at all", t => { + const room = getThreadRoomFromThreadEvent() //This line should be giving a type-error, so it's not @ts-ignored on purpose. This is to test the desired behavior of that function, ie. „it CAN TAKE an undefined VALUE (as tested above), but you can just LEAVE the value completely undefined” (well, you can leave it like that from JS syntax perspective (which is why this test passes), but it makes no sense from usage standpoint, as it just gives back undefined). So this isn't a logic test (that's handled above), as much as it is a TypeScript test. + t.equal(room, undefined) +}) + module.exports.mockGetEffectivePower = mockGetEffectivePower From 8260396254fcf2ac454fbdc52db11be86e6a4233 Mon Sep 17 00:00:00 2001 From: Guzio Date: Sat, 28 Feb 2026 14:14:27 +0000 Subject: [PATCH 39/43] replace newlines instead of stripping them (what DC does by default) --- src/matrix/matrix-command-handler.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/matrix/matrix-command-handler.js b/src/matrix/matrix-command-handler.js index 8fbdcf3..a0b2adf 100644 --- a/src/matrix/matrix-command-handler.js +++ b/src/matrix/matrix-command-handler.js @@ -304,7 +304,7 @@ const commands = [{ format: "org.matrix.custom.html", formatted_body: "/thread usage:
Run this command as /thread [Thread Name] to create a thread on Discord (and optionally Matrix, if one doesn't exist already). The message from which said thread will branch, is chosen based on the following rules:
  • If ran stand-alone (not as a reply, nor in a Matrix thread), the created thread will branch from the command-message itself. The Thread Name argument must be provided in this case, otherwise you get this help message.
  • If sent as a reply (outside a Matrix thread), the thread will branch from the message to which you replied.
  • If ran inside an existing Matrix thread (regardless of whether it's a reply or not), the created Discord thread will be branching from the same message as the Matrix thread already is.
" }) - words[1] = (await api.getEvent(event.room_id, branchedFromMxEvent)).content.body + words[1] = (await api.getEvent(event.room_id, branchedFromMxEvent)).content.body.replaceAll("\n", " ") words[1] = words[1].length < 100 ? words[1] : words[1].slice(0, 96) + "..." } From a877122ef637bbaa9f80486d675a2cde06118e4d Mon Sep 17 00:00:00 2001 From: Guzio Date: Mon, 2 Mar 2026 13:06:30 +0000 Subject: [PATCH 40/43] fix one more final UX pet-peeve --- src/m2d/event-dispatcher.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/m2d/event-dispatcher.js b/src/m2d/event-dispatcher.js index ca8fbaf..5000a8c 100644 --- a/src/m2d/event-dispatcher.js +++ b/src/m2d/event-dispatcher.js @@ -226,7 +226,7 @@ async event => { api.sendEvent(event.room_id, "m.room.message", { body: "⚠️ **This message may not have been bridged to Discord in the way you thought it was gonna be!**\n\nIt seems like you sent this message inside a Matrix thread. Matrix threads don't work like Discord threads - they are effectively just „fancy replies”, not independent rooms/channels (any „thread-like appearance” is handled purely client-side - and even then, most Matrix clients don't handle it particularly well, with Element being the only one known to actually render threads as threads), and as such, they are bridged as replies to Discord. *In other words: __Discord users will not be aware that you sent this message inside a thread - the reply will go directly onto the main channel.__ If the thread you sent this message in is old, such a random reply **may be distracting** to Discord users!*\n\nFor the sake of Discord parity (and for better support in numerous Matrix clients - as stated above, most Matrix clients don't handle threads particularly well, and they just render in-thread messages as fancy replies), it is recommended to send threaded messages inside a separate Matrix room that gets bridged to Discord. "+ (bridgedTo ? "Luckily for you, this thread already has one! You can access it on https://matrix.to/#/"+bridgedTo+"?"+(await utils.getViaServersQuery(bridgedTo, api)).toString() : "Please run `/thread [Optional: Thread Name]` to create such a room for this thread, or get a link to it if someone else has already done so. If you run `/thread` (without any arguments) outside any threads and not as a reply, you'll get more info about this command")+".\n\n*You can read more about the rationale behind this design choice [here](https://gitdab.com/cadence/out-of-your-element/src/branch/main/docs/threads-as-rooms.md).*", format: "org.matrix.custom.html", - formatted_body: "⚠️ This message may not have been bridged to Discord in the way you thought it was gonna be!

It seems like you sent this message inside a Matrix thread. Matrix threads don't work like Discord threads - they are effectively just „fancy replies”, not independent rooms/channels (any „thread-like appearance” is handled purely client-side - and even then, most Matrix clients don't handle it particularly well, with Element being the only one known to actually render threads as threads), and as such, they are bridged as replies to Discord. In other words: Discord users will not be aware that you sent this message inside a thread - the reply will go directly onto the main channel. If the thread you sent this message in is old, such a random reply may be distracting to Discord users!

For the sake of Discord parity (and for better support in numerous Matrix clients - as stated above, most Matrix clients don't handle threads particularly well, and they just render in-thread messages as fancy replies), it is recommended to send threaded messages inside a separate Matrix room that gets bridged to Discord. "+ (bridgedTo ? "Luckily for you, this thread already has one! You can access it on https://matrix.to/#/"+bridgedTo+"?"+(await utils.getViaServersQuery(bridgedTo, api)).toString()+"" : "Please run /thread [Optional: Thread Name] to create such a room for this thread, or get a link to it if someone else has already done so. If you run /thread (without any arguments) outside any threads and not as a reply, you'll get more info about this command")+".

You can read more about the rationale behind this design choice here.", + formatted_body: "⚠️ This message may not have been bridged to Discord in the way you thought it was gonna be!

It seems like you sent this message inside a Matrix thread. Matrix threads don't work like Discord threads - they are effectively just „fancy replies”, not independent rooms/channels (any „thread-like appearance” is handled purely client-side - and even then, most Matrix clients don't handle it particularly well, with Element being the only one known to actually render threads as threads), and as such, they are bridged as replies to Discord. In other words: Discord users will not be aware that you sent this message inside a thread - the reply will go directly onto the main channel. If the thread you sent this message in is old, such a random reply may be distracting to Discord users!

For the sake of Discord parity (and for better support in numerous Matrix clients - as stated above, most Matrix clients don't handle threads particularly well, and they just render in-thread messages as fancy replies), it is recommended to send threaded messages inside a separate Matrix room that gets bridged to Discord. "+ (bridgedTo ? "Luckily for you, this thread already has one! You can access it on "+bridgedTo+"" : "Please run /thread [Optional: Thread Name] to create such a room for this thread, or get a link to it if someone else has already done so. If you run /thread (without any arguments) outside any threads and not as a reply, you'll get more info about this command")+".

You can read more about the rationale behind this design choice here.", "m.mentions": { "user_ids": [event.sender]}, "m.relates_to": { event_id: event.content["m.relates_to"].event_id, From 20ce420303486a1f05316baa17eba5c5ec10eb6a Mon Sep 17 00:00:00 2001 From: Guzio Date: Mon, 2 Mar 2026 13:12:17 +0000 Subject: [PATCH 41/43] copy-pasted Cadence's message as documentation --- docs/threads-as-rooms.md | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 docs/threads-as-rooms.md diff --git a/docs/threads-as-rooms.md b/docs/threads-as-rooms.md new file mode 100644 index 0000000..012c7af --- /dev/null +++ b/docs/threads-as-rooms.md @@ -0,0 +1,9 @@ +I thought pretty hard about it and I opted to make threads separate rooms because + +1. parity: discord has separate things like permissions and pins for threads, matrix cannot do this at all unless the thread is a separate room +2. usage styles: most discord threads I've seen tend to be long-lived, spanning months or years, which isn't suited to matrix because of the timeline + - I'm in a discord thread for posting photos of food that gets a couple posts a week and has a timeline going back to 2023 +3. the timeline: if a matrix room has threads, and you want to scroll back through the timeline of a room OR of one of its threads, the timeline is merged, so you have to download every message linearised and throw them away if they aren't part of the thread you're looking through. it's bad for threads and it's bad for the main room +4. it is also very very complex for clients to implement read receipts and typing indicators correctly for the merged timeline. if your client doesn't implement this, or doesn't do it correctly, you have a bad experience. many clients don't. element seems to have done it well enough, but is an exception + +overall in my view, threads-as-rooms has better parity and fewer downsides over native threads. but if there are things you don't like about this approach, I'm happy to discuss and see if we can improve them. \ No newline at end of file From b84b848d04528dec764310307f1e11941e288445 Mon Sep 17 00:00:00 2001 From: Guzio Date: Mon, 2 Mar 2026 13:15:29 +0000 Subject: [PATCH 42/43] temporary change to VSC settings So that I can squash-merge it all without leaving the trace of any extra unsolicited changes --- .vscode/settings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index ea571e8..9f1e183 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,5 @@ { "editor.insertSpaces": false, "editor.detectIndentation": false, - "editor.tabSize": 4 + "editor.tabSize": 3 } From b38abe81a6ab581c3759a345c8e3cf0e2fbbe2fe Mon Sep 17 00:00:00 2001 From: Guzio Date: Mon, 2 Mar 2026 13:44:36 +0000 Subject: [PATCH 43/43] reworded the error Turns out that auto-create is ALWAYS on for threads (which creates some hilarious situations, where the channel gets duplicated if it ever got unbridged). Also, manual bridging isn't even possible. Uhh... Sure! Let's just say, then, that it's the admin's problem to auto-create it (given the duplication - this is probably a better idea to leave it to them). A proper fix for this (and also to limit (tho not fix) the dupe-by-autocreate problem) would probably be to allow for manual bridging on threads, but I really don't have time for this before The Merge (my ADHD is kicking-in on this update, and I have a feeling that if I don't PR soon, I'm gonna not do it for another 3 months). --- src/matrix/matrix-command-handler.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/matrix/matrix-command-handler.js b/src/matrix/matrix-command-handler.js index a0b2adf..6758f78 100644 --- a/src/matrix/matrix-command-handler.js +++ b/src/matrix/matrix-command-handler.js @@ -335,7 +335,7 @@ const commands = [{ return api.sendEvent(event.room_id, "m.room.message", { ...ctx, msgtype: "m.text", - body: "There already exists a Discord thread for the message you ran this command on" + (thread ? " - you may join its bridged room here: https://matrix.to/#/"+thread+"?"+(await mxUtils.getViaServersQuery(thread, api)).toString() : ", so a new one cannot be crated. However, it seems like that thread isn't bridged to any Matrix rooms. Please ask the space/server admins to rectify this issue (or have someone send a message in that thread on Discord side to bridge it automatically, if that's enabled for this space/server) by manually creating the bridge. (If you're said admin and you can see that said bridge already exists, but this message is still showing up, please report that as a bug.)") + body: "There already exists a Discord thread for the message you ran this command on" + (thread ? " - you may join its bridged room here: https://matrix.to/#/"+thread+"?"+(await mxUtils.getViaServersQuery(thread, api)).toString() : ", so a new one cannot be crated. However, it seems like that thread isn't bridged to any Matrix rooms. Please ask the space/server admins to rectify this issue by creating the bridge. (If you're said admin and you can see that said bridge already exists, but this error message is still showing up, please report that as a bug.)") }) case (50024): return api.sendEvent(event.room_id, "m.room.message", {