diff --git a/scripts/backfill.js b/scripts/backfill.js index c0c440e..27600f0 100644 --- a/scripts/backfill.js +++ b/scripts/backfill.js @@ -38,8 +38,12 @@ passthrough.select = orm.select /** @type {import("../src/d2m/event-dispatcher")}*/ const eventDispatcher = sync.require("../src/d2m/event-dispatcher") -/** @type {import("../src/d2m/actions/create-room")} */ -const createRoom = sync.require("../src/d2m/actions/create-room") + +const roomID = passthrough.select("channel_room", "room_id", {channel_id: channelID}).pluck().get() +if (!roomID) { + console.error("Please choose a channel that's already bridged.") + process.exit(1) +} ;(async () => { await discord.cloud.connect() @@ -56,18 +60,6 @@ async function event(event) { if (!channel) return const guild_id = event.d.id - let roomID = passthrough.select("channel_room", "room_id", {channel_id: channelID}).pluck().get() - if (!roomID) { - console.log(`Channel #${channel.name} is not bridged yet. Attempting to auto-create...`) - try { - roomID = await createRoom.syncRoom(channelID) - console.log(`Successfully bridged to new room: ${roomID}`) - } catch (e) { - console.error(`Failed to auto-create room: ${e.message}`) - process.exit(1) - } - } - let last = backfill.prepare("SELECT cast(max(message_id) as TEXT) FROM backfill WHERE channel_id = ?").pluck().get(channelID) || "0" console.log(`OK, processing messages for #${channel.name}, continuing from ${last}`) diff --git a/src/d2m/actions/remove-member.js b/src/d2m/actions/remove-member.js deleted file mode 100644 index f4d18cf..0000000 --- a/src/d2m/actions/remove-member.js +++ /dev/null @@ -1,124 +0,0 @@ -// @ts-check - -const DiscordTypes = require("discord-api-types/v10") - -const passthrough = require("../../passthrough") -const {discord, sync, db, select, from} = passthrough -/** @type {import("../../matrix/api")} */ -const api = sync.require("../../matrix/api") - -/** - * Make a specific sim leave all rooms in a guild and clean up the database. - * @param {string} mxid - * @param {string} guildID - * @returns {Promise} number of rooms left - */ -async function removeSimFromGuild(mxid, guildID) { - const rooms = from("sim_member") - .join("channel_room", "room_id") - .where({mxid, guild_id: guildID}) - .pluck("room_id") - .all() - - for (const roomID of rooms) { - try { - await api.leaveRoom(roomID, mxid) - } catch (e) { - // Room may no longer exist or sim may already have left - console.log(`[remove member] failed to leave room ${roomID}: ${e}`) - } - db.prepare("DELETE FROM sim_member WHERE mxid = ? AND room_id = ?").run(mxid, roomID) - } - - return rooms.length -} - -/** - * Remove a user's sim and their PluralKit proxy sims from all rooms in a guild. - * Called when a Discord user leaves a guild. - * @param {string} userID Discord user ID - * @param {string} guildID Discord guild ID - */ -async function removeMember(userID, guildID) { - // Remove the user's own sim - const mxid = select("sim", "mxid", {user_id: userID}).pluck().get() - if (mxid) { - const count = await removeSimFromGuild(mxid, guildID) - if (count) console.log(`[remove member] removed sim for ${userID} from ${count} rooms in guild ${guildID}`) - } - - // Remove PluralKit proxy sims owned by this user - const pkUserIDs = select("sim_proxy", "user_id", {proxy_owner_id: userID}).pluck().all() - for (const pkUserID of pkUserIDs) { - const pkMxid = select("sim", "mxid", {user_id: pkUserID}).pluck().get() - if (!pkMxid) continue - const count = await removeSimFromGuild(pkMxid, guildID) - if (count) console.log(`[remove member] removed pk sim for ${pkUserID} (owner: ${userID}) from ${count} rooms in guild ${guildID}`) - } -} - -/** - * Backfill: check all sims in guild rooms and remove those whose Discord users have left the guild. - * Called on GUILD_CREATE to catch removals that happened while the bridge was offline. - * @param {DiscordTypes.GatewayGuildCreateDispatchData} guild - */ -async function checkMissedMembers(guild) { - if (guild.unavailable) return - - // Find all distinct regular Discord user IDs with sims in this guild's rooms - // Exclude PK sims (UUIDs with dashes) and webhook sims (with underscores) - const rows = from("sim_member") - .join("channel_room", "room_id") - .join("sim", "mxid") - .where({guild_id: guild.id}) - .and("AND user_id NOT LIKE '%-%' AND user_id NOT LIKE '%\\_%' ESCAPE '\\'") - .pluck("user_id") - .all() - - const userIDs = [...new Set(rows)] - - for (const userID of userIDs) { - try { - await discord.snow.guild.getGuildMember(guild.id, userID) - } catch (e) { - if (String(e).includes("10007") || String(e).includes("Unknown Member")) { - console.log(`[remove member] backfill: user ${userID} is no longer in guild ${guild.id}`) - await removeMember(userID, guild.id) - } - // Other errors (rate limits, network issues) - skip this user - } - } - - // Also check PK proxy owners who may not have a regular sim but whose PK sims are in guild rooms - const pkRows = from("sim_member") - .join("channel_room", "room_id") - .join("sim", "mxid") - .join("sim_proxy", "user_id") - .where({guild_id: guild.id}) - .pluck("proxy_owner_id") - .all() - - const pkOwnerIDs = [...new Set(pkRows)].filter(id => !userIDs.includes(id)) - - for (const ownerID of pkOwnerIDs) { - try { - await discord.snow.guild.getGuildMember(guild.id, ownerID) - } catch (e) { - if (String(e).includes("10007") || String(e).includes("Unknown Member")) { - console.log(`[remove member] backfill: pk owner ${ownerID} is no longer in guild ${guild.id}`) - // Only remove PK sims for this owner, not their own sim (they don't have one in this guild) - const pkUserIDs = select("sim_proxy", "user_id", {proxy_owner_id: ownerID}).pluck().all() - for (const pkUserID of pkUserIDs) { - const pkMxid = select("sim", "mxid", {user_id: pkUserID}).pluck().get() - if (!pkMxid) continue - const count = await removeSimFromGuild(pkMxid, guild.id) - if (count) console.log(`[remove member] backfill: removed pk sim for ${pkUserID} (owner: ${ownerID}) from ${count} rooms in guild ${guild.id}`) - } - } - } - } -} - -module.exports.removeSimFromGuild = removeSimFromGuild -module.exports.removeMember = removeMember -module.exports.checkMissedMembers = checkMissedMembers diff --git a/src/d2m/actions/remove-member.test.js b/src/d2m/actions/remove-member.test.js deleted file mode 100644 index 383ba7d..0000000 --- a/src/d2m/actions/remove-member.test.js +++ /dev/null @@ -1,202 +0,0 @@ -// @ts-check - -const {test} = require("supertape") -const {removeSimFromGuild, removeMember, checkMissedMembers} = require("./remove-member") -const passthrough = require("../../passthrough") -const {db, sync, discord} = passthrough - -/** @type {import("../../matrix/api")} */ -const api = sync.require("../../matrix/api") - -function setupTestSim(userID, simName, mxid) { - db.prepare("INSERT OR IGNORE INTO sim (user_id, username, sim_name, mxid) VALUES (?, ?, ?, ?)").run(userID, simName, simName, mxid) -} - -function setupTestSimMember(mxid, roomID) { - db.prepare("INSERT OR IGNORE INTO sim_member (mxid, room_id, hashed_profile_content) VALUES (?, ?, NULL)").run(mxid, roomID) -} - -function cleanupTestSim(userID) { - const mxid = db.prepare("SELECT mxid FROM sim WHERE user_id = ?").pluck().get(userID) - if (mxid) { - db.prepare("DELETE FROM sim_member WHERE mxid = ?").run(mxid) - } - db.prepare("DELETE FROM sim WHERE user_id = ?").run(userID) -} - -test("remove-member: removeSimFromGuild removes sim from all guild rooms", async t => { - const mxid = "@_ooye_testrm1:cadence.moe" - const guildID = "112760669178241024" - const roomID1 = "!kLRqKKUQXcibIMtOpl:cadence.moe" - const roomID2 = "!fGgIymcYWOqjbSRUdV:cadence.moe" - - setupTestSim("999999990", "testrm1", mxid) - setupTestSimMember(mxid, roomID1) - setupTestSimMember(mxid, roomID2) - - const leftRooms = [] - const originalLeaveRoom = api.leaveRoom - api.leaveRoom = async (roomID, userMxid) => { leftRooms.push({roomID, mxid: userMxid}) } - - try { - const count = await removeSimFromGuild(mxid, guildID) - - t.equal(count, 2) - t.equal(leftRooms.length, 2) - t.ok(leftRooms.some(r => r.roomID === roomID1)) - t.ok(leftRooms.some(r => r.roomID === roomID2)) - - const remaining = db.prepare("SELECT COUNT(*) FROM sim_member WHERE mxid = ?").pluck().get(mxid) - t.equal(remaining, 0) - } finally { - api.leaveRoom = originalLeaveRoom - cleanupTestSim("999999990") - } -}) - -test("remove-member: removeSimFromGuild only affects rooms in the specified guild", async t => { - const mxid = "@_ooye_testrm2:cadence.moe" - const guildID = "112760669178241024" - const guildRoom = "!kLRqKKUQXcibIMtOpl:cadence.moe" // guild 112760669178241024 - const otherGuildRoom = "!BnKuBPCvyfOkhcUjEu:cadence.moe" // guild 66192955777486848 - - setupTestSim("999999991", "testrm2", mxid) - setupTestSimMember(mxid, guildRoom) - setupTestSimMember(mxid, otherGuildRoom) - - const leftRooms = [] - const originalLeaveRoom = api.leaveRoom - api.leaveRoom = async (roomID, userMxid) => { leftRooms.push({roomID, mxid: userMxid}) } - - try { - const count = await removeSimFromGuild(mxid, guildID) - - t.equal(count, 1) - t.equal(leftRooms[0].roomID, guildRoom) - - const otherRemaining = db.prepare("SELECT COUNT(*) FROM sim_member WHERE mxid = ? AND room_id = ?").pluck().get(mxid, otherGuildRoom) - t.equal(otherRemaining, 1, "other guild's room should be untouched") - } finally { - api.leaveRoom = originalLeaveRoom - cleanupTestSim("999999991") - } -}) - -test("remove-member: removeSimFromGuild handles leaveRoom errors gracefully", async t => { - const mxid = "@_ooye_testrm3:cadence.moe" - const guildID = "112760669178241024" - const roomID = "!kLRqKKUQXcibIMtOpl:cadence.moe" - - setupTestSim("999999992", "testrm3", mxid) - setupTestSimMember(mxid, roomID) - - const originalLeaveRoom = api.leaveRoom - api.leaveRoom = async () => { throw new Error("not in room") } - - try { - const count = await removeSimFromGuild(mxid, guildID) - t.equal(count, 1) - - const remaining = db.prepare("SELECT COUNT(*) FROM sim_member WHERE mxid = ?").pluck().get(mxid) - t.equal(remaining, 0, "sim_member should be deleted even if leaveRoom fails") - } finally { - api.leaveRoom = originalLeaveRoom - cleanupTestSim("999999992") - } -}) - -test("remove-member: removeMember removes user sim and their PK proxy sims", async t => { - const userID = "999999993" - const mxid = "@_ooye_testrm4:cadence.moe" - const pkUserID = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee" - const pkMxid = "@_ooye__pk_testrmpk:cadence.moe" - const guildID = "112760669178241024" - const roomID = "!kLRqKKUQXcibIMtOpl:cadence.moe" - - setupTestSim(userID, "testrm4", mxid) - setupTestSimMember(mxid, roomID) - setupTestSim(pkUserID, "_pk_testrmpk", pkMxid) - setupTestSimMember(pkMxid, roomID) - db.prepare("INSERT OR IGNORE INTO sim_proxy (user_id, proxy_owner_id, displayname) VALUES (?, ?, ?)").run(pkUserID, userID, "Test PK") - - const leftRooms = [] - const originalLeaveRoom = api.leaveRoom - api.leaveRoom = async (roomID, userMxid) => { leftRooms.push({roomID, mxid: userMxid}) } - - try { - await removeMember(userID, guildID) - - t.equal(leftRooms.length, 2) - t.ok(leftRooms.some(r => r.mxid === mxid), "user sim should leave") - t.ok(leftRooms.some(r => r.mxid === pkMxid), "pk sim should leave") - - t.equal(db.prepare("SELECT COUNT(*) FROM sim_member WHERE mxid = ?").pluck().get(mxid), 0) - t.equal(db.prepare("SELECT COUNT(*) FROM sim_member WHERE mxid = ?").pluck().get(pkMxid), 0) - } finally { - api.leaveRoom = originalLeaveRoom - db.prepare("DELETE FROM sim_proxy WHERE user_id = ?").run(pkUserID) - cleanupTestSim(userID) - cleanupTestSim(pkUserID) - } -}) - -test("remove-member: removeMember does nothing for unknown user", async t => { - const leftRooms = [] - const originalLeaveRoom = api.leaveRoom - api.leaveRoom = async (roomID, userMxid) => { leftRooms.push({roomID, mxid: userMxid}) } - - try { - await removeMember("000000000000000000", "112760669178241024") - t.equal(leftRooms.length, 0) - } finally { - api.leaveRoom = originalLeaveRoom - } -}) - -test("remove-member: checkMissedMembers removes sims for departed users", async t => { - const userID = "999999994" - const mxid = "@_ooye_testrm5:cadence.moe" - const guildID = "112760669178241024" - const roomID = "!kLRqKKUQXcibIMtOpl:cadence.moe" - - setupTestSim(userID, "testrm5", mxid) - setupTestSimMember(mxid, roomID) - - const leftRooms = [] - const originalLeaveRoom = api.leaveRoom - api.leaveRoom = async (roomID, userMxid) => { leftRooms.push({roomID, mxid: userMxid}) } - - const originalSnow = discord.snow - discord.snow = { - guild: { - getGuildMember: async (gid, uid) => { - if (uid === userID) throw new Error('{"message": "Unknown Member", "code": 10007}') - return {} - } - } - } - - try { - await checkMissedMembers({id: guildID}) - - t.ok(leftRooms.some(r => r.mxid === mxid), "departed user's sim should be removed") - t.equal(db.prepare("SELECT COUNT(*) FROM sim_member WHERE mxid = ?").pluck().get(mxid), 0) - } finally { - api.leaveRoom = originalLeaveRoom - discord.snow = originalSnow - cleanupTestSim(userID) - } -}) - -test("remove-member: checkMissedMembers skips unavailable guilds", async t => { - const leftRooms = [] - const originalLeaveRoom = api.leaveRoom - api.leaveRoom = async (roomID, userMxid) => { leftRooms.push({roomID, mxid: userMxid}) } - - try { - await checkMissedMembers({id: "112760669178241024", unavailable: true}) - t.equal(leftRooms.length, 0, "should not process unavailable guilds") - } finally { - api.leaveRoom = originalLeaveRoom - } -}) diff --git a/src/d2m/converters/message-to-event.js b/src/d2m/converters/message-to-event.js index 4ec28c2..7f77b81 100644 --- a/src/d2m/converters/message-to-event.js +++ b/src/d2m/converters/message-to-event.js @@ -769,21 +769,7 @@ async function messageToEvent(message, guild, options = {}, di) { // Then scheduled events if (message.content && di?.snow) { for (const match of [...message.content.matchAll(/discord\.gg\/([A-Za-z0-9]+)\?event=([0-9]{18,})/g)]) { // snowflake has minimum 18 because the events feature is at least that old - let invite - try { - invite = await di.snow.invite.getInvite(match[1], {guild_scheduled_event_id: match[2]}) - } catch (e) { - // Skip expired events and invites - if (e.code === 10006 || e.httpStatus === 404) { - console.warn(`[Backfill] Skipped expired scheduled event: ${match[0]}`) - const fallbackBody = `[Expired Scheduled Event: ${match[0]}]` - const fallbackHtml = `
Expired Scheduled Event: ${match[0]}
` - await addTextEvent(fallbackBody, fallbackHtml, "m.notice") - continue - } - throw e - } - + const invite = await di.snow.invite.getInvite(match[1], {guild_scheduled_event_id: match[2]}) const event = invite.guild_scheduled_event if (!event) continue // the event ID provided was not valid diff --git a/src/d2m/converters/message-to-event.test.js b/src/d2m/converters/message-to-event.test.js index 166c3d7..1a73aea 100644 --- a/src/d2m/converters/message-to-event.test.js +++ b/src/d2m/converters/message-to-event.test.js @@ -1538,38 +1538,6 @@ test("message2event: vc invite event renders embed with room link", async t => { ]) }) -test("message2event: expired event invite renders fallback notice", async t => { - const events = await messageToEvent({content: "https://discord.gg/placeholder?event=1381190945646710824"}, {}, {}, { - snow: { - invite: { - getInvite: async () => { - const error = new Error("Unknown Invite") - error.code = 10006 - throw error - } - } - } - }) - t.deepEqual(events, [ - { - $type: "m.room.message", - body: "https://discord.gg/placeholder?event=1381190945646710824", - format: "org.matrix.custom.html", - formatted_body: "https://discord.gg/placeholder?event=1381190945646710824", - "m.mentions": {}, - msgtype: "m.text", - }, - { - $type: "m.room.message", - msgtype: "m.notice", - body: "[Expired Scheduled Event: discord.gg/placeholder?event=1381190945646710824]", - format: "org.matrix.custom.html", - formatted_body: "
Expired Scheduled Event: discord.gg/placeholder?event=1381190945646710824
", - "m.mentions": {} - } - ]) -}) - test("message2event: channel links are converted even inside lists (parser post-processer descends into list items)", async t => { let called = 0 const events = await messageToEvent({ diff --git a/src/d2m/discord-client.js b/src/d2m/discord-client.js index 4b3719c..7b0fcf8 100644 --- a/src/d2m/discord-client.js +++ b/src/d2m/discord-client.js @@ -23,7 +23,7 @@ class DiscordClient { /** @type {import("cloudstorm").IClientOptions["intents"]} */ const intents = [ "DIRECT_MESSAGES", "DIRECT_MESSAGE_REACTIONS", "DIRECT_MESSAGE_TYPING", - "GUILDS", "GUILD_EMOJIS_AND_STICKERS", "GUILD_MEMBERS", "GUILD_MESSAGES", "GUILD_MESSAGE_REACTIONS", "GUILD_MESSAGE_TYPING", "GUILD_WEBHOOKS", "GUILD_MESSAGE_POLLS", + "GUILDS", "GUILD_EMOJIS_AND_STICKERS", "GUILD_MESSAGES", "GUILD_MESSAGE_REACTIONS", "GUILD_MESSAGE_TYPING", "GUILD_WEBHOOKS", "GUILD_MESSAGE_POLLS", "MESSAGE_CONTENT" ] if (reg.ooye.receive_presences !== false) intents.push("GUILD_PRESENCES") diff --git a/src/d2m/discord-packets.js b/src/d2m/discord-packets.js index 60ed3ac..8cf2fde 100644 --- a/src/d2m/discord-packets.js +++ b/src/d2m/discord-packets.js @@ -51,7 +51,6 @@ const utils = { await eventDispatcher.checkMissedExpressions(message.d) await eventDispatcher.checkMissedPins(client, message.d) await eventDispatcher.checkMissedMessages(client, message.d) - await eventDispatcher.checkMissedMembers(message.d) } catch (e) { console.error("Failed to sync missed events. To retry, please fix this error and restart OOYE:") console.error(e) diff --git a/src/d2m/event-dispatcher.js b/src/d2m/event-dispatcher.js index 8194ab1..01bbc67 100644 --- a/src/d2m/event-dispatcher.js +++ b/src/d2m/event-dispatcher.js @@ -38,8 +38,6 @@ const vote = sync.require("./actions/poll-vote") const matrixEventDispatcher = sync.require("../m2d/event-dispatcher") /** @type {import("../discord/interactions/matrix-info")} */ const matrixInfoInteraction = sync.require("../discord/interactions/matrix-info") -/** @type {import("./actions/remove-member")} */ -const removeMember = sync.require("./actions/remove-member") const {Semaphore} = require("@chriscdn/promise-semaphore") const checkMissedPinsSema = new Semaphore() @@ -415,24 +413,5 @@ module.exports = { const status = data.status if (!status) return setPresence.presenceTracker.incomingPresence(data.user.id, data.guild_id, status) - }, - - /** - * When a Discord user leaves (or is kicked/banned from) the guild, make their sims leave the bridged Matrix rooms. - * Also removes PluralKit proxy sims owned by the departing user. - * @param {import("./discord-client")} client - * @param {DiscordTypes.GatewayGuildMemberRemoveDispatchData} data - */ - async GUILD_MEMBER_REMOVE(client, data) { - if (data.user.id === client.user.id) return // Don't process if the bot itself is removed - await removeMember.removeMember(data.user.id, data.guild_id) - }, - - /** - * When logging back in, check for sims whose Discord users have left the guild while the bridge was offline. - * @param {DiscordTypes.GatewayGuildCreateDispatchData} guild - */ - async checkMissedMembers(guild) { - await removeMember.checkMissedMembers(guild) } } diff --git a/test/test.js b/test/test.js index 6094e47..e05b687 100644 --- a/test/test.js +++ b/test/test.js @@ -13,10 +13,7 @@ const {green} = require("ansi-colors") const passthrough = require("../src/passthrough") const db = new sqlite(":memory:") -const readReg = require("../src/matrix/read-registration") -readReg.reg = readReg.getTemplateRegistration("cadence.moe") -const {reg} = readReg -reg.url = "http://localhost:6693" +const {reg} = require("../src/matrix/read-registration") reg.ooye.discord_token = "Njg0MjgwMTkyNTUzODQ0NzQ3.Xl3zlw.baby" reg.ooye.server_origin = "https://matrix.cadence.moe" // so that tests will pass even when hard-coded reg.ooye.server_name = "cadence.moe" @@ -148,7 +145,6 @@ file._actuallyUploadDiscordFileToMxc = function(url, res) { throw new Error(`Not require("../src/d2m/actions/create-room.test") require("../src/d2m/actions/create-space.test") require("../src/d2m/actions/register-user.test") - require("../src/d2m/actions/remove-member.test") require("../src/d2m/converters/edit-to-changes.test") require("../src/d2m/converters/emoji-to-key.test") require("../src/d2m/converters/find-mentions.test")