diff --git a/.editorconfig b/.editorconfig deleted file mode 100644 index 089c28f..0000000 --- a/.editorconfig +++ /dev/null @@ -1,6 +0,0 @@ -[*] -indent_style = tab - -[*.pug] -indent_style = space -indent_size = 2 diff --git a/src/d2m/actions/register-pk-user.js b/src/d2m/actions/register-pk-user.js index b5e44e5..27e949c 100644 --- a/src/d2m/actions/register-pk-user.js +++ b/src/d2m/actions/register-pk-user.js @@ -146,7 +146,7 @@ async function syncUser(messageID, author, roomID, shouldActuallySync) { try { // API lookup var pkMessage = await fetchMessage(messageID) - db.prepare("INSERT OR IGNORE INTO sim_proxy (user_id, proxy_owner_id, displayname) VALUES (?, ?, ?)").run(pkMessage.member.uuid, pkMessage.sender, author.username) + db.prepare("REPLACE INTO sim_proxy (user_id, proxy_owner_id, displayname) VALUES (?, ?, ?)").run(pkMessage.member.uuid, pkMessage.sender, author.username) } catch (e) { // Fall back to offline cache const senderMxid = from("sim_proxy").join("sim", "user_id").join("sim_member", "mxid").where({displayname: author.username, room_id: roomID}).pluck("mxid").get() diff --git a/src/d2m/converters/message-to-event.js b/src/d2m/converters/message-to-event.js index a8e5a6b..1d6288a 100644 --- a/src/d2m/converters/message-to-event.js +++ b/src/d2m/converters/message-to-event.js @@ -33,9 +33,10 @@ function getDiscordParseCallbacks(message, guild, useHTML) { user: node => { const mxid = select("sim", "mxid", {user_id: node.id}).pluck().get() const interaction = message.interaction_metadata || message.interaction - const username = message.mentions.find(ment => ment.id === node.id)?.username - || message.referenced_message?.mentions.find(ment => ment.id === node.id)?.username + const username = message.mentions?.find(ment => ment.id === node.id)?.username + || message.referenced_message?.mentions?.find(ment => ment.id === node.id)?.username || (interaction?.user.id === node.id ? interaction.user.username : null) + || (message.author.id === node.id ? message.author.username : null) || node.id if (mxid && useHTML) { return `@${username}` diff --git a/src/matrix/api.test.js b/src/matrix/api.test.js index da92385..82565eb 100644 --- a/src/matrix/api.test.js +++ b/src/matrix/api.test.js @@ -24,7 +24,3 @@ test("api path: real world mxid", t => { test("api path: extras number works", t => { t.equal(path(`/client/v3/rooms/!example/timestamp_to_event`, null, {ts: 1687324651120}), "/client/v3/rooms/!example/timestamp_to_event?ts=1687324651120") }) - -test("api path: multiple via params", t => { - t.equal(path(`/client/v3/rooms/!example/join`, null, {via: ["cadence.moe", "matrix.org"], ts: 1687324651120}), "/client/v3/rooms/!example/join?via=cadence.moe&via=matrix.org&ts=1687324651120") -}) diff --git a/src/web/pug/guild.pug b/src/web/pug/guild.pug index cedc32a..92ffa1b 100644 --- a/src/web/pug/guild.pug +++ b/src/web/pug/guild.pug @@ -54,10 +54,6 @@ block body .s-page-title.mb24 h1.s-page-title--header= guild.name - form(method="post" action=rel("/api/unlink-space") hx-confirm="Do you want to unlink this server?\nThis will unlink every channels listed below.\nIt may take a moment to clean up Matrix resources.") - input(type="hidden" name="guild_id" value=guild.id) - button.s-btn.s-btn__muted.s-btn__xs(hx-post=rel("/api/unlink-space") hx-trigger="click" hx-disabled-elt="this")!= icons.Icons.IconLinkSm - .d-flex.g16(class="sm:fw-wrap") .fl-grow1 h2.fs-headline1 Invite a Matrix user diff --git a/src/web/routes/link.js b/src/web/routes/link.js index a6a581f..c5f404e 100644 --- a/src/web/routes/link.js +++ b/src/web/routes/link.js @@ -12,8 +12,6 @@ const auth = sync.require("../auth") const mreq = sync.require("../../matrix/mreq") const {reg} = require("../../matrix/read-registration") -const me = `@${reg.sender_localpart}:${reg.ooye.server_name}` - /** * @param {H3Event} event * @returns {import("../../matrix/api")} @@ -41,60 +39,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 - * @param {string} guild_id - */ -async function doRoomUnlink(event, channel_id, guild_id) { - const createRoom = getCreateRoom(event) - - // Check that the channel (if it exists) is part of this guild - /** @type {any} */ - let channel = discord.channels.get(channel_id) - if (channel) { - if (!("guild_id" in channel) || channel.guild_id !== guild_id) throw createError({status: 400, message: "Bad Request", data: `Channel ID ${channel_id} is not part of guild ${guild_id}`}) - } else { - // Otherwise, if the channel isn't cached, it must have been deleted. - // There's no other authentication here - it's okay for anyone to unlink a deleted channel just by knowing its ID. - channel = {id: channel_id} - } - - // Check channel is currently bridged - const row = select("channel_room", "channel_id", {channel_id: channel_id}).get() - if (!row) throw createError({status: 400, message: "Bad Request", data: `Channel ID ${channel_id} is not currently bridged`}) - - // Do it - await createRoom.unbridgeDeletedChannel(channel, guild_id) -} - const schema = { linkSpace: z.object({ guild_id: z.string(), @@ -108,20 +52,18 @@ const schema = { unlink: z.object({ guild_id: z.string(), channel_id: z.string() - }), - unlinkSpace: z.object({ - guild_id: z.string(), - }), + }) } 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"}) @@ -141,6 +83,7 @@ as.router.post("/api/link-space", defineEventHandler(async event => { } // Check bridge has PL 100 + const me = `@${reg.sender_localpart}:${reg.ooye.server_name}` /** @type {Ty.Event.M_Power_Levels?} */ let powerLevelsStateContent = null try { @@ -165,12 +108,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 @@ -234,44 +183,33 @@ 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) + const createRoom = getCreateRoom(event) - await doRoomUnlink(event, channel_id, 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"}) - setResponseHeader(event, "HX-Refresh", "true") - return null // 204 -})) + // 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"}) -as.router.post("/api/unlink-space", defineEventHandler(async event => { - const {guild_id} = await readValidatedBody(event, schema.unlinkSpace.parse) - const api = getAPI(event) - await validateGuildAccess(event, guild_id) - - const spaceID = select("guild_space", "space_id", {guild_id: guild_id}).pluck().get() - 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) + // Check that the channel (if it exists) is part of this guild + /** @type {any} */ + let channel = discord.channels.get(channel_id) + if (channel) { + if (!("guild_id" in channel) || channel.guild_id !== guild_id) throw createError({status: 400, message: "Bad Request", data: `Channel ID ${channel_id} is not part of guild ${guild_id}`}) + } else { + // Otherwise, if the channel isn't cached, it must have been deleted. + // There's no other authentication here - it's okay for anyone to unlink a deleted channel just by knowing its ID. + channel = {id: channel_id} } - const remainingLinkedChannels = select("channel_room", ["channel_id", "room_id", "name", "nick"], {guild_id: guild_id}).all() - if (remainingLinkedChannels.length !== 0) - throw createError({status: 400, message: "Bad Request", data: "Some linked room still exists after trying to unlink all of them. Aborting the space unlinking..."}) + // Check channel is currently bridged + const row = select("channel_room", "channel_id", {channel_id: channel_id}).get() + if (!row) throw createError({status: 400, message: "Bad Request", data: `Channel ID ${channel_id} is not currently bridged`}) - 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) - - // 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) + // Do it + await createRoom.unbridgeDeletedChannel(channel, guild_id) setResponseHeader(event, "HX-Refresh", "true") return null // 204 diff --git a/src/web/routes/link.test.js b/src/web/routes/link.test.js index cc39354..0d8d366 100644 --- a/src/web/routes/link.test.js +++ b/src/web/routes/link.test.js @@ -630,76 +630,3 @@ test("web unlink room: checks that the channel is bridged", async t => { })) t.equal(error.data, "Channel ID 665310973967597573 is not currently bridged") }) - -// ***** - -test("web unlink space: access denied if not logged in to Discord", async t => { - const [error] = await tryToCatch(() => router.test("post", "/api/unlink-space", { - body: { - guild_id: "665289423482519565" - } - })) - t.equal(error.data, "Can't edit a guild you don't have Manage Server permissions in") -}) - -test("web unlink space: checks that guild exists", async t => { - const [error] = await tryToCatch(() => router.test("post", "/api/unlink-space", { - sessionData: { - managedGuilds: ["2"] - }, - body: { - guild_id: "2" - } - })) - t.equal(error.data, "Discord guild does not exist or bot has not joined it") -}) - -test("web unlink space: checks that a space is linked to the guild", async t => { - const row = db.prepare("SELECT * FROM guild_space WHERE guild_id = '665289423482519565'").get() - db.prepare("DELETE FROM guild_space WHERE guild_id = '665289423482519565'").run() - - const [error] = await tryToCatch(() => router.test("post", "/api/unlink-space", { - sessionData: { - managedGuilds: ["665289423482519565"] - }, - body: { - guild_id: "665289423482519565" - } - })) - t.equal(error.data, "Matrix space does not exist or bot has not linked it") - - db.prepare("INSERT INTO guild_space (guild_id, space_id, privacy_level, presence, url_preview) VALUES (?, ?, ?, ?, ?)").run(row.guild_id, row.space_id, row.privacy_level, row.presence, row.url_preview) - const new_row = db.prepare("SELECT * FROM guild_space WHERE guild_id = '665289423482519565'").get() - t.deepEqual(row, new_row) -}) - -test("web unlink space: successfully calls unbridgeDeletedChannel on linked channels in space", async t => { - // Need to re-link the room to check it is properly unlinked by the unlink-space - await router.test("post", "/api/link", { - sessionData: { - managedGuilds: ["665289423482519565"] - }, - body: { - discord: "665310973967597573", - matrix: "!NDbIqNpJyPvfKRnNcr:cadence.moe", - guild_id: "665289423482519565" - }, - }) - - let called = 0 - await router.test("post", "/api/unlink-space", { - sessionData: { - managedGuilds: ["665289423482519565"] - }, - body: { - guild_id: "665289423482519565", - }, - createRoom: { - async unbridgeDeletedChannel(channel) { - called++ - t.equal(channel.id, "665310973967597573") - } - } - }) - t.equal(called, 1) -}) diff --git a/test/test.js b/test/test.js index 8d9ad16..3695a84 100644 --- a/test/test.js +++ b/test/test.js @@ -17,8 +17,6 @@ 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" -reg.ooye.namespace_prefix = "_ooye_" -reg.sender_localpart = "_ooye_bot" reg.id = "baby" reg.as_token = "don't actually take authenticated actions on the server" reg.hs_token = "don't actually take authenticated actions on the server"