diff --git a/src/d2m/actions/edit-message.js b/src/d2m/actions/edit-message.js index f86a9c8..73ef4bd 100644 --- a/src/d2m/actions/edit-message.js +++ b/src/d2m/actions/edit-message.js @@ -16,9 +16,9 @@ const mreq = sync.require("../../matrix/mreq") /** * @param {import("discord-api-types/v10").GatewayMessageCreateDispatchData} message * @param {import("discord-api-types/v10").APIGuild} guild - * @param {{speedbump_id: string, speedbump_webhook_id: string} | null} row data about the webhook which is proxying messages in this channel + * @param {{userID: string, webhookID: string} | null} proxyWebhook data about the webhook which is proxying messages in this channel */ -async function editMessage(message, guild, row) { +async function editMessage(message, guild, proxyWebhook) { const historicalRoomOfMessage = from("message_room").join("historical_channel_room", "historical_room_index").where({message_id: message.id}).select("room_id").get() const currentRoom = from("channel_room").join("historical_channel_room", "room_id").where({channel_id: message.channel_id}).select("room_id", "historical_room_index").get() if (!currentRoom) return @@ -27,9 +27,9 @@ async function editMessage(message, guild, row) { let {roomID, eventsToRedact, eventsToReplace, eventsToSend, senderMxid, promotions} = await editToChanges.editToChanges(message, guild, api) - if (row && row.speedbump_webhook_id === message.webhook_id) { + if (proxyWebhook && proxyWebhook.webhookID === message.webhook_id) { // Handle the PluralKit public instance - if (row.speedbump_id === "466378653216014359") { + if (proxyWebhook.userID === "466378653216014359") { senderMxid = await registerPkUser.syncUser(message.id, message.author, roomID, true) } } diff --git a/src/d2m/actions/send-message.js b/src/d2m/actions/send-message.js index 5b3b4f3..42a0b5c 100644 --- a/src/d2m/actions/send-message.js +++ b/src/d2m/actions/send-message.js @@ -28,9 +28,9 @@ const channelWebhook = sync.require("../../m2d/actions/channel-webhook") * @param {DiscordTypes.GatewayMessageCreateDispatchData} message * @param {DiscordTypes.APIGuildChannel} channel * @param {DiscordTypes.APIGuild} guild - * @param {{speedbump_id: string, speedbump_webhook_id: string} | null} row data about the webhook which is proxying messages in this channel + * @param {{userID: string, webhookID: string} | null} proxyWebhook data about the webhook which is proxying messages in this channel */ -async function sendMessage(message, channel, guild, row) { +async function sendMessage(message, channel, guild, proxyWebhook) { const roomID = await createRoom.ensureRoom(message.channel_id) const historicalRoomIndex = select("historical_channel_room", "historical_room_index", {room_id: roomID}).pluck().get() assert(historicalRoomIndex) @@ -38,9 +38,9 @@ async function sendMessage(message, channel, guild, row) { let senderMxid = null if (dUtils.isWebhookMessage(message)) { const useWebhookProfile = select("guild_space", "webhook_profile", {guild_id: guild.id}).pluck().get() ?? 0 - if (row && row.speedbump_webhook_id === message.webhook_id) { + if (proxyWebhook && proxyWebhook.webhookID === message.webhook_id) { // Handle the PluralKit public instance - if (row.speedbump_id === "466378653216014359") { + if (proxyWebhook.userID === "466378653216014359") { senderMxid = await registerPkUser.syncUser(message.id, message.author, roomID, true) } } else if (useWebhookProfile) { diff --git a/src/d2m/actions/speedbump.js b/src/d2m/actions/speedbump.js index 42e3a35..22a8f5a 100644 --- a/src/d2m/actions/speedbump.js +++ b/src/d2m/actions/speedbump.js @@ -5,9 +5,9 @@ const {discord, select, db} = passthrough const DEBUG_SPEEDBUMP = false -function debugSpeedbump(message) { +function debugSpeedbump(...args) { if (DEBUG_SPEEDBUMP) { - console.log(message) + console.log(...args) } } @@ -19,6 +19,11 @@ const KNOWN_BOTS = new Set([ "466378653216014359" // PluralKit ]) +const CONTEXT_ENUM = { + create: "create", + update: "update" +} + /** * Fetch new speedbump data for the channel and put it in the database as cache * @param {string} channelID @@ -34,55 +39,68 @@ async function updateCache(channelID, lastChecked) { db.prepare("UPDATE channel_room SET speedbump_id = ?, speedbump_webhook_id = ?, speedbump_checked = ? WHERE channel_id = ?").run(foundApplication, foundWebhook, now, channelID) } -/** @type {Map} messageID -> number of gateway events currently bumping */ +/** + * @typedef BumpingEntry + * @prop {number} number number of gateway events currently bumping for this message ID + * @prop {boolean} hasCreate whether there was a message create within the events currently bumping + */ + +/** @type {Map} messageID -> BumpingEntry */ const bumping = new Map() /** * Slow down a message. After it passes the speedbump, return whether it's okay or if it's been deleted. + * @param {keyof typeof CONTEXT_ENUM} context * @param {string} messageID * @returns whether it was deleted */ -async function doSpeedbump(messageID) { - let value = (bumping.get(messageID) ?? 0) + 1 - bumping.set(messageID, value) - debugSpeedbump(`[speedbump] WAIT ${messageID}++ = ${value}`) +async function doSpeedbump(context, messageID) { + const entry = bumping.get(messageID) ?? (() => { + const entry = {number: 0, hasCreate: false} + bumping.set(messageID, entry) + return entry + })() + + entry.number++ + if (context === "create") entry.hasCreate = true + + debugSpeedbump(`[speedbump] WAIT ${messageID}++ =`, entry) await new Promise(resolve => setTimeout(resolve, SPEEDBUMP_SPEED)) if (!bumping.has(messageID)) { debugSpeedbump(`[speedbump] DELETED ${messageID}`) - return true + return {skip: true, hasCreate: null} } - value = (bumping.get(messageID) ?? 0) - 1 - if (value <= 0) { - debugSpeedbump(`[speedbump] OK ${messageID}-- = ${value}`) + + if (--entry.number <= 0) { + debugSpeedbump(`[speedbump] OK ${messageID}-- =`, entry) bumping.delete(messageID) - return false + return {skip: false, hasCreate: entry.hasCreate} } else { - debugSpeedbump(`[speedbump] MULTI ${messageID}-- = ${value}`) - bumping.set(messageID, value) - return true + debugSpeedbump(`[speedbump] MULTI ${messageID}-- =`, entry) + return {skip: true, hasCreate: null} } } /** * Check whether to slow down a message, and do it. After it passes the speedbump, return whether it's okay or if it's been deleted. - * @param {string} channelID - * @param {string} messageID - * @param {string} [userID] if provided, only slow down the message when the user has used PK before + * @param {keyof typeof CONTEXT_ENUM} context + * @param {{id: string, channel_id: string, author: {id: string}, backfill?: boolean}} message uses the ID to identify, and the userID to only slow down the message when the user has used PK before * @returns whether it was deleted, and data about the channel's (not thread's) speedbump */ -async function maybeDoSpeedbump(channelID, messageID, userID) { - let row = select("channel_room", ["room_id", "thread_parent", "speedbump_id", "speedbump_webhook_id"], {channel_id: channelID}).get() +async function maybeDoSpeedbump(context, message) { + let row = select("channel_room", ["room_id", "thread_parent", "speedbump_id", "speedbump_webhook_id"], {channel_id: message.channel_id}).get() if (row?.thread_parent) row = select("channel_room", ["room_id", "thread_parent", "speedbump_id", "speedbump_webhook_id"], {channel_id: row.thread_parent}).get() // webhooks belong to the channel, not the thread - if (!row?.speedbump_webhook_id) return {affected: false, row: null} // channel not affected, no speedbump - if (userID) { - if (row.speedbump_webhook_id === userID) return {affected: false, row} // shortcut - const userHasProxy = select("sim_proxy", "user_id", {proxy_owner_id: userID}).pluck().get() - if (!userHasProxy) return {affected: false, row} // user has not used PK before, no speedbump - } - const affected = await doSpeedbump(messageID) - return {affected, row} // maybe affected, and there is a speedbump + if (!row?.speedbump_webhook_id || !row?.speedbump_id) return {skip: false, proxyWebhook: null} // channel not affected, no speedbump + + let proxyWebhook = {userID: row.speedbump_id, webhookID: row.speedbump_webhook_id} + if (message.backfill) return {skip: false, proxyWebhook} // don't slow messages during backfill + if (proxyWebhook.webhookID === message.author.id) return {skip: false, proxyWebhook} // shortcut + const userHasProxy = select("sim_proxy", "user_id", {proxy_owner_id: message.author.id}).pluck().get() + if (!userHasProxy) return {skip: false, proxyWebhook} // user has not used PK before, no speedbump + const {skip, hasCreate} = await doSpeedbump(context, message.id) + return {skip, hasCreate, proxyWebhook} // maybe affected, and there is a speedbump } /** diff --git a/src/d2m/event-dispatcher.js b/src/d2m/event-dispatcher.js index c0c33d9..0556697 100644 --- a/src/d2m/event-dispatcher.js +++ b/src/d2m/event-dispatcher.js @@ -288,7 +288,6 @@ module.exports = { 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() if (!roomID) return // channel wasn't being bridged in the first place - // @ts-ignore await createRoom.unbridgeChannel(channel, guildID) }, @@ -313,11 +312,10 @@ module.exports = { if (!createRoom.existsOrAutocreatable(channel, guild.id)) return // Check that the sending-to room exists or is autocreatable - const {affected, row} = await speedbump.maybeDoSpeedbump(message.channel_id, message.id, message.author.id) - if (affected) return + const {skip, proxyWebhook} = await speedbump.maybeDoSpeedbump("create", message) + if (skip) return - // @ts-ignore - await sendMessage.sendMessage(message, channel, guild, row) + await sendMessage.sendMessage(message, channel, guild, proxyWebhook) retrigger.finishedBridging(message.id) }, @@ -335,22 +333,27 @@ module.exports = { if (dUtils.isEphemeralMessage(data)) return // Ephemeral messages are for the eyes of the receiver only! // Edits need to go through the speedbump as well. If the message is delayed but the edit isn't, we don't have anything to edit from. - const {affected, row} = await speedbump.maybeDoSpeedbump(data.channel_id, data.id, data.author.id) - if (affected) return - - // Check that the sending-to room exists, and deal with Eventual Consistency(TM) - if (!await retrigger.waitForMessage(data.id)) return + const {skip, hasCreate, proxyWebhook} = await speedbump.maybeDoSpeedbump("update", data) + if (skip) return /** @type {DiscordTypes.GatewayMessageCreateDispatchData} */ - // @ts-ignore const message = data 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. const guild = client.guilds.get(channel.guild_id) assert(guild) - // @ts-ignore - await retrigger.pauseChanges(message.id, editMessage.editMessage(message, guild, row)) + if (!hasCreate) { + // Standard path for most message updates + // Check that the target message already exists, and deal with Eventual Consistency(TM) + if (!await retrigger.waitForMessage(data.id)) return + await retrigger.pauseChanges(message.id, editMessage.editMessage(message, guild, proxyWebhook)) + } + else { + // Path for edit packets that were speedbumped into the latest copy of a message that needs to be created + // Just pretend to be MESSAGE_CREATE + await sendMessage.sendMessage(message, channel, guild, proxyWebhook) + } }, /**