diff --git a/d2m/actions/delete-message.js b/d2m/actions/delete-message.js index cca5d25..496d827 100644 --- a/d2m/actions/delete-message.js +++ b/d2m/actions/delete-message.js @@ -4,21 +4,25 @@ const passthrough = require("../../passthrough") const {sync, db, select, from} = passthrough /** @type {import("../../matrix/api")} */ const api = sync.require("../../matrix/api") +/** @type {import("./speedbump")} */ +const speedbump = sync.require("./speedbump") /** * @param {import("discord-api-types/v10").GatewayMessageDeleteDispatchData} data */ async function deleteMessage(data) { - const roomID = select("channel_room", "room_id", {channel_id: data.channel_id}).pluck().get() - if (!roomID) return + const row = select("channel_room", ["room_id", "speedbump_id", "speedbump_checked"], {channel_id: data.channel_id}).get() + if (!row) return const eventsToRedact = select("event_message", "event_id", {message_id: data.id}).pluck().all() db.prepare("DELETE FROM message_channel WHERE message_id = ?").run(data.id) db.prepare("DELETE FROM event_message WHERE message_id = ?").run(data.id) for (const eventID of eventsToRedact) { // Unfortunately, we can't specify a sender to do the redaction as, unless we find out that info via the audit logs - await api.redactEvent(roomID, eventID) + await api.redactEvent(row.room_id, eventID) } + + speedbump.updateCache(data.channel_id, row.speedbump_id, row.speedbump_checked) } /** diff --git a/d2m/actions/speedbump.js b/d2m/actions/speedbump.js new file mode 100644 index 0000000..b56cbf6 --- /dev/null +++ b/d2m/actions/speedbump.js @@ -0,0 +1,51 @@ +// @ts-check + +const DiscordTypes = require("discord-api-types/v10") +const passthrough = require("../../passthrough") +const {discord, db} = passthrough + +const SPEEDBUMP_SPEED = 4000 // 4 seconds delay +const SPEEDBUMP_UPDATE_FREQUENCY = 2 * 60 * 60 // 2 hours + +/** @type {Set} */ +const KNOWN_BOTS = new Set([ + "466378653216014359" // PluralKit +]) + +/** + * Fetch new speedbump data for the channel and put it in the database as cache + * @param {string} channelID + * @param {string?} speedbumpID + * @param {number?} speedbumpChecked + */ +async function updateCache(channelID, speedbumpID, speedbumpChecked) { + const now = Math.floor(Date.now() / 1000) + if (speedbumpChecked && now - speedbumpChecked < SPEEDBUMP_UPDATE_FREQUENCY) return + const webhooks = await discord.snow.webhook.getChannelWebhooks(channelID) + const found = webhooks.find(b => KNOWN_BOTS.has(b.application_id))?.application_id || null + db.prepare("UPDATE channel_room SET speedbump_id = ?, speedbump_checked = ? WHERE channel_id = ?").run(found, now, channelID) +} + +/** @type {Set} set of messageID */ +const bumping = new Set() + +/** + * Slow down a message. After it passes the speedbump, return whether it's okay or if it's been deleted. + * @param {string} messageID + */ +async function doSpeedbump(messageID) { + bumping.add(messageID) + await new Promise(resolve => setTimeout(resolve, SPEEDBUMP_SPEED)) + return !bumping.delete(messageID) +} + +/** + * @param {string} messageID + */ +function onMessageDelete(messageID) { + bumping.delete(messageID) +} + +module.exports.updateCache = updateCache +module.exports.doSpeedbump = doSpeedbump +module.exports.onMessageDelete = onMessageDelete diff --git a/d2m/discord-client.js b/d2m/discord-client.js index b1a1e81..80dcbcf 100644 --- a/d2m/discord-client.js +++ b/d2m/discord-client.js @@ -47,7 +47,16 @@ class DiscordClient { if (listen !== "no") { this.cloud.on("event", message => discordPackets.onPacket(this, message, listen)) } - this.cloud.on("error", console.error) + + 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/d2m/event-dispatcher.js b/d2m/event-dispatcher.js index 3544064..3a80d0a 100644 --- a/d2m/event-dispatcher.js +++ b/d2m/event-dispatcher.js @@ -31,6 +31,8 @@ const dUtils = sync.require("../discord/utils") const discordCommandHandler = sync.require("../discord/discord-command-handler") /** @type {import("../m2d/converters/utils")} */ const mxUtils = require("../m2d/converters/utils") +/** @type {import("./actions/speedbump")} */ +const speedbump = sync.require("./actions/speedbump") /** @type {any} */ // @ts-ignore bad types from semaphore const Semaphore = require("@chriscdn/promise-semaphore") @@ -236,9 +238,12 @@ module.exports = { if (message.author.username === "Deleted User") return // Nothing we can do for deleted users. if (message.webhook_id) { const row = select("webhook", "webhook_id", {webhook_id: message.webhook_id}).pluck().get() - if (row) { - // The message was sent by the bridge's own webhook on discord. We don't want to reflect this back, so just drop it. - return + 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. + } else { + const speedbumpID = select("channel_room", "speedbump_id", {channel_id: message.channel_id}).pluck().get() + if (speedbumpID) { + const affected = await speedbump.doSpeedbump(message.id) + if (affected) return } } const channel = client.channels.get(message.channel_id) @@ -299,6 +304,7 @@ module.exports = { * @param {DiscordTypes.GatewayMessageDeleteDispatchData} data */ async onMessageDelete(client, data) { + speedbump.onMessageDelete(data.id) await deleteMessage.deleteMessage(data) }, diff --git a/db/migrations/0009-add-speedbump-id.sql b/db/migrations/0009-add-speedbump-id.sql new file mode 100644 index 0000000..e971146 --- /dev/null +++ b/db/migrations/0009-add-speedbump-id.sql @@ -0,0 +1,6 @@ +BEGIN TRANSACTION; + +ALTER TABLE channel_room ADD COLUMN speedbump_id TEXT; +ALTER TABLE channel_room ADD COLUMN speedbump_checked INTEGER; + +COMMIT; diff --git a/db/orm-defs.d.ts b/db/orm-defs.d.ts index 64e5c77..f0f9a67 100644 --- a/db/orm-defs.d.ts +++ b/db/orm-defs.d.ts @@ -7,6 +7,8 @@ export type Models = { thread_parent: string | null custom_avatar: string | null last_bridged_pin_timestamp: number | null + speedbump_id: string | null + speedbump_checked: number | null } event_message: { diff --git a/docs/pluralkit-notetaking.md b/docs/pluralkit-notetaking.md index 697cdee..d03ddb7 100644 --- a/docs/pluralkit-notetaking.md +++ b/docs/pluralkit-notetaking.md @@ -85,7 +85,9 @@ OOYE's speedbump will prevent the edit command appearing at all on Matrix-side, ## Database schema -TBD +* channel_room + + speedbump_id - the ID of the webhook that may be proxying in this channel + + speedbump_checked - time in unix seconds when the webhooks were last queried ## Unsolved problems diff --git a/m2d/converters/event-to-message.test.js b/m2d/converters/event-to-message.test.js index 98c153f..4f1c1dd 100644 --- a/m2d/converters/event-to-message.test.js +++ b/m2d/converters/event-to-message.test.js @@ -581,10 +581,6 @@ test("event2message: ordered list start attribute works", async t => { room_id: '!cBxtVRxDlZvSVhJXVK:cadence.moe', sender: '@Milan:tchncs.de', type: 'm.room.message', - }, {}, { - api: { - getStateEvent: async () => ({displayname: "Milan"}) - } }), { ensureJoined: [], @@ -2194,7 +2190,7 @@ test("event2message: mentioning events falls back to original link when the chan } }, {}, { api: { - /* c8 skip next 3 */ + /* c8 ignore next 3 */ async getEvent() { t.fail("getEvent should not be called because it should quit early due to no channel-guild") } diff --git a/stdin.js b/stdin.js index da69d7c..5e23f72 100644 --- a/stdin.js +++ b/stdin.js @@ -17,6 +17,7 @@ const file = sync.require("./matrix/file") const sendEvent = sync.require("./m2d/actions/send-event") const eventDispatcher = sync.require("./d2m/event-dispatcher") const updatePins = sync.require("./d2m/actions/update-pins") +const speedbump = sync.require("./d2m/actions/speedbump") const ks = sync.require("./matrix/kstate") const guildID = "112760669178241024"