diff --git a/d2m/actions/register-pk-user.js b/d2m/actions/register-pk-user.js index 2ead4ff..1e223c6 100644 --- a/d2m/actions/register-pk-user.js +++ b/d2m/actions/register-pk-user.js @@ -142,19 +142,8 @@ async function syncUser(author, pkMessage, roomID) { } /** @returns {Promise} */ -async function fetchMessage(messageID) { - // Their backend is weird. Sometimes it says "message not found" (code 20006) on the first try, so we make multiple attempts. - let attempts = 0 - do { - var res = await fetch(`https://api.pluralkit.me/v2/messages/${messageID}`) - if (res.ok) return res.json() - - // I think the backend needs some time to update. - await new Promise(resolve => setTimeout(resolve, 2000)) - } while (++attempts < 3) - - const errorMessage = await res.json() - throw new Error(`PK API returned an error after ${attempts} tries: ${JSON.stringify(errorMessage)}`) +function fetchMessage(messageID) { + return fetch(`https://api.pluralkit.me/v2/messages/${messageID}`).then(res => res.json()) } module.exports._memberToStateContent = memberToStateContent diff --git a/d2m/actions/retrigger.js b/d2m/actions/retrigger.js deleted file mode 100644 index 2475daf..0000000 --- a/d2m/actions/retrigger.js +++ /dev/null @@ -1,61 +0,0 @@ -// @ts-check - -const {EventEmitter} = require("events") -const passthrough = require("../../passthrough") -const {select} = passthrough - -const DEBUG_RETRIGGER = false - -function debugRetrigger(message) { - if (DEBUG_RETRIGGER) { - console.log(message) - } -} - -const emitter = new EventEmitter() - -/** - * Due to Eventual Consistency(TM) an update/delete may arrive before the original message arrives - * (or before the it has finished being bridged to an event). - * In this case, wait until the original message has finished bridging, then retrigger the passed function. - * @template {(...args: any) => Promise} T - * @param {string} messageID - * @param {T} fn - * @param {Parameters} rest - * @returns {boolean} false if the event was found and the function will be ignored, true if the event was not found and the function will be retriggered - */ -function eventNotFoundThenRetrigger(messageID, fn, ...rest) { - const eventID = select("event_message", "event_id", {message_id: messageID}).pluck().get() - if (eventID) { - debugRetrigger(`[retrigger] OK mid <-> eid = ${messageID} <-> ${eventID}`) - return false // event was found so don't retrigger - } - - debugRetrigger(`[retrigger] WAIT mid <-> eid = ${messageID} <-> ${eventID}`) - emitter.addListener(messageID, () => { - debugRetrigger(`[retrigger] TRIGGER mid = ${messageID}`) - fn(...rest) - }) - // if the event never arrives, don't trigger the callback, just clean up - setTimeout(() => { - if (emitter.listeners(messageID).length) { - debugRetrigger(`[retrigger] EXPIRE mid = ${messageID}`) - } - emitter.removeAllListeners(messageID) - }, 60 * 1000) // 1 minute - return true // event was not found, then retrigger -} - -/** - * Triggers any pending operations that were waiting on the corresponding event ID. - * @param {string} messageID - */ -function messageFinishedBridging(messageID) { - if (emitter.listeners(messageID).length) { - debugRetrigger(`[retrigger] EMIT mid = ${messageID}`) - } - emitter.emit(messageID) -} - -module.exports.eventNotFoundThenRetrigger = eventNotFoundThenRetrigger -module.exports.messageFinishedBridging = messageFinishedBridging diff --git a/d2m/actions/send-message.js b/d2m/actions/send-message.js index 6d100d4..346ac0d 100644 --- a/d2m/actions/send-message.js +++ b/d2m/actions/send-message.js @@ -39,8 +39,15 @@ async function sendMessage(message, channel, guild, row) { } else if (row && row.speedbump_webhook_id === message.webhook_id) { // Handle the PluralKit public instance if (row.speedbump_id === "466378653216014359") { - const pkMessage = await registerPkUser.fetchMessage(message.id) - senderMxid = await registerPkUser.syncUser(message.author, pkMessage, roomID) + const root = await registerPkUser.fetchMessage(message.id) + // Member is null if member was deleted. We just got this message, so member surely exists. + if (!root.member) { + const e = new Error("PK API did not return a member") + message["__pk_response__"] = root + console.error(root) + throw e + } + senderMxid = await registerPkUser.syncUser(message.author, root, roomID) } } diff --git a/d2m/converters/edit-to-changes.js b/d2m/converters/edit-to-changes.js index c80bb49..dd8e39f 100644 --- a/d2m/converters/edit-to-changes.js +++ b/d2m/converters/edit-to-changes.js @@ -30,8 +30,7 @@ function eventCanBeEdited(ev) { * @param {import("../../matrix/api")} api simple-as-nails dependency injection for the matrix API */ async function editToChanges(message, guild, api) { - // If it is a user edit, allow deleting old messages (e.g. they might have removed text from an image). - // If it is the system adding a generated embed to a message, don't delete old messages since the system only sends partial data. + // If it is a user edit, allow deleting old messages (e.g. they might have removed text from an image). If it is the system adding a generated embed to a message, don't delete old messages since the system only sends partial data. const isGeneratedEmbed = !("content" in message) @@ -39,21 +38,22 @@ async function editToChanges(message, guild, api) { const roomID = select("channel_room", "room_id", {channel_id: message.channel_id}).pluck().get() assert(roomID) - const oldEventRows = select("event_message", ["event_id", "event_type", "event_subtype", "part", "reaction_part"], {message_id: message.id}).all() - /** @type {string?} Null if we don't have a sender in the room, which will happen if it's a webhook's message. The bridge bot will do the edit instead. */ let senderMxid = null if (message.author) { senderMxid = from("sim").join("sim_member", "mxid").where({user_id: message.author.id, room_id: roomID}).pluck("mxid").get() || null } else { // Should be a system generated embed. We want the embed to be sent by the same user who sent the message, so that the messages get grouped in most clients. - const eventID = oldEventRows[0].event_id // a calling function should have already checked that there is at least one message to edit + const eventID = select("event_message", "event_id", {message_id: message.id}).pluck().get() + assert(eventID) // this should have been checked earlier in a calling function const event = await api.getEvent(roomID, eventID) if (utils.eventSenderIsFromDiscord(event.sender)) { senderMxid = event.sender } } + const oldEventRows = select("event_message", ["event_id", "event_type", "event_subtype", "part", "reaction_part"], {message_id: message.id}).all() + // Figure out what we will be replacing them with const newFallbackContent = await messageToEvent.messageToEvent(message, guild, {includeEditFallbackStar: true}, {api}) diff --git a/d2m/event-dispatcher.js b/d2m/event-dispatcher.js index 7f27b77..ec04750 100644 --- a/d2m/event-dispatcher.js +++ b/d2m/event-dispatcher.js @@ -33,8 +33,6 @@ const discordCommandHandler = sync.require("../discord/discord-command-handler") const mxUtils = require("../m2d/converters/utils") /** @type {import("./actions/speedbump")} */ const speedbump = sync.require("./actions/speedbump") -/** @type {import("./actions/retrigger")} */ -const retrigger = sync.require("./actions/retrigger") /** @type {any} */ // @ts-ignore bad types from semaphore const Semaphore = require("@chriscdn/promise-semaphore") @@ -254,8 +252,6 @@ module.exports = { // @ts-ignore await sendMessage.sendMessage(message, channel, guild, row), await discordCommandHandler.execute(message, channel, guild) - - retrigger.messageFinishedBridging(message.id) }, /** @@ -263,14 +259,6 @@ module.exports = { * @param {DiscordTypes.GatewayMessageUpdateDispatchData} data */ async onMessageUpdate(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. - if (!(typeof data.content === "string" || "embeds" in data)) return - - // Deal with Eventual Consistency(TM) - if (retrigger.eventNotFoundThenRetrigger(data.id, module.exports.onMessageUpdate, client, data)) return - if (data.webhook_id) { const row = select("webhook", "webhook_id", {webhook_id: data.webhook_id}).pluck().get() if (row) return // The message was sent by the bridge's own webhook on discord. We don't want to reflect this back, so just drop it. @@ -282,16 +270,21 @@ module.exports = { const {affected, row} = await speedbump.maybeDoSpeedbump(data.channel_id, data.id) if (affected) return - /** @type {DiscordTypes.GatewayMessageCreateDispatchData} */ - // @ts-ignore - const message = 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. + if (typeof data.content === "string" || "embeds" in data) { + /** @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 editMessage.editMessage(message, guild, row) + 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 editMessage.editMessage(message, guild, row) + } }, /** @@ -318,7 +311,6 @@ module.exports = { */ async onMessageDelete(client, data) { speedbump.onMessageDelete(data.id) - if (retrigger.eventNotFoundThenRetrigger(data.id, module.exports.onMessageDelete, client, data)) return await deleteMessage.deleteMessage(data) },