From 4cb99feeb2482f5849a5ea4fcafff82d966502c7 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Thu, 13 Mar 2025 17:15:40 +1300 Subject: [PATCH] Rework event dispatchers --- src/d2m/discord-packets.js | 50 ++--------------- src/d2m/event-dispatcher.js | 97 +++++++++++++------------------- src/m2d/event-dispatcher.js | 109 +++++++++++++++++++++--------------- 3 files changed, 105 insertions(+), 151 deletions(-) diff --git a/src/d2m/discord-packets.js b/src/d2m/discord-packets.js index c9424a4..017d50e 100644 --- a/src/d2m/discord-packets.js +++ b/src/d2m/discord-packets.js @@ -157,59 +157,17 @@ const utils = { } // Event dispatcher for OOYE bridge operations - if (listen === "full") { + if (listen === "full" && message.t) { try { - if (message.t === "GUILD_UPDATE") { - await eventDispatcher.onGuildUpdate(client, message.d) - - } else if (message.t === "GUILD_EMOJIS_UPDATE" || message.t === "GUILD_STICKERS_UPDATE") { - await eventDispatcher.onExpressionsUpdate(client, message.d) - - } else if (message.t === "CHANNEL_UPDATE") { - await eventDispatcher.onChannelOrThreadUpdate(client, message.d, false) - - } else if (message.t === "CHANNEL_PINS_UPDATE") { - await eventDispatcher.onChannelPinsUpdate(client, message.d) - - } else if (message.t === "CHANNEL_DELETE") { - await eventDispatcher.onChannelDelete(client, message.d) - - } else if (message.t === "THREAD_CREATE") { - // @ts-ignore - await eventDispatcher.onThreadCreate(client, message.d) - - } else if (message.t === "THREAD_UPDATE") { - // @ts-ignore - await eventDispatcher.onChannelOrThreadUpdate(client, message.d, true) - - } else if (message.t === "MESSAGE_CREATE") { - await eventDispatcher.onMessageCreate(client, message.d) - - } else if (message.t === "MESSAGE_UPDATE") { - await eventDispatcher.onMessageUpdate(client, message.d) - - } else if (message.t === "MESSAGE_DELETE") { - await eventDispatcher.onMessageDelete(client, message.d) - - } else if (message.t === "MESSAGE_DELETE_BULK") { - await eventDispatcher.onMessageDeleteBulk(client, message.d) - - } else if (message.t === "TYPING_START") { - await eventDispatcher.onTypingStart(client, message.d) - - } else if (message.t === "MESSAGE_REACTION_ADD") { - await eventDispatcher.onReactionAdd(client, message.d) - - } else if (message.t === "MESSAGE_REACTION_REMOVE" || message.t === "MESSAGE_REACTION_REMOVE_EMOJI" || message.t === "MESSAGE_REACTION_REMOVE_ALL") { + 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 === "PRESENCE_UPDATE") { - eventDispatcher.onPresenceUpdate(client, 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) diff --git a/src/d2m/event-dispatcher.js b/src/d2m/event-dispatcher.js index b98abfc..63ef3e0 100644 --- a/src/d2m/event-dispatcher.js +++ b/src/d2m/event-dispatcher.js @@ -2,7 +2,6 @@ const assert = require("assert").strict const DiscordTypes = require("discord-api-types/v10") -const util = require("util") const {sync, db, select, from} = require("../passthrough") /** @type {import("./actions/send-message")}) */ @@ -27,8 +26,6 @@ const updatePins = sync.require("./actions/update-pins") const api = sync.require("../matrix/api") /** @type {import("../discord/utils")} */ const dUtils = sync.require("../discord/utils") -/** @type {import("../m2d/converters/utils")} */ -const mxUtils = require("../m2d/converters/utils") /** @type {import("./actions/speedbump")} */ const speedbump = sync.require("./actions/speedbump") /** @type {import("./actions/retrigger")} */ @@ -42,8 +39,6 @@ const matrixEventDispatcher = sync.require("../m2d/event-dispatcher") const Semaphore = require("@chriscdn/promise-semaphore") const checkMissedPinsSema = new Semaphore() -let lastReportedEvent = 0 - // Grab Discord events we care about for the bridge, check them, and pass them on module.exports = { @@ -53,45 +48,14 @@ module.exports = { * @param {import("cloudstorm").IGatewayMessage} gatewayMessage */ async onError(client, e, gatewayMessage) { - console.error("hit event-dispatcher's error handler with this exception:") - console.error(e) // TODO: also log errors into a file or into the database, maybe use a library for this? or just wing it? definitely need to be able to store the formatted event body to load back in later - console.error(`while handling this ${gatewayMessage.t} gateway event:`) - console.dir(gatewayMessage.d, {depth: null}) - - if (gatewayMessage.t === "TYPING_START") return - - if (Date.now() - lastReportedEvent < 5000) return - lastReportedEvent = Date.now() - const channelID = gatewayMessage.d["channel_id"] if (!channelID) return const roomID = select("channel_room", "room_id", {channel_id: channelID}).pluck().get() if (!roomID) return - const builder = new mxUtils.MatrixStringBuilder() - builder.addLine("\u26a0 Bridged event from Discord not delivered", "\u26a0 Bridged event from Discord not delivered") - builder.addLine(`Gateway event: ${gatewayMessage.t}`) + if (gatewayMessage.t === "TYPING_START") return - let errorIntroLine = e.toString() - if (e.cause) { - errorIntroLine += ` (cause: ${e.cause})` - } - builder.addLine(errorIntroLine) - - const stack = matrixEventDispatcher.stringifyErrorStack(e) - builder.addLine(`Error trace:\n${stack}`, `
Error trace
${stack}
`) - - builder.addLine("", `
Original payload
${util.inspect(gatewayMessage.d, false, 4, false)}
`) - await api.sendEvent(roomID, "m.room.message", { - ...builder.get(), - "moe.cadence.ooye.error": { - source: "discord", - payload: gatewayMessage - }, - "m.mentions": { - user_ids: ["@cadence:cadence.moe"] - } - }) + await matrixEventDispatcher.sendError(roomID, "Discord", gatewayMessage.t, e, gatewayMessage.d) }, /** @@ -151,7 +115,7 @@ module.exports = { backfill: true, ...messages[i] } - await module.exports.onMessageCreate(client, simulatedGatewayDispatchData) + await module.exports.MESSAGE_CREATE(client, simulatedGatewayDispatchData) } } }, @@ -198,7 +162,7 @@ module.exports = { * @param {import("./discord-client")} client * @param {DiscordTypes.APIThreadChannel} thread */ - async onThreadCreate(client, thread) { + async THREAD_CREATE(client, thread) { const channelID = thread.parent_id || undefined const parentRoomID = select("channel_room", "room_id", {channel_id: channelID}).pluck().get() if (!parentRoomID) return // Not interested in a thread if we aren't interested in its wider channel (won't autocreate) @@ -210,7 +174,7 @@ module.exports = { * @param {import("./discord-client")} client * @param {DiscordTypes.GatewayGuildUpdateDispatchData} guild */ - async onGuildUpdate(client, guild) { + async GUILD_UPDATE(client, guild) { const spaceID = select("guild_space", "space_id", {guild_id: guild.id}).pluck().get() if (!spaceID) return await createSpace.syncSpace(guild) @@ -219,19 +183,26 @@ module.exports = { /** * @param {import("./discord-client")} client * @param {DiscordTypes.GatewayChannelUpdateDispatchData} channelOrThread - * @param {boolean} isThread */ - async onChannelOrThreadUpdate(client, channelOrThread, isThread) { + async CHANNEL_UPDATE(client, channelOrThread) { const roomID = select("channel_room", "room_id", {channel_id: channelOrThread.id}).pluck().get() if (!roomID) return // No target room to update the data on await createRoom.syncRoom(channelOrThread.id) }, + /** + * @param {import("./discord-client")} client + * @param {DiscordTypes.GatewayChannelUpdateDispatchData} thread + */ + async THREAD_UPDATE(client, thread) { + await module.exports.CHANNEL_UPDATE(client, thread) + }, + /** * @param {import("./discord-client")} client * @param {DiscordTypes.GatewayChannelPinsUpdateDispatchData} data */ - async onChannelPinsUpdate(client, data) { + async CHANNEL_PINS_UPDATE(client, data) { const roomID = select("channel_room", "room_id", {channel_id: data.channel_id}).pluck().get() if (!roomID) return // No target room to update pins in const convertedTimestamp = updatePins.convertTimestamp(data.last_pin_timestamp) @@ -242,7 +213,7 @@ module.exports = { * @param {import("./discord-client")} client * @param {DiscordTypes.GatewayChannelDeleteDispatchData} channel */ - async onChannelDelete(client, channel) { + async CHANNEL_DELETE(client, channel) { const guildID = channel["guild_id"] if (!guildID) return // channel must have been a DM channel or something const roomID = select("channel_room", "room_id", {channel_id: channel.id}).pluck().get() @@ -255,7 +226,7 @@ module.exports = { * @param {import("./discord-client")} client * @param {DiscordTypes.GatewayMessageCreateDispatchData} message */ - async onMessageCreate(client, message) { + async MESSAGE_CREATE(client, message) { if (message.author.username === "Deleted User") return // Nothing we can do for deleted users. const channel = client.channels.get(message.channel_id) if (!channel || !("guild_id" in channel) || !channel.guild_id) return // Nothing we can do in direct messages. @@ -285,7 +256,7 @@ module.exports = { * @param {import("./discord-client")} client * @param {DiscordTypes.GatewayMessageUpdateDispatchData} data */ - async onMessageUpdate(client, data) { + async MESSAGE_UPDATE(client, data) { // Based on looking at data they've sent me over the gateway, this is the best way to check for meaningful changes. // If the message content is a string then it includes all interesting fields and is meaningful. // Otherwise, if there are embeds, then the system generated URL preview embeds. @@ -303,7 +274,7 @@ module.exports = { if (affected) return // Check that the sending-to room exists, and deal with Eventual Consistency(TM) - if (retrigger.eventNotFoundThenRetrigger(data.id, module.exports.onMessageUpdate, client, data)) return + if (retrigger.eventNotFoundThenRetrigger(data.id, module.exports.MESSAGE_UPDATE, client, data)) return /** @type {DiscordTypes.GatewayMessageCreateDispatchData} */ // @ts-ignore @@ -321,7 +292,7 @@ module.exports = { * @param {import("./discord-client")} client * @param {DiscordTypes.GatewayMessageReactionAddDispatchData} data */ - async onReactionAdd(client, data) { + async MESSAGE_REACTION_ADD(client, data) { if (data.user_id === client.user.id) return // m2d reactions are added by the discord bot user - do not reflect them back to matrix. await addReaction.addReaction(data) }, @@ -338,25 +309,25 @@ module.exports = { * @param {import("./discord-client")} client * @param {DiscordTypes.GatewayMessageDeleteDispatchData} data */ - async onMessageDelete(client, data) { + async MESSAGE_DELETE(client, data) { speedbump.onMessageDelete(data.id) - if (retrigger.eventNotFoundThenRetrigger(data.id, module.exports.onMessageDelete, client, data)) return + if (retrigger.eventNotFoundThenRetrigger(data.id, module.exports.MESSAGE_DELETE, client, data)) return await deleteMessage.deleteMessage(data) }, - /** + /** * @param {import("./discord-client")} client * @param {DiscordTypes.GatewayMessageDeleteBulkDispatchData} data */ - async onMessageDeleteBulk(client, data) { - await deleteMessage.deleteMessageBulk(data) - }, + async MESSAGE_DELETE_BULK(client, data) { + await deleteMessage.deleteMessageBulk(data) + }, /** * @param {import("./discord-client")} client * @param {DiscordTypes.GatewayTypingStartDispatchData} data */ - async onTypingStart(client, data) { + async TYPING_START(client, data) { const roomID = select("channel_room", "room_id", {channel_id: data.channel_id}).pluck().get() if (!roomID) return const mxid = from("sim").join("sim_member", "mxid").where({user_id: data.user_id, room_id: roomID}).pluck("mxid").get() @@ -369,9 +340,17 @@ module.exports = { /** * @param {import("./discord-client")} client - * @param {DiscordTypes.GatewayGuildEmojisUpdateDispatchData | DiscordTypes.GatewayGuildStickersUpdateDispatchData} data + * @param {DiscordTypes.GatewayGuildEmojisUpdateDispatchData} data */ - async onExpressionsUpdate(client, data) { + async GUILD_EMOJIS_UPDATE(client, data) { + await createSpace.syncSpaceExpressions(data, false) + }, + + /** + * @param {import("./discord-client")} client + * @param {DiscordTypes.GatewayGuildStickersUpdateDispatchData} data + */ + async GUILD_STICKERS_UPDATE(client, data) { await createSpace.syncSpaceExpressions(data, false) }, @@ -379,7 +358,7 @@ module.exports = { * @param {import("./discord-client")} client * @param {DiscordTypes.GatewayPresenceUpdateDispatchData} data */ - onPresenceUpdate(client, data) { + PRESENCE_UPDATE(client, data) { const status = data.status if (!status) return setPresence.presenceTracker.incomingPresence(data.user.id, data.guild_id, status) diff --git a/src/m2d/event-dispatcher.js b/src/m2d/event-dispatcher.js index 1bc97de..a55c326 100644 --- a/src/m2d/event-dispatcher.js +++ b/src/m2d/event-dispatcher.js @@ -42,7 +42,7 @@ function stringifyErrorStack(err, depth = 0) { } // add full stack trace if one exists, otherwise convert to string - let stackLines = ( err?.stack ?? `${err}` ).replace(/^/gm, " ".repeat(depth)).trim().split("\n") + let stackLines = String(err?.stack ?? err).replace(/^/gm, " ".repeat(depth)).trim().split("\n") let cloudstormLine = stackLines.findIndex(l => l.includes("/node_modules/cloudstorm/")) if (cloudstormLine !== -1) { stackLines = stackLines.slice(0, cloudstormLine - 2) @@ -80,56 +80,72 @@ function stringifyErrorStack(err, depth = 0) { return collapsed; } +/** + * @param {string} roomID + * @param {"Discord" | "Matrix"} source + * @param {any} type + * @param {any} e + * @param {any} payload + */ +async function sendError(roomID, source, type, e, payload) { + console.error(`Error while processing a ${type} ${source} event:`) + console.error(e) + console.dir(payload, {depth: null}) + + if (Date.now() - lastReportedEvent < 5000) return null + lastReportedEvent = Date.now() + + let errorIntroLine = e.toString() + if (e.cause) { + errorIntroLine += ` (cause: ${e.cause})` + } + + const builder = new utils.MatrixStringBuilder() + + const cloudflareErrorTitle = errorIntroLine.match(/.*?discord\.com \| ([^<]*)<\/title>/s)?.[1] + if (cloudflareErrorTitle) { + builder.addLine( + `\u26a0 Matrix event not delivered to Discord. Discord might be down right now. Cloudflare error: ${cloudflareErrorTitle}`, + `\u26a0 <strong>Matrix event not delivered to Discord</strong><br>Discord might be down right now. Cloudflare error: ${cloudflareErrorTitle}` + ) + } else { + // What + const what = source === "Discord" ? "Bridged event from Discord not delivered" : "Matrix event not delivered to Discord" + builder.addLine(`\u26a0 ${what}`, `\u26a0 <strong>${what}</strong>`) + + // Who + builder.addLine(`Event type: ${type}`) + + // Why + builder.addLine(errorIntroLine) + + // Where + const stack = stringifyErrorStack(e) + builder.addLine(`Error trace:\n${stack}`, `<details><summary>Error trace</summary><pre>${stack}</pre></details>`) + + // How + builder.addLine("", `<details><summary>Original payload</summary><pre>${util.inspect(payload, false, 4, false)}</pre></details>`) + } + + // Send + await api.sendEvent(roomID, "m.room.message", { + ...builder.get(), + "moe.cadence.ooye.error": { + source: source.toLowerCase(), + payload + }, + "m.mentions": { + user_ids: ["@cadence:cadence.moe"] + } + }) +} + function guard(type, fn) { return async function(event, ...args) { try { return await fn(event, ...args) } catch (e) { - console.error(`Exception while processing a ${type} Matrix event:`) - console.dir(event, {depth: null}) - - if (Date.now() - lastReportedEvent < 5000) return - lastReportedEvent = Date.now() - - let errorIntroLine = e.toString() - if (e.cause) { - errorIntroLine += ` (cause: ${e.cause})` - } - - const cloudflareErrorTitle = errorIntroLine.match(/<!DOCTYPE html>.*?<title>discord\.com \| ([^<]*)<\/title>/s)?.[1] - if (cloudflareErrorTitle) { - return api.sendEvent(event.room_id, "m.room.message", { - msgtype: "m.text", - body: `\u26a0 Matrix event not delivered to Discord. Cloudflare error: ${cloudflareErrorTitle}.`, - format: "org.matrix.custom.html", - formatted_body: `\u26a0 <strong>Matrix event not delivered to Discord</strong><br>Cloudflare error: ${cloudflareErrorTitle}`, - "moe.cadence.ooye.error": { - source: "matrix", - payload: event - } - }) - } - - const stack = stringifyErrorStack(e) - api.sendEvent(event.room_id, "m.room.message", { - msgtype: "m.text", - body: "\u26a0 Matrix event not delivered to Discord. See formatted content for full details.", - format: "org.matrix.custom.html", - formatted_body: "\u26a0 <strong>Matrix event not delivered to Discord</strong>" - + `<br>Event type: ${type}` - + `<br>${errorIntroLine}` - + `<br><details><summary>Error trace</summary>` - + `<pre>${stack}</pre></details>` - + `<details><summary>Original payload</summary>` - + `<pre>${util.inspect(event, false, 4, false)}</pre></details>`, - "moe.cadence.ooye.error": { - source: "matrix", - payload: event - }, - "m.mentions": { - user_ids: ["@cadence:cadence.moe"] - } - }) + await sendError(event.room_id, "Matrix", type, e, event) } } } @@ -356,3 +372,4 @@ async event => { })) module.exports.stringifyErrorStack = stringifyErrorStack +module.exports.sendError = sendError