diff --git a/docs/developer-orientation.md b/docs/developer-orientation.md index 0f5e0c0..bff64c2 100644 --- a/docs/developer-orientation.md +++ b/docs/developer-orientation.md @@ -113,7 +113,6 @@ Total transitive production dependencies: 144 * (0) ansi-colors: Helps with interactive prompting for the initial setup, and it's already pulled in by enquirer. * (1) chunk-text: It does what I want. * (0) cloudstorm: Discord gateway library with bring-your-own-caching that I trust. -* (0) denque: Much faster than using arrays as a queue. * (0) discord-api-types: Bitfields needed at runtime and types needed for development. * (0) domino: DOM implementation that's already pulled in by turndown. * (2) enquirer: Interactive prompting for the initial setup rather than forcing users to edit YAML non-interactively. diff --git a/package-lock.json b/package-lock.json index 04dbbe4..3a3a893 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,8 +24,7 @@ "ansi-colors": "^4.1.3", "better-sqlite3": "^12.2.0", "chunk-text": "^2.0.1", - "cloudstorm": "^0.19.0", - "denque": "^2.1.0", + "cloudstorm": "^0.17.1", "discord-api-types": "^0.38.38", "domino": "^2.1.6", "enquirer": "^2.4.1", @@ -38,7 +37,7 @@ "mime-types": "^2.1.35", "prettier-bytes": "^1.0.4", "sharp": "^0.34.5", - "snowtransfer": "^0.19.0", + "snowtransfer": "^0.17.5", "try-to-catch": "^4.0.5", "uqr": "^0.1.2", "xxhash-wasm": "^1.0.2", @@ -1317,13 +1316,13 @@ } }, "node_modules/cloudstorm": { - "version": "0.19.0", - "resolved": "https://registry.npmjs.org/cloudstorm/-/cloudstorm-0.19.0.tgz", - "integrity": "sha512-mjuP5o2nS/CgSljajh08KjN6jYMfNBBttSPHc1YUN74f7OkmhUhv8QE8JFDXomxCfdnLNJcqBQtjRLrs8nkfVQ==", + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/cloudstorm/-/cloudstorm-0.17.1.tgz", + "integrity": "sha512-LYUwzHagRYRd93XocOqi+HCHdzPYI9cW7Yf7pYqinxgG+Qka1OiqBKWTCcLiEuiqXaOV30kr8c6aZ/c1QcDP4Q==", "license": "MIT", "dependencies": { - "discord-api-types": "^0.38.49", - "snowtransfer": "^0.19.0" + "discord-api-types": "^0.38.47", + "snowtransfer": "^0.17.5" }, "engines": { "node": ">=22.0.0" @@ -1458,15 +1457,6 @@ "integrity": "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==", "license": "MIT" }, - "node_modules/denque": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", - "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", - "license": "Apache-2.0", - "engines": { - "node": ">=0.10" - } - }, "node_modules/destr": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz", @@ -1483,9 +1473,9 @@ } }, "node_modules/discord-api-types": { - "version": "0.38.49", - "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.38.49.tgz", - "integrity": "sha512-XnqcWmnFZFAE8ZM8SHAw9DIV8D3Or00rMQ8iQLotrEA2PmXhl+ykaf6L6q4l474hrSUH1JaYcv+iOMRWp2p6Tg==", + "version": "0.38.47", + "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.38.47.tgz", + "integrity": "sha512-XgXQodHQBAE6kfD7kMvVo30863iHX1LHSqNq6MGUTDwIFCCvHva13+rwxyxVXDqudyApMNAd32PGjgVETi5rjA==", "license": "MIT", "workspaces": [ "scripts/actions/documentation" @@ -2589,12 +2579,12 @@ } }, "node_modules/snowtransfer": { - "version": "0.19.0", - "resolved": "https://registry.npmjs.org/snowtransfer/-/snowtransfer-0.19.0.tgz", - "integrity": "sha512-Vebj4FCdpUsEKAzqC1p50ol5o09CLnrffvBCpTv0FNFefvXPXwogxR8kr4zJApIRt7oWQVZZVkxAq4XnGlICjQ==", + "version": "0.17.7", + "resolved": "https://registry.npmjs.org/snowtransfer/-/snowtransfer-0.17.7.tgz", + "integrity": "sha512-scbOjYezo1Ycfk21atCEkeXIISTT7R7JTHCdiZ/7m7k4XbSb6o5q8Mu2fev5IqFpNyqIVjA0d/MZQ+eP/gtwfg==", "license": "MIT", "dependencies": { - "discord-api-types": "^0.38.49" + "discord-api-types": "^0.38.47" }, "engines": { "node": ">=22.0.0" diff --git a/package.json b/package.json index c086517..769c677 100644 --- a/package.json +++ b/package.json @@ -33,8 +33,7 @@ "ansi-colors": "^4.1.3", "better-sqlite3": "^12.2.0", "chunk-text": "^2.0.1", - "cloudstorm": "^0.19.0", - "denque": "^2.1.0", + "cloudstorm": "^0.17.1", "discord-api-types": "^0.38.38", "domino": "^2.1.6", "enquirer": "^2.4.1", @@ -47,7 +46,7 @@ "mime-types": "^2.1.35", "prettier-bytes": "^1.0.4", "sharp": "^0.34.5", - "snowtransfer": "^0.19.0", + "snowtransfer": "^0.17.5", "try-to-catch": "^4.0.5", "uqr": "^0.1.2", "xxhash-wasm": "^1.0.2", diff --git a/scripts/setup.js b/scripts/setup.js index 6ce4c9f..69b62a2 100644 --- a/scripts/setup.js +++ b/scripts/setup.js @@ -359,7 +359,7 @@ function defineEchoHandler() { console.log("✅ Emojis are ready...") // set profile data on discord... - await discord.snow.user.editSelf({avatar: "data:image/png;base64," + avatarBuffer.toString("base64")}) + await discord.snow.user.updateSelf({avatar: "data:image/png;base64," + avatarBuffer.toString("base64")}) console.log("✅ Discord profile updated...") // set profile data on homeserver... diff --git a/src/d2m/actions/create-space.js b/src/d2m/actions/create-space.js index 403fdb2..7a751e2 100644 --- a/src/d2m/actions/create-space.js +++ b/src/d2m/actions/create-space.js @@ -8,8 +8,6 @@ const {reg} = require("../../matrix/read-registration") const passthrough = require("../../passthrough") const {discord, sync, db, select} = passthrough -/** @type {import("../../matrix/mreq")} */ -const mreq = sync.require("../../matrix/mreq") /** @type {import("../../matrix/api")} */ const api = sync.require("../../matrix/api") /** @type {import("../../matrix/file")} */ @@ -239,12 +237,8 @@ async function syncSpaceExpressions(data, checkBeforeSync) { try { existing = await api.getStateEvent(spaceID, "im.ponies.room_emotes", eventKey) } catch (e) { - if (e instanceof mreq.MatrixServerError && e.httpStatus < 400) { - // State event not found. This space doesn't have any existing emojis. We create a dummy empty event for comparison's sake. - existing = fn([], guild) - } else { - throw e - } + // State event not found. This space doesn't have any existing emojis. We create a dummy empty event for comparison's sake. + existing = fn([], guild) } if (isDeepStrictEqual(existing, content)) return } diff --git a/src/d2m/discord-client.js b/src/d2m/discord-client.js index b9b45e2..9f1d1ff 100644 --- a/src/d2m/discord-client.js +++ b/src/d2m/discord-client.js @@ -14,8 +14,6 @@ const {sync} = passthrough /** @type {import("./discord-packets")} */ const discordPackets = sync.require("./discord-packets") -const CONNECTION_DEBUG = false - class DiscordClient { /** * @param {string} discordToken @@ -61,17 +59,15 @@ class DiscordClient { }) } - if (CONNECTION_DEBUG) { - const addEventLogger = (eventName, logName) => { - this.cloud.on(eventName, (...args) => { - const d = new Date().toISOString().slice(0, 19) - console.error(`[${d} Client ${logName}]`, ...args) - }) - } - addEventLogger("error", "Error") - addEventLogger("disconnected", "Disconnected") - addEventLogger("ready", "Ready") + const addEventLogger = (eventName, logName) => { + this.cloud.on(eventName, (...args) => { + const d = new Date().toISOString().slice(0, 19) + console.error(`[${d} Client ${logName}]`, ...args) + }) } + addEventLogger("error", "Error") + addEventLogger("disconnected", "Disconnected") + addEventLogger("ready", "Ready") } } diff --git a/src/d2m/discord-packets.js b/src/d2m/discord-packets.js index b17ad4e..afea9ea 100644 --- a/src/d2m/discord-packets.js +++ b/src/d2m/discord-packets.js @@ -1,222 +1,173 @@ // @ts-check -const assert = require("assert") -const {scheduler} = require("timers/promises") +// Discord library internals type beat + +const DiscordTypes = require("discord-api-types/v10") const passthrough = require("../passthrough") -const {sync} = passthrough +const {sync, db} = passthrough -/** @type {import("../matrix/homeserver-status")} */ -const homeserverStatus = sync.require("../matrix/homeserver-status") +const utils = { + /** + * @param {import("./discord-client")} client + * @param {import("cloudstorm").IGatewayMessage} message + * @param {string} listen "full", "half", "no" - whether to set up the event listeners for OOYE to operate + */ + async onPacket(client, message, listen) { + // requiring this later so that the client is already constructed by the time event-dispatcher is loaded + /** @type {typeof import("./event-dispatcher")} */ + const eventDispatcher = sync.require("./event-dispatcher") + /** @type {import("../discord/register-interactions")} */ + const interactions = sync.require("../discord/register-interactions") -let checkedHomeserver = false + // Client internals, keep track of the state we need + if (message.t === "READY") { + if (client.ready) return + client.ready = true + client.user = message.d.user + client.application = message.d.application + console.log(`Discord logged in as ${client.user.username}#${client.user.discriminator} (${client.user.id})`) + interactions.registerInteractions() -/** - * @param {import("./discord-client")} client - * @param {import("cloudstorm").IGatewayMessage} message - * @param {string} listen "full", "half", "no" - whether to set up the event listeners for OOYE to operate - */ -async function onPacket(client, message, listen) { - // requiring this later so that the client is already constructed by the time event-dispatcher is loaded - /** @type {typeof import("./event-dispatcher")} */ - const eventDispatcher = sync.require("./event-dispatcher") - /** @type {import("../discord/register-interactions")} */ - const interactions = sync.require("../discord/register-interactions") + } 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 + client.guilds.set(message.d.id, message.d) + const arr = [] + client.guildChannelMap.set(message.d.id, arr) + for (const channel of message.d.channels || []) { + // @ts-ignore + channel.guild_id = message.d.id + arr.push(channel.id) + client.channels.set(channel.id, channel) + } + for (const thread of message.d.threads || []) { + // @ts-ignore + thread.guild_id = message.d.id + arr.push(thread.id) + client.channels.set(thread.id, thread) + } - // Client internals, keep track of the state we need - if (message.t === "READY") { - if (client.ready) return - client.ready = true - client.user = message.d.user - client.application = message.d.application - console.log(`Discord logged in as ${client.user.username}#${client.user.discriminator} (${client.user.id})`) - 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 - client.guilds.set(message.d.id, message.d) - const arr = [] - client.guildChannelMap.set(message.d.id, arr) - for (const channel of message.d.channels || []) { - // @ts-ignore - channel.guild_id = message.d.id - arr.push(channel.id) - client.channels.set(channel.id, channel) - } - for (const thread of message.d.threads || []) { - // @ts-ignore - thread.guild_id = message.d.id - arr.push(thread.id) - client.channels.set(thread.id, thread) - } - - 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~") + if (listen === "full") { + try { + await eventDispatcher.checkMissedExpressions(message.d) + await eventDispatcher.checkMissedMessages(client, message.d) + 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:") + console.error(e) } - await scheduler.wait(5000) - if (isMainCharacter) { - checkedHomeserver = true - process.stdout.write("Connecting to homeserver... ") - await homeserverStatus.homeserverStatus.waitForOnline(true) - console.log("ok.\nReplaying past events. Welcome to Out Of Your Element.") + } + + } else if (message.t === "GUILD_UPDATE") { + const guild = client.guilds.get(message.d.id) + if (guild) { + for (const prop of Object.keys(message.d)) { + if (!["channels", "threads"].includes(prop)) { + guild[prop] = message.d[prop] + } + } + } + + } else if (message.t === "GUILD_EMOJIS_UPDATE") { + const guild = client.guilds.get(message.d.guild_id) + if (guild) { + guild.emojis = message.d.emojis + } + + } else if (message.t === "GUILD_STICKERS_UPDATE") { + const guild = client.guilds.get(message.d.guild_id) + if (guild) { + guild.stickers = message.d.stickers + } + + } else if (message.t === "GUILD_ROLE_CREATE" || message.t === "GUILD_ROLE_UPDATE" || message.t === "GUILD_ROLE_DELETE") { + const guild = client.guilds.get(message.d.guild_id) + /** Delete this in case of UPDATE or DELETE */ + const targetID = "role_id" in message.d ? message.d.role_id : message.d.role.id + /** Add this in case of CREATE or UPDATE */ + const newRoles = [] + if ("role" in message.d) newRoles.push(message.d.role) + if (guild) { + const targetIndex = guild.roles.findIndex(r => r.id === targetID) + if (targetIndex !== -1) { + // Role already exists. Delete it and maybe replace it. + guild.roles.splice(targetIndex, 1, ...newRoles) } else { - await homeserverStatus.homeserverStatus.waitForOnline(false) - } - - await eventDispatcher.checkMissedExpressions(message.d) - await eventDispatcher.checkMissedMessages(client, message.d) - 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:") - console.error(e) - } - } - - } else if (message.t === "GUILD_UPDATE") { - const guild = client.guilds.get(message.d.id) - if (guild) { - for (const prop of Object.keys(message.d)) { - if (!["channels", "threads"].includes(prop)) { - guild[prop] = message.d[prop] + // Role doesn't already exist. + guild.roles.push(...newRoles) } } - } - } else if (message.t === "GUILD_EMOJIS_UPDATE") { - const guild = client.guilds.get(message.d.guild_id) - if (guild) { - guild.emojis = message.d.emojis - } - - } else if (message.t === "GUILD_STICKERS_UPDATE") { - const guild = client.guilds.get(message.d.guild_id) - if (guild) { - guild.stickers = message.d.stickers - } - - } else if (message.t === "GUILD_ROLE_CREATE" || message.t === "GUILD_ROLE_UPDATE" || message.t === "GUILD_ROLE_DELETE") { - const guild = client.guilds.get(message.d.guild_id) - /** Delete this in case of UPDATE or DELETE */ - const targetID = "role_id" in message.d ? message.d.role_id : message.d.role.id - /** Add this in case of CREATE or UPDATE */ - const newRoles = [] - if ("role" in message.d) newRoles.push(message.d.role) - if (guild) { - const targetIndex = guild.roles.findIndex(r => r.id === targetID) - if (targetIndex !== -1) { - // Role already exists. Delete it and maybe replace it. - guild.roles.splice(targetIndex, 1, ...newRoles) - } else { - // Role doesn't already exist. - guild.roles.push(...newRoles) + } else if (message.t === "GUILD_MEMBER_UPDATE") { + const guild = client.guilds.get(message.d.guild_id) + const member = guild?.members.find(m => m.user.id === message.d.user.id) + if (member) { // only update existing members (i.e. the bot's own member) - don't want to inflate the cache with new irrelevant ones + Object.assign(member, message.d) } - } - } else if (message.t === "GUILD_MEMBER_UPDATE") { - const guild = client.guilds.get(message.d.guild_id) - const member = guild?.members.find(m => m.user.id === message.d.user.id) - if (member) { // only update existing members (i.e. the bot's own member) - don't want to inflate the cache with new irrelevant ones - Object.assign(member, message.d) - } + } else if (message.t === "THREAD_CREATE") { + client.channels.set(message.d.id, message.d) + if (message.d["guild_id"]) { + const channels = client.guildChannelMap.get(message.d["guild_id"]) + if (channels && !channels.includes(message.d.id)) channels.push(message.d.id) + } - } else if (message.t === "THREAD_CREATE") { - client.channels.set(message.d.id, message.d) - if (message.d["guild_id"]) { - const channels = client.guildChannelMap.get(message.d["guild_id"]) - if (channels && !channels.includes(message.d.id)) channels.push(message.d.id) - } - - } else if (message.t === "CHANNEL_UPDATE" || message.t === "THREAD_UPDATE") { - client.channels.set(message.d.id, message.d) + } else if (message.t === "CHANNEL_UPDATE" || message.t === "THREAD_UPDATE") { + client.channels.set(message.d.id, message.d) - } else if (message.t === "CHANNEL_PINS_UPDATE") { - const channel = client.channels.get(message.d.channel_id) - if (channel) { - channel["last_pin_timestamp"] = message.d.last_pin_timestamp - } + } else if (message.t === "CHANNEL_PINS_UPDATE") { + const channel = client.channels.get(message.d.channel_id) + if (channel) { + channel["last_pin_timestamp"] = message.d.last_pin_timestamp + } - } else if (message.t === "GUILD_DELETE") { - client.guilds.delete(message.d.id) - const channels = client.guildChannelMap.get(message.d.id) - if (channels) { - for (const id of channels) client.channels.delete(id) - } - client.guildChannelMap.delete(message.d.id) - - - } else if (message.t === "CHANNEL_CREATE") { - client.channels.set(message.d.id, message.d) - if (message.d["guild_id"]) { // obj[prop] notation can be used to access a property without typescript complaining that it doesn't exist on all values something can have - const channels = client.guildChannelMap.get(message.d["guild_id"]) - if (channels && !channels.includes(message.d.id)) channels.push(message.d.id) - } - - } else if (message.t === "CHANNEL_DELETE") { - client.channels.delete(message.d.id) - if (message.d["guild_id"]) { - const channels = client.guildChannelMap.get(message.d["guild_id"]) + } else if (message.t === "GUILD_DELETE") { + client.guilds.delete(message.d.id) + const channels = client.guildChannelMap.get(message.d.id) if (channels) { - const previous = channels.indexOf(message.d.id) - if (previous !== -1) channels.splice(previous, 1) + for (const id of channels) client.channels.delete(id) + } + client.guildChannelMap.delete(message.d.id) + + + } else if (message.t === "CHANNEL_CREATE") { + client.channels.set(message.d.id, message.d) + if (message.d["guild_id"]) { // obj[prop] notation can be used to access a property without typescript complaining that it doesn't exist on all values something can have + const channels = client.guildChannelMap.get(message.d["guild_id"]) + if (channels && !channels.includes(message.d.id)) channels.push(message.d.id) + } + + } else if (message.t === "CHANNEL_DELETE") { + client.channels.delete(message.d.id) + if (message.d["guild_id"]) { + const channels = client.guildChannelMap.get(message.d["guild_id"]) + if (channels) { + const previous = channels.indexOf(message.d.id) + if (previous !== -1) channels.splice(previous, 1) + } + } + } + + // Event dispatcher for OOYE bridge operations + if (listen === "full" && message.t) { + try { + if (message.t === "MESSAGE_REACTION_REMOVE" || message.t === "MESSAGE_REACTION_REMOVE_EMOJI" || message.t === "MESSAGE_REACTION_REMOVE_ALL") { + await eventDispatcher.onSomeReactionsRemoved(client, message.d) + + } else if (message.t === "INTERACTION_CREATE") { + await interactions.dispatchInteraction(message.d) + + } else if (message.t in eventDispatcher) { + await eventDispatcher[message.t](client, message.d) + } + } catch (e) { + // Let OOYE try to handle errors too + await eventDispatcher.onError(client, e, message) } } } - - // Event dispatcher for OOYE bridge operations - if (listen === "full" && message.t) { - const alwaysRealTimeEvents = ["PRESENCE_UPDATE"] - if (alwaysRealTimeEvents.includes(message.t) || homeserverStatus.homeserverStatus.isRealTime()) { - dispatchPacketToBridge(client, message) - } else { - homeserverStatus.homeserverStatus.queuePacket(message) - } - } } -/** - * @param {import("./discord-client")} client - * @param {import("cloudstorm").IGatewayMessage} message - */ -async function dispatchPacketToBridge(client, message) { - // requiring this later so that the client is already constructed by the time event-dispatcher is loaded - /** @type {typeof import("./event-dispatcher")} */ - const eventDispatcher = sync.require("./event-dispatcher") - /** @type {import("../discord/register-interactions")} */ - const interactions = sync.require("../discord/register-interactions") - - assert(message.t) // checked above - - try { - if (message.t === "MESSAGE_REACTION_REMOVE" || message.t === "MESSAGE_REACTION_REMOVE_EMOJI" || message.t === "MESSAGE_REACTION_REMOVE_ALL") { - await eventDispatcher.onSomeReactionsRemoved(client, message.d) - - } else if (message.t === "INTERACTION_CREATE") { - await interactions.dispatchInteraction(message.d) - - } else if (message.t in eventDispatcher) { - await eventDispatcher[message.t](client, message.d) - } - } catch (e) { - // Let OOYE try to handle errors too - await eventDispatcher.onError(client, e, message) - } -} - -module.exports.onPacket = onPacket -module.exports.dispatchPacketToBridge = dispatchPacketToBridge +module.exports = utils diff --git a/src/d2m/event-dispatcher.js b/src/d2m/event-dispatcher.js index 31ef7ea..03ca72b 100644 --- a/src/d2m/event-dispatcher.js +++ b/src/d2m/event-dispatcher.js @@ -60,7 +60,7 @@ module.exports = { matrixEventDispatcher.printError(gatewayMessage.t, "Discord", e, gatewayMessage) - const channelID = gatewayMessage.d?.["channel_id"] + const channelID = gatewayMessage.d["channel_id"] if (!channelID) return const roomID = select("channel_room", "room_id", {channel_id: channelID}).pluck().get() if (!roomID) return diff --git a/src/discord/register-interactions.js b/src/discord/register-interactions.js index dc97287..66012b4 100644 --- a/src/discord/register-interactions.js +++ b/src/discord/register-interactions.js @@ -16,7 +16,7 @@ const ping = sync.require("./interactions/ping.js") // User must have EVERY permission in default_member_permissions to be able to use the command function registerInteractions() { - discord.snow.interaction.editApplicationCommands(id, [{ + discord.snow.interaction.bulkOverwriteApplicationCommands(id, [{ name: "Matrix info", contexts: [DiscordTypes.InteractionContextType.Guild], type: DiscordTypes.ApplicationCommandType.Message, diff --git a/src/m2d/actions/emoji-sheet.js b/src/m2d/actions/emoji-sheet.js index 077240c..ed5ab88 100644 --- a/src/m2d/actions/emoji-sheet.js +++ b/src/m2d/actions/emoji-sheet.js @@ -23,7 +23,7 @@ async function getAndConvertEmoji(mxc) { const res = await api.getMedia(mxc, {signal: abortController.signal}) if (res.status !== 200) { const root = await res.json() - throw new mreq.MatrixServerError(root, res.status, {mxc}) + throw new mreq.MatrixServerError(root, {mxc}) } const readable = stream.Readable.fromWeb(res.body) return emojiSheetConverter.convertImageStream(readable, () => { diff --git a/src/m2d/actions/sticker.js b/src/m2d/actions/sticker.js index f5a1e16..8eeb5d2 100644 --- a/src/m2d/actions/sticker.js +++ b/src/m2d/actions/sticker.js @@ -22,7 +22,7 @@ async function getAndResizeSticker(mxc) { const res = await api.getMedia(mxc) if (res.status !== 200) { const root = await res.json() - throw new mreq.MatrixServerError(root, res.status, {mxc}) + throw new mreq.MatrixServerError(root, {mxc}) } const streamIn = Readable.fromWeb(res.body) diff --git a/src/m2d/actions/update-pins.js b/src/m2d/actions/update-pins.js index 1b3f1bd..1ff2bb9 100644 --- a/src/m2d/actions/update-pins.js +++ b/src/m2d/actions/update-pins.js @@ -16,9 +16,9 @@ async function updatePins(pins, prev) { .select("reference_channel_id", "message_id").where({event_id}).and("ORDER BY part ASC").get() if (!row) continue if (added) { - discord.snow.channel.createChannelPinnedMessage(row.reference_channel_id, row.message_id, "Message pinned on Matrix") + discord.snow.channel.addChannelPinnedMessage(row.reference_channel_id, row.message_id, "Message pinned on Matrix") } else { - discord.snow.channel.deleteChannelPinnedMessage(row.reference_channel_id, row.message_id, "Message unpinned on Matrix") + discord.snow.channel.removeChannelPinnedMessage(row.reference_channel_id, row.message_id, "Message unpinned on Matrix") } } } diff --git a/src/m2d/event-dispatcher.js b/src/m2d/event-dispatcher.js index 0615743..3580d1b 100644 --- a/src/m2d/event-dispatcher.js +++ b/src/m2d/event-dispatcher.js @@ -24,8 +24,6 @@ const vote = sync.require("./actions/vote") const matrixCommandHandler = sync.require("../matrix/matrix-command-handler") /** @type {import("../matrix/utils")} */ const utils = sync.require("../matrix/utils") -/** @type {import("../matrix/mreq")}) */ -const mreq = sync.require("../matrix/mreq") /** @type {import("../matrix/api")}) */ const api = sync.require("../matrix/api") /** @type {import("../d2m/actions/create-room")} */ @@ -34,8 +32,6 @@ const createRoom = sync.require("../d2m/actions/create-room") const roomUpgrade = require("../matrix/room-upgrade") /** @type {import("../d2m/actions/retrigger")} */ const retrigger = sync.require("../d2m/actions/retrigger") -/** @type {import("../matrix/homeserver-status")} */ -const homeserverStatus = sync.require("../matrix/homeserver-status") const {reg} = require("../matrix/read-registration") let lastReportedEvent = 0 @@ -170,12 +166,7 @@ async function sendError(roomID, source, type, e, payload) { key: "🔁" } }) - } catch (e) { - if (e instanceof mreq.MatrixServerError && [502, 503].includes(e.httpStatus)) { - // Matrix homeserver is down (reverse proxy indicated failure; synapse doesn't generate 502/503 when posting an event to a room) - homeserverStatus.homeserverStatus.setErrorWithPacket(payload) - } - } + } catch (e) {} } function guard(type, fn) { diff --git a/src/matrix/api.js b/src/matrix/api.js index 47fc565..f68380f 100644 --- a/src/matrix/api.js +++ b/src/matrix/api.js @@ -452,8 +452,7 @@ async function ping() { headers: { Authorization: `Bearer ${reg.as_token}` }, - body: "{}", - signal: AbortSignal.timeout(15e3) + body: "{}" }) const root = await res.json() return { diff --git a/src/matrix/homeserver-status.js b/src/matrix/homeserver-status.js deleted file mode 100644 index 3ceac84..0000000 --- a/src/matrix/homeserver-status.js +++ /dev/null @@ -1,134 +0,0 @@ -// @ts-check - -const assert = require("assert") -const Denque = require("denque") -const StateMachine = require("snowtransfer").StateMachine - -const passthrough = require("../passthrough") -const {sync} = passthrough -/** @type {import("../d2m/discord-packets")} */ -const discordPackets = sync.require("../d2m/discord-packets") -/** @type {import("../matrix/api")} */ -const api = sync.require("../matrix/api") - -const DEBUG_HOMESERVER_STATUS = false - -function debugHomeserverStatus(message) { - if (DEBUG_HOMESERVER_STATUS) { - console.log(message) - } -} - -const homeserverStatus = new class HomeserverStatus { - constructor() { - /** @private */ - this.queue = new Denque() - - /** @private */ - this.pingInterval = undefined - - /** @private */ - this.sm = new StateMachine("online") - .defineState("online") - - .defineState("checking", { - onEnter: [async () => { - const pingResult = await api.ping().catch(e => ({ok: false})) - if (pingResult.ok) { - this.sm.doTransition("check ok") - } else { - this.sm.doTransition("check fail") - } - }], - onLeave: [], - transitions: new Map() - }) - - .defineState("offline", { - onEnter: [() => { - this.pingInterval = setInterval(async () => { - const pingResult = await api.ping().catch(e => ({ok: false, status: "net", root: e.message})) - if (pingResult.ok) { - this.sm.doTransition("ping ok") - } - }, 15e3) - }], - onLeave: [() => { - clearInterval(this.pingInterval) - }], - transitions: new Map() - }) - - .defineState("recovering", { - onEnter: [async () => { // Drain queue. - while (!this.queue.isEmpty()) { - const packet = this.queue.peekFront() // same position as .shift() - debugHomeserverStatus(`homeserver status: ${new Date().toISOString()} dq packet ${packet.t} ${packet.d?.content}`) - await discordPackets.dispatchPacketToBridge(passthrough.discord, packet) - if (this.sm.currentStateName !== "recovering") return // got kicked out due to another error - this.queue.shift() - } - this.sm.doTransition("recovered") - }], - onLeave: [], - transitions: new Map() - }) - - .defineUniversalTransition("error", "offline") - .defineTransition("offline", "ping ok", "recovering") - .defineTransition("recovering", "recovered", "online") - .defineTransition("online", "check", "checking") - .defineTransition("checking", "check ok", "recovering") - .defineTransition("checking", "check fail", "offline") - - this.sm.on("enter", st => debugHomeserverStatus(`homeserver status: ${st}`)) - this.sm.setMaxListeners(101) - this.sm.freeze() - } - - isRealTime() { - return this.sm.currentStateName === "online" - } - - /** @param {boolean} forceCheck */ - waitForOnline(forceCheck) { - const onlinePromise = new Promise(resolve => { - // Already online? Start check or just done - if (this.sm.currentStateName === "online") { - if (forceCheck) { - this.sm.doTransition("check") - } else { - return resolve(null) - } - } - - // Checking or not online. Wait for online. - const onlineListener = stateName => { - if (stateName === "online") { - this.sm.removeListener("enter", onlineListener) - resolve(null) - } - } - this.sm.on("enter", onlineListener) - }) - return onlinePromise - } - - /** - * When offline or recovering, call this for incoming packets to queue them to be sent in order later. - */ - queuePacket(packet) { - assert(["offline", "recovering"].includes(this.sm.currentStateName)) - this.queue.push(packet) - } - - setErrorWithPacket(packet) { - const wasRecovering = this.sm.currentStateName === "recovering" - this.sm.doTransition("error") - if (!wasRecovering) { // if was recovering then packet is already in the right place in queue - this.queuePacket(packet) - } - } -} - -module.exports.homeserverStatus = homeserverStatus diff --git a/src/matrix/mreq.js b/src/matrix/mreq.js index bf238f2..bb59506 100644 --- a/src/matrix/mreq.js +++ b/src/matrix/mreq.js @@ -9,12 +9,9 @@ const {reg} = require("./read-registration.js") const baseUrl = `${reg.ooye.server_origin}/_matrix` class MatrixServerError extends Error { - /** @param {number} httpStatus} */ - constructor(data, httpStatus, opts) { + constructor(data, opts) { super(data.error || data.errcode) this.data = data - /** @type {number} */ - this.httpStatus = httpStatus /** @type {string} */ this.errcode = data.errcode this.opts = opts @@ -47,11 +44,11 @@ async function _convertBody(body) { async function makeMatrixServerError(res, opts = {}) { delete opts.headers?.["Authorization"] if (res.headers.get("content-type") === "application/json") { - return new MatrixServerError(await res.json(), res.status, opts) + return new MatrixServerError(await res.json(), opts) } else if (res.headers.get("content-type")?.startsWith("text/")) { - return new MatrixServerError({errcode: "CX_SERVER_ERROR", error: `Server returned HTTP status ${res.status}`, message: await res.text()}, res.status, opts) + return new MatrixServerError({errcode: "CX_SERVER_ERROR", error: `Server returned HTTP status ${res.status}`, message: await res.text()}, opts) } else { - return new MatrixServerError({errcode: "CX_SERVER_ERROR", error: `Server returned HTTP status ${res.status}`, content_type: res.headers.get("content-type")}, res.status, opts) + return new MatrixServerError({errcode: "CX_SERVER_ERROR", error: `Server returned HTTP status ${res.status}`, content_type: res.headers.get("content-type")}, opts) } } @@ -81,12 +78,12 @@ async function mreq(method, url, bodyIn, extra = {}) { var root = JSON.parse(text) } catch (e) { delete opts.headers?.["Authorization"] - throw new MatrixServerError(text, res.status, {baseUrl, url, ...opts}) + throw new MatrixServerError(text, {baseUrl, url, ...opts}) } if (!res.ok || root.errcode) { delete opts.headers?.["Authorization"] - throw new MatrixServerError(root, res.status, {baseUrl, url, ...opts}) + throw new MatrixServerError(root, {baseUrl, url, ...opts}) } return root } diff --git a/start.js b/start.js index 51942c6..39e8ea0 100755 --- a/start.js +++ b/start.js @@ -31,9 +31,8 @@ sync.require("./src/m2d/event-dispatcher") ;(async () => { await migrate.migrate(db) - process.stdout.write("Connecting to Discord... ") await discord.cloud.connect() - console.log("ok.") + console.log("Discord gateway started") sync.require("./src/web/server") await power.applyPower()