Compare commits

...
Sign in to create a new pull request.

5 commits

7 changed files with 76 additions and 34 deletions

View file

@ -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.

View file

@ -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<Ty.PkMessage>} */
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)}`)

View file

@ -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<boolean>} if true then the message did not arrive
* @returns {Promise<boolean>} 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<boolean>} if true then the message did not arrive
* @returns {Promise<boolean>} 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<boolean>} if true then the message did not arrive
* @returns {Promise<boolean>} if false then the message did not arrive
*/
function waitForReactionEvent(eventID) {
const {promise, resolve} = Promise.withResolvers()

View file

@ -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)
})
}

View file

@ -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<string> | 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

View file

@ -153,7 +153,7 @@ async function sendEvent(event) {
channel_id: messageResponse.channel_id,
guild_id: guild.id,
embeds: messageResponse.embeds
}, guild, null)
}, guild)
)
}
}

View file

@ -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