diff --git a/src/web/pug/guild.pug b/src/web/pug/guild.pug index acc1acb..1d03b68 100644 --- a/src/web/pug/guild.pug +++ b/src/web/pug/guild.pug @@ -205,7 +205,7 @@ block body p.s-description All of the above, and publicly visible in the Matrix space directory (like Server Discovery) h3.mt32.fs-category Manually link channels - form.d-flex.g16.ai-start(method="post" action="/api/link") + form.d-flex.g16.ai-start(hx-post="/api/privacy-level" hx-trigger="submit" hx-disabled-elt="this") .fl-grow2.s-btn-group.fd-column.w40 each channel in unlinkedChannels input.s-btn--radio(type="radio" name="discord" id=channel.id value=channel.id) @@ -220,7 +220,8 @@ block body +matrix(room, true) else .s-empty-state.p8 All Matrix rooms are linked. + input(type="hidden" name="guild_id" value=guild_id) div - button.s-btn.s-btn__icon.s-btn__filled - != icons.Icons.IconLink - = ` Connect` + button.s-btn.s-btn__icon.s-btn__filled.htmx-indicator + != icons.Icons.IconMerge + = ` Link` diff --git a/src/web/routes/link.js b/src/web/routes/link.js new file mode 100644 index 0000000..90cfd53 --- /dev/null +++ b/src/web/routes/link.js @@ -0,0 +1,63 @@ +// @ts-check + +const {z} = require("zod") +const {defineEventHandler, useSession, createError, readValidatedBody} = require("h3") +const Ty = require("../../types") + +const {discord, db, as, sync, select, from} = require("../../passthrough") +/** @type {import("../../d2m/actions/create-space")} */ +const createSpace = sync.require("../../d2m/actions/create-space") +/** @type {import("../../d2m/actions/create-room")} */ +const createRoom = sync.require("../../d2m/actions/create-room") +const {reg} = require("../../matrix/read-registration") + +/** @type {import("../../matrix/api")} */ +const api = sync.require("../../matrix/api") + +const schema = { + link: z.object({ + guild_id: z.string(), + matrix: z.string(), + discord: z.string() + }) +} + +as.router.post("/api/link", defineEventHandler(async event => { + const parsedBody = await readValidatedBody(event, schema.link.parse) + const session = await useSession(event, {password: reg.as_token}) + + // Check guild ID or nonce + const guildID = parsedBody.guild_id + if (!(session.data.managedGuilds || []).includes(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 + const channel = discord.channels.get(parsedBody.discord) + if (!channel) throw createError({status: 400, message: "Bad Request", data: "Discord channel does not exist"}) + + // Check channel and room are not already bridged + const row = from("channel_room").select("channel_id", "room_id").and("WHERE channel_id = ? OR room_id = ?").get(parsedBody.discord, parsedBody.matrix) + if (row) throw createError({status: 400, message: "Bad Request", data: `Channel ID ${row.channel_id} and room ID ${row.room_id} are already bridged and cannot be reused`}) + + // Check room exists and bridge is joined and bridge has PL 100 + const self = `@${reg.sender_localpart}:${reg.ooye.server_name}` + /** @type {Ty.Event.M_Room_Member} */ + const memberEvent = await api.getStateEvent(parsedBody.matrix, "m.room.member", self) + if (memberEvent.membership !== "join") throw createError({status: 400, message: "Bad Request", data: "Matrix room does not exist"}) + /** @type {Ty.Event.M_Power_Levels} */ + const powerLevelsStateContent = await api.getStateEvent(parsedBody.matrix, "m.room.power_levels", "") + const selfPowerLevel = powerLevelsStateContent.users?.[self] || powerLevelsStateContent.users_default || 0 + if (selfPowerLevel < (powerLevelsStateContent.state_default || 50) || selfPowerLevel < 100) throw createError({status: 400, message: "Bad Request", data: "OOYE needs power level 100 (admin) in the target Matrix room"}) + + // Insert database entry + db.prepare("INSERT INTO channel_room (channel_id, room_id, name, guild_id) VALUES (?, ?, ?, ?)").run(parsedBody.discord, parsedBody.matrix, channel.name, guildID) + + // Sync room data and space child + createRoom.syncRoom(parsedBody.discord) + + return null // 204 +})) diff --git a/src/web/server.js b/src/web/server.js index d1b4cb8..f9f12a1 100644 --- a/src/web/server.js +++ b/src/web/server.js @@ -23,8 +23,9 @@ pugSync.createRoute(as.router, "/ok", "ok.pug") sync.require("./routes/download-matrix") sync.require("./routes/download-discord") -sync.require("./routes/invite") sync.require("./routes/guild-settings") +sync.require("./routes/invite") +sync.require("./routes/link") sync.require("./routes/oauth") sync.require("./routes/qr")