From f4c1ea7c7f7d5a265b84ce464cd8e9e26d934a32 Mon Sep 17 00:00:00 2001 From: Elliu Date: Tue, 4 Nov 2025 23:54:41 +0900 Subject: [PATCH 1/4] /unlink-space: properly leave guild and clean DB --- src/web/routes/link.js | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/src/web/routes/link.js b/src/web/routes/link.js index 4698dfe6..e3da17f2 100644 --- a/src/web/routes/link.js +++ b/src/web/routes/link.js @@ -266,31 +266,38 @@ as.router.post("/api/unlink-space", defineEventHandler(async event => { await validateGuildAccess(event, guild_id) const spaceID = select("guild_space", "space_id", {guild_id: guild_id}).pluck().get() - if (!spaceID) + if (!spaceID) { throw createError({status: 400, message: "Bad Request", data: "Matrix space does not exist or bot has not linked it"}) + } const linkedChannels = select("channel_room", ["channel_id", "room_id", "name", "nick"], {guild_id: guild_id}).all() for (const channel of linkedChannels) { await doRoomUnlink(event, channel.channel_id, guild_id) + + // FIXME: probably fix the underlying issue instead: + // If not waiting for ~1s, then the room is half unbridged: + // the resources in the room is not properly cleaned up, meaning that the sim users + // and the bridge user are not power demoted nor leave the room + // The entry from the channel_room table is not deleted + // After that, writing in the discord channel does nothing, + // and writing in the matrix channel spawns an error for not finding guild_id + await new Promise(r => setTimeout(r, 5000)); } const remainingLinkedChannels = select("channel_room", ["channel_id", "room_id", "name", "nick"], {guild_id: guild_id}).all() - if (remainingLinkedChannels.length !== 0) + if (remainingLinkedChannels.length !== 0) { throw createError({status: 500, message: "Internal Server Error", data: "Some linked room still exists after trying to unlink all of them. Aborting the space unlinking..."}) + } await api.setUserPower(spaceID, me, 0) await api.leaveRoom(spaceID) db.prepare("DELETE FROM guild_space WHERE guild_id=? AND space_id=?").run(guild_id, spaceID) + db.prepare("DELETE FROM guild_active WHERE guild_id=?").run(guild_id) + await discord.snow.user.leaveGuild(guild_id) + db.prepare("DELETE FROM invite WHERE room_id=?").run(spaceID) - // NOTE: not deleting from guild_active as this can lead to inconsistent state: - // if we only delete from DB, the guild is still displayed on the top-right dropdown, - // but when selected we get the "Please add the bot to your server using the buttons on the home page." page - // - // So either keep as-is, or delete from guild_active, but also leave the discord guild? Not sure if we want that or not - // db.prepare("DELETE FROM guild_active WHERE guild_id=?").run(guild_id) - - setResponseHeader(event, "HX-Refresh", "true") - return null // 204 + setResponseHeader(event, "HX-Redirect", "/") + return null })) From ccc10564f1e33ab277bc15f360b8c65f2d0ea867 Mon Sep 17 00:00:00 2001 From: Elliu Date: Wed, 5 Nov 2025 00:04:13 +0900 Subject: [PATCH 2/4] fix matrix / db resource cleanup on space unlink --- src/d2m/actions/create-room.js | 6 +++--- src/web/routes/link.js | 15 +++------------ 2 files changed, 6 insertions(+), 15 deletions(-) diff --git a/src/d2m/actions/create-room.js b/src/d2m/actions/create-room.js index ff5782dc..7395abb4 100644 --- a/src/d2m/actions/create-room.js +++ b/src/d2m/actions/create-room.js @@ -459,12 +459,12 @@ async function unbridgeDeletedChannel(channel, guildID) { const webhook = select("webhook", ["webhook_id", "webhook_token"], {channel_id: channel.id}).get() if (webhook) { await discord.snow.webhook.deleteWebhook(webhook.webhook_id, webhook.webhook_token) - db.prepare("DELETE FROM webhook WHERE channel_id = ?").run(channel.id) + await db.prepare("DELETE FROM webhook WHERE channel_id = ?").run(channel.id) } // delete room from database - db.prepare("DELETE FROM member_cache WHERE room_id = ?").run(roomID) - db.prepare("DELETE FROM channel_room WHERE room_id = ? AND channel_id = ?").run(roomID, channel.id) // cascades to most other tables, like messages + await db.prepare("DELETE FROM member_cache WHERE room_id = ?").run(roomID) + await db.prepare("DELETE FROM channel_room WHERE room_id = ? AND channel_id = ?").run(roomID, channel.id) // cascades to most other tables, like messages if (!botInRoom) return diff --git a/src/web/routes/link.js b/src/web/routes/link.js index e3da17f2..8f844ab1 100644 --- a/src/web/routes/link.js +++ b/src/web/routes/link.js @@ -274,15 +274,6 @@ as.router.post("/api/unlink-space", defineEventHandler(async event => { for (const channel of linkedChannels) { await doRoomUnlink(event, channel.channel_id, guild_id) - - // FIXME: probably fix the underlying issue instead: - // If not waiting for ~1s, then the room is half unbridged: - // the resources in the room is not properly cleaned up, meaning that the sim users - // and the bridge user are not power demoted nor leave the room - // The entry from the channel_room table is not deleted - // After that, writing in the discord channel does nothing, - // and writing in the matrix channel spawns an error for not finding guild_id - await new Promise(r => setTimeout(r, 5000)); } const remainingLinkedChannels = select("channel_room", ["channel_id", "room_id", "name", "nick"], {guild_id: guild_id}).all() @@ -293,10 +284,10 @@ as.router.post("/api/unlink-space", defineEventHandler(async event => { await api.setUserPower(spaceID, me, 0) await api.leaveRoom(spaceID) - db.prepare("DELETE FROM guild_space WHERE guild_id=? AND space_id=?").run(guild_id, spaceID) - db.prepare("DELETE FROM guild_active WHERE guild_id=?").run(guild_id) + await db.prepare("DELETE FROM guild_space WHERE guild_id=? AND space_id=?").run(guild_id, spaceID) + await db.prepare("DELETE FROM guild_active WHERE guild_id=?").run(guild_id) await discord.snow.user.leaveGuild(guild_id) - db.prepare("DELETE FROM invite WHERE room_id=?").run(spaceID) + await db.prepare("DELETE FROM invite WHERE room_id=?").run(spaceID) setResponseHeader(event, "HX-Redirect", "/") return null From 0f2e575df21bf940e4780c30d2701da989f62471 Mon Sep 17 00:00:00 2001 From: Elliu Date: Wed, 5 Nov 2025 00:04:38 +0900 Subject: [PATCH 3/4] on unbriding room, also demote powel level of bridge user in matrix room --- src/d2m/actions/create-room.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/d2m/actions/create-room.js b/src/d2m/actions/create-room.js index 7395abb4..da2462d9 100644 --- a/src/d2m/actions/create-room.js +++ b/src/d2m/actions/create-room.js @@ -507,6 +507,7 @@ async function unbridgeDeletedChannel(channel, guildID) { } // leave room + await api.setUserPower(roomID, bot, 0) await api.leaveRoom(roomID) } From b45eeb150e0702c201b8f710a3bdaa8e9f7d90be Mon Sep 17 00:00:00 2001 From: Elliu Date: Wed, 5 Nov 2025 00:20:20 +0900 Subject: [PATCH 4/4] manually revert 3597a3b: "Factorize some of the space link/unlink sanity checks" --- src/web/routes/link.js | 56 +++++++++++++++++++----------------------- 1 file changed, 25 insertions(+), 31 deletions(-) diff --git a/src/web/routes/link.js b/src/web/routes/link.js index 8f844ab1..054f60e7 100644 --- a/src/web/routes/link.js +++ b/src/web/routes/link.js @@ -41,33 +41,6 @@ function getCreateSpace(event) { return event.context.createSpace || sync.require("../../d2m/actions/create-space") } -/** - * @param {H3Event} event - * @param {string} guild_id - */ -async function validateUserHaveRightsOnGuild(event, guild_id) { - const managed = await auth.getManagedGuilds(event) - if (!managed.has(guild_id)) - throw createError({status: 403, message: "Forbidden", data: "Can't edit a guild you don't have Manage Server permissions in"}) -} - -/** - * @param {H3Event} event - * @param {string} guild_id - * @returns {Promise} - */ -async function validateGuildAccess(event, guild_id) { - // Check guild ID or nonce - await validateUserHaveRightsOnGuild(event, guild_id) - - // Check guild exists - const guild = discord.guilds.get(guild_id) - if (!guild) - throw createError({status: 400, message: "Bad Request", data: "Discord guild does not exist or bot has not joined it"}) - - return guild -} - /** * @param {H3Event} event * @param {string} channel_id @@ -117,11 +90,12 @@ const schema = { as.router.post("/api/link-space", defineEventHandler(async event => { const parsedBody = await readValidatedBody(event, schema.linkSpace.parse) const session = await auth.useSession(event) + const managed = await auth.getManagedGuilds(event) const api = getAPI(event) // Check guild ID const guildID = parsedBody.guild_id - await validateUserHaveRightsOnGuild(event, guildID) + if (!managed.has(guildID)) throw createError({status: 403, message: "Forbidden", data: "Can't edit a guild you don't have Manage Server permissions in"}) // Check space ID if (!session.data.mxid) throw createError({status: 403, message: "Forbidden", data: "Can't link with your Matrix space if you aren't logged in to Matrix"}) @@ -169,12 +143,18 @@ as.router.post("/api/link-space", defineEventHandler(async event => { as.router.post("/api/link", defineEventHandler(async event => { const parsedBody = await readValidatedBody(event, schema.link.parse) + const managed = await auth.getManagedGuilds(event) const api = getAPI(event) const createRoom = getCreateRoom(event) const createSpace = getCreateSpace(event) + // Check guild ID or nonce const guildID = parsedBody.guild_id - const guild = await validateGuildAccess(event, guildID) + if (!managed.has(guildID)) throw createError({status: 403, message: "Forbidden", data: "Can't edit a guild you don't have Manage Server permissions in"}) + + // Check guild is bridged + const guild = discord.guilds.get(guildID) + if (!guild) throw createError({status: 400, message: "Bad Request", data: "Discord guild does not exist or bot has not joined it"}) const spaceID = await createSpace.ensureSpace(guild) // Check channel exists @@ -252,7 +232,14 @@ as.router.post("/api/link", defineEventHandler(async event => { as.router.post("/api/unlink", defineEventHandler(async event => { const {channel_id, guild_id} = await readValidatedBody(event, schema.unlink.parse) - await validateGuildAccess(event, guild_id) + const managed = await auth.getManagedGuilds(event) + + // Check guild ID or nonce + if (!managed.has(guild_id)) throw createError({status: 403, message: "Forbidden", data: "Can't edit a guild you don't have Manage Server permissions in"}) + + // Check guild exists + const guild = discord.guilds.get(guild_id) + if (!guild) throw createError({status: 400, message: "Bad Request", data: "Discord guild does not exist or bot has not joined it"}) await doRoomUnlink(event, channel_id, guild_id) @@ -262,8 +249,15 @@ as.router.post("/api/unlink", defineEventHandler(async event => { as.router.post("/api/unlink-space", defineEventHandler(async event => { const {guild_id} = await readValidatedBody(event, schema.unlinkSpace.parse) + const managed = await auth.getManagedGuilds(event) const api = getAPI(event) - await validateGuildAccess(event, guild_id) + + // Check guild ID or nonce + if (!managed.has(guild_id)) throw createError({status: 403, message: "Forbidden", data: "Can't edit a guild you don't have Manage Server permissions in"}) + + // Check guild exists + const guild = discord.guilds.get(guild_id) + if (!guild) throw createError({status: 400, message: "Bad Request", data: "Discord guild does not exist or bot has not joined it"}) const spaceID = select("guild_space", "space_id", {guild_id: guild_id}).pluck().get() if (!spaceID) {