From 7faa8a3c9e29bbef93bf2e7c31c76c6011ef9167 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Fri, 11 Sep 2026 12:47:15 +1200 Subject: [PATCH 1/5] Remove unwise nextTick delay --- src/d2m/discord-client.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/d2m/discord-client.js b/src/d2m/discord-client.js index b9b45e2..408c21c 100644 --- a/src/d2m/discord-client.js +++ b/src/d2m/discord-client.js @@ -55,9 +55,7 @@ class DiscordClient { this.guildChannelMap = new Map() if (listen !== "no") { this.cloud.on("event", message => { - process.nextTick(() => { - discordPackets.onPacket(this, message, listen) - }) + discordPackets.onPacket(this, message, listen) }) } From 5e9f3cd532e325f869b26d3a8e74c011b1415b40 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Fri, 11 Sep 2026 12:51:29 +1200 Subject: [PATCH 2/5] Add retries back to PluralKit API --- src/d2m/actions/register-pk-user.js | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/d2m/actions/register-pk-user.js b/src/d2m/actions/register-pk-user.js index e68d2cc..bb8335b 100644 --- a/src/d2m/actions/register-pk-user.js +++ b/src/d2m/actions/register-pk-user.js @@ -3,6 +3,7 @@ const assert = require("assert").strict const {reg} = require("../../matrix/read-registration") const Ty = require("../../types") +const {scheduler} = require("timers/promises") const passthrough = require("../../passthrough") const {sync, db, select, from} = passthrough @@ -14,14 +15,21 @@ const file = sync.require("../../matrix/file") const registerUser = sync.require("./register-user") /** @returns {Promise} */ -async function fetchMessage(messageID) { +async function fetchMessage(messageID, attempt = 1) { try { var res = await fetch(`https://api.pluralkit.me/v2/messages/${messageID}`) } catch (networkError) { // Network issue, raise a more readable message throw new Error(`Failed to connect to PK API: ${networkError.toString()}`) } - if (!res.ok) throw new Error(`PK API returned an error: ${await res.text()}`) + if (!res.ok) { + if (attempt < 2) { + await scheduler.wait(5000) + return fetchMessage(messageID, attempt + 1) + } else { + throw new Error(`PK API returned an error: ${await res.text()}`) + } + } /** @type {any} */ const root = await res.json() if (!root.member) throw new Error(`PK API didn't return member data: ${JSON.stringify(root)}`) From cf95f7d915cc9edf859b397de22fdf1fd11fba8e Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Fri, 11 Sep 2026 12:57:46 +1200 Subject: [PATCH 3/5] Add AI policy --- readme.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/readme.md b/readme.md index e8a8e7e..deb98da 100644 --- a/readme.md +++ b/readme.md @@ -8,6 +8,10 @@ Modern Matrix-to-Discord appservice bridge, created by [@cadence:cadence.moe](ht ![](https://cadence.moe/i/f42a3f) +## AI policy + +Out Of Your Element is 100% organically cultivated ethical hand-fed code. Let's work together to conserve that! + ## Why a new bridge? * Modern: Supports new Discord features like replies, threads and stickers, and new Matrix features like edits, spaces and space membership. From 965fcab646dd3fb1eb1f63994a364541d8ba3719 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Fri, 11 Sep 2026 20:07:36 +1200 Subject: [PATCH 4/5] m->d edits now wait for original message --- src/d2m/actions/retrigger.js | 10 +++++----- src/m2d/actions/send-event.js | 2 +- src/m2d/converters/event-to-message.js | 3 +++ 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/d2m/actions/retrigger.js b/src/d2m/actions/retrigger.js index 43f400d..d7ac1a6 100644 --- a/src/d2m/actions/retrigger.js +++ b/src/d2m/actions/retrigger.js @@ -41,7 +41,7 @@ const storage = new class { this.timers.set(id, setTimeout(() => this.resolve(id, false), 60 * 1000).unref()) // 1 minute } } - + /** @param {string} id */ isNotPaused(id) { return !storage.paused.has(id) @@ -98,7 +98,7 @@ function waitFor(id, resolve, existsInDatabase) { const GET_EVENT_PREPARED = from("event_message").select("event_id").and("WHERE event_id = ?").prepare().raw() /** * @param {string} eventID - * @returns {Promise} if true then the message did not arrive + * @returns {Promise} if false then the message did not arrive */ function waitForEvent(eventID) { const {promise, resolve} = Promise.withResolvers() @@ -109,7 +109,7 @@ function waitForEvent(eventID) { const GET_MESSAGE_PREPARED = from("event_message").select("message_id").and("WHERE message_id = ?").prepare().raw() /** * @param {string} messageID - * @returns {Promise} if true then the message did not arrive + * @returns {Promise} if false then the message did not arrive */ function waitForMessage(messageID) { const {promise, resolve} = Promise.withResolvers() @@ -120,7 +120,7 @@ function waitForMessage(messageID) { const GET_REACTION_EVENT_PREPARED = from("reaction").select("hashed_event_id").and("WHERE hashed_event_id = ?").prepare().raw() /** * @param {string} eventID - * @returns {Promise} if true then the message did not arrive + * @returns {Promise} if false then the message did not arrive */ function waitForReactionEvent(eventID) { const {promise, resolve} = Promise.withResolvers() @@ -156,4 +156,4 @@ module.exports.waitForMessage = waitForMessage module.exports.waitForEvent = waitForEvent module.exports.waitForReactionEvent = waitForReactionEvent module.exports.pauseChanges = pauseChanges -module.exports.finishedBridging = finishedBridging \ No newline at end of file +module.exports.finishedBridging = finishedBridging diff --git a/src/m2d/actions/send-event.js b/src/m2d/actions/send-event.js index 00557a1..2e6bf5c 100644 --- a/src/m2d/actions/send-event.js +++ b/src/m2d/actions/send-event.js @@ -153,7 +153,7 @@ async function sendEvent(event) { channel_id: messageResponse.channel_id, guild_id: guild.id, embeds: messageResponse.embeds - }, guild, null) + }, guild) ) } } diff --git a/src/m2d/converters/event-to-message.js b/src/m2d/converters/event-to-message.js index c014a3e..a017e99 100644 --- a/src/m2d/converters/event-to-message.js +++ b/src/m2d/converters/event-to-message.js @@ -32,6 +32,8 @@ const setupEmojis = sync.require("../actions/setup-emojis") const userToMxid = sync.require("../../d2m/converters/user-to-mxid") /** @type {import("../../web/routes/letter-avatar")} */ const letterAvatar = sync.require("../../web/routes/letter-avatar") +/** @type {import("../../d2m/actions/retrigger")} */ +const retrigger = sync.require("../../d2m/actions/retrigger") /** @type {[RegExp, string][]} */ const markdownEscapes = [ @@ -696,6 +698,7 @@ async function eventToMessage(event, guild, channel, di) { // Check if we have a pointer to what was edited const originalEventId = relatesTo.event_id if (!originalEventId) return + if (!await retrigger.waitForEvent(originalEventId)) return messageIDsToEdit = select("event_message", "message_id", {event_id: originalEventId}, "ORDER BY part").pluck().all() if (!messageIDsToEdit.length) return From c71120e463a43c113338265d1d1e902bfdb0f935 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Fri, 11 Sep 2026 23:27:39 +1200 Subject: [PATCH 5/5] Better Discord startup checking --- src/d2m/discord-packets.js | 75 ++++++++++++++++++++++++++------------ 1 file changed, 52 insertions(+), 23 deletions(-) diff --git a/src/d2m/discord-packets.js b/src/d2m/discord-packets.js index 988f981..992689f 100644 --- a/src/d2m/discord-packets.js +++ b/src/d2m/discord-packets.js @@ -1,6 +1,6 @@ // @ts-check -const assert = require("assert") +const assert = require("assert").strict const {scheduler} = require("timers/promises") const passthrough = require("../passthrough") const {sync} = passthrough @@ -8,7 +8,32 @@ const {sync} = passthrough /** @type {import("../matrix/homeserver-status")} */ const homeserverStatus = sync.require("../matrix/homeserver-status") -let checkedHomeserver = false +const guildReadyStatus = new class { + /** @type {Set | null} */ + unavailableGuilds = null + _allReady = Promise.withResolvers() + + /** + * @param {string} guildID + * @returns {boolean} true if it was the last one + */ + makeReady(guildID) { + assert(this.unavailableGuilds) + if (this.unavailableGuilds.delete(guildID) && this.unavailableGuilds.size === 0) { + this._allReady.resolve(null) + return true + } + return false + } + + allReady() { + return this.unavailableGuilds && this.unavailableGuilds.size === 0 + } + + waitForAllReady() { + return this._allReady.promise + } +} /** * @param {import("./discord-client")} client @@ -28,12 +53,27 @@ async function onPacket(client, message, listen) { client.ready = true client.user = message.d.user client.application = message.d.application + guildReadyStatus.unavailableGuilds = new Set(message.d.guilds.filter(g => g.unavailable).map(g => g.id)) console.log(`Discord logged in as ${client.user.username}#${client.user.discriminator} (${client.user.id})`) + process.stdout.write("Waiting for guilds to warm up... ") interactions.registerInteractions() } else if (message.t === "GUILD_CREATE") { - message.d.members = message.d.members.filter(m => m.user.id === client.user.id) // only keep the bot's own member - it's needed to determine private channels on web + message.d.members = message.d.members.filter(m => m.user.id === client.user.id) // only keep the bot account's member - it's needed for roles to determine private channels on web client.guilds.set(message.d.id, message.d) + + /* + Info about guilds is populated one guild at a time. + For m->d bridging to work, the guild needs to be populated, so we need to have GUILD_CREATE for the guild. + If we ping the homeserver, it will send us any pending events, so we need to wait for all GUILD_CREATES before we ping. + We must attempt a ping because we don't want to try sending missed d->m messages to an offline homeserver. + The "all guilds ready" delay can be removed if ONE of the following is done: + 1. m->d can queue incoming events until their guild exists in memory + 2. d->m missed messages can have their errors handled and added to queue, rather than pinging first + */ + const firstReady = guildReadyStatus.allReady() + const lastGuildReady = guildReadyStatus.makeReady(message.d.id) + const arr = [] client.guildChannelMap.set(message.d.id, arr) for (const channel of message.d.channels || []) { @@ -51,28 +91,14 @@ async function onPacket(client, message, listen) { if (listen === "full") { try { - /* - Info about guilds is populated one guild at a time. - For m->d bridging to work, the guild needs to be populated, so we need to have GUILD_CREATE for the guild. - If we ping the homeserver, it will send us any pending events, so we need to wait for all GUILD_CREATES before we ping. - We must attempt a ping because we don't want to try sending missed d->m messages to an offline homeserver. - This delay can be removed if ONE of the following is done: - 1. m->d can queue incoming events until their guild exists in memory - 2. d->m missed messages can have their errors handled and added to queue, rather than pinging first - */ - let isMainCharacter = false - if (!checkedHomeserver) { - checkedHomeserver = true - isMainCharacter = true - console.log("Warming up guilds~") - } - await scheduler.wait(5000) - if (isMainCharacter) { - checkedHomeserver = true - process.stdout.write("Connecting to homeserver... ") + // Wait for guilds to be connected and homeserver to be online. If this is the last guild, a different code path is used to trigger the homeserver check. + if (lastGuildReady) { + process.stdout.write(`ok, ${client.guilds.size} available.\nConnecting to homeserver... `) + // await guildReadyStatus.waitForAllReady() - no need, we already checked this is the last guild await homeserverStatus.homeserverStatus.waitForOnline(true) console.log("ok.\nReplaying past events. Welcome to Out Of Your Element.") } else { + await guildReadyStatus.waitForAllReady() await homeserverStatus.homeserverStatus.waitForOnline(false) } @@ -81,7 +107,9 @@ async function onPacket(client, message, listen) { await eventDispatcher.checkMissedPins(client, message.d) await eventDispatcher.checkMissedLeaves(client, message.d) } catch (e) { - console.error("Failed to sync missed events. To retry, please fix this error and restart OOYE:") + if (firstReady) { + console.error("Failed to sync missed events. To retry, please fix this error and restart OOYE:") + } console.error(e) } } @@ -225,3 +253,4 @@ async function dispatchPacketToBridge(client, message) { module.exports.onPacket = onPacket module.exports.dispatchPacketToBridge = dispatchPacketToBridge +module.exports.guildReadyStatus = guildReadyStatus