UI for linking existing space
This commit is contained in:
@@ -2,9 +2,9 @@
|
||||
|
||||
const assert = require("assert/strict")
|
||||
const {z} = require("zod")
|
||||
const {defineEventHandler, useSession, createError, readValidatedBody} = require("h3")
|
||||
const {defineEventHandler, useSession, createError, readValidatedBody, getRequestHeader, setResponseHeader, sendRedirect} = require("h3")
|
||||
|
||||
const {as, db, sync} = require("../../passthrough")
|
||||
const {as, db, sync, select} = require("../../passthrough")
|
||||
const {reg} = require("../../matrix/read-registration")
|
||||
|
||||
/** @type {import("../../d2m/actions/create-space")} */
|
||||
@@ -29,6 +29,17 @@ as.router.post("/api/autocreate", defineEventHandler(async event => {
|
||||
if (!(session.data.managedGuilds || []).concat(session.data.matrixGuilds || []).includes(parsedBody.guild_id)) throw createError({status: 403, message: "Forbidden", data: "Can't change settings for a guild you don't have Manage Server permissions in"})
|
||||
|
||||
db.prepare("UPDATE guild_active SET autocreate = ? WHERE guild_id = ?").run(+!!parsedBody.autocreate, parsedBody.guild_id)
|
||||
|
||||
// If showing a partial page due to incomplete setup, need to refresh the whole page to show the alternate version
|
||||
const spaceID = select("guild_space", "space_id", {guild_id: parsedBody.guild_id}).pluck().get()
|
||||
if (!spaceID) {
|
||||
if (getRequestHeader(event, "HX-Request")) {
|
||||
setResponseHeader(event, "HX-Refresh", "true")
|
||||
} else {
|
||||
return sendRedirect(event, "", 302)
|
||||
}
|
||||
}
|
||||
|
||||
return null // 204
|
||||
}))
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ const {LRUCache} = require("lru-cache")
|
||||
const Ty = require("../../types")
|
||||
const uqr = require("uqr")
|
||||
|
||||
const {discord, as, sync, select} = require("../../passthrough")
|
||||
const {discord, as, sync, select, from, db} = require("../../passthrough")
|
||||
/** @type {import("../pug-sync")} */
|
||||
const pugSync = sync.require("../pug-sync")
|
||||
/** @type {import("../../d2m/actions/create-space")} */
|
||||
@@ -109,15 +109,21 @@ function getChannelRoomsLinks(guildID, rooms) {
|
||||
as.router.get("/guild", defineEventHandler(async event => {
|
||||
const {guild_id} = await getValidatedQuery(event, schema.guild.parse)
|
||||
const session = await useSession(event, {password: reg.as_token})
|
||||
const row = select("guild_space", ["space_id", "privacy_level"], {guild_id}).get()
|
||||
const row = from("guild_active").join("guild_space", "guild_id", "left").select("space_id", "privacy_level", "autocreate").where({guild_id}).get()
|
||||
// @ts-ignore
|
||||
const guild = discord.guilds.get(guild_id)
|
||||
|
||||
// Permission problems
|
||||
if (!guild_id || !guild || !(session.data.managedGuilds || []).concat(session.data.matrixGuilds || []).includes(guild_id)) {
|
||||
if (!guild_id || !guild || !(session.data.managedGuilds || []).concat(session.data.matrixGuilds || []).includes(guild_id) || !row) {
|
||||
return pugSync.render(event, "guild_access_denied.pug", {guild_id})
|
||||
}
|
||||
|
||||
// Self-service guild that hasn't been linked yet - needs a special page encouraging the link flow
|
||||
if (!row.space_id && row.autocreate === 0) {
|
||||
const spaces = db.prepare("SELECT room_id, type, name, avatar FROM invite LEFT JOIN guild_space ON invite.room_id = guild_space.space_id WHERE mxid = ? AND space_id IS NULL and type = 'm.space'").all(session.data.mxid)
|
||||
return pugSync.render(event, "guild_not_linked.pug", {guild, guild_id, spaces})
|
||||
}
|
||||
|
||||
const nonce = randomUUID()
|
||||
validNonce.set(nonce, guild_id)
|
||||
|
||||
@@ -128,10 +134,10 @@ as.router.get("/guild", defineEventHandler(async event => {
|
||||
const svg = generatedSvg.replace(/viewBox="0 0 ([0-9]+) ([0-9]+)"/, `data-nonce="${nonce}" width="$1" height="$2" $&`)
|
||||
assert.notEqual(svg, generatedSvg)
|
||||
|
||||
// Unlinked guild
|
||||
if (!row) {
|
||||
// Easy mode guild that hasn't been linked yet - need to remove elements that would require an existing space
|
||||
if (!row.space_id) {
|
||||
const links = getChannelRoomsLinks(guild_id, [])
|
||||
return pugSync.render(event, "guild.pug", {guild, guild_id, svg, ...links})
|
||||
return pugSync.render(event, "guild.pug", {guild, guild_id, svg, ...links, ...row})
|
||||
}
|
||||
|
||||
// Linked guild
|
||||
|
||||
@@ -18,6 +18,7 @@ test("web guild: access denied when not logged in", async t => {
|
||||
test("web guild: asks to select guild if not selected", async t => {
|
||||
const content = await router.test("get", "/guild", {
|
||||
sessionData: {
|
||||
user_id: "1",
|
||||
managedGuilds: []
|
||||
},
|
||||
})
|
||||
@@ -27,6 +28,7 @@ test("web guild: asks to select guild if not selected", async t => {
|
||||
test("web guild: access denied when guild id messed up", async t => {
|
||||
const content = await router.test("get", "/guild?guild_id=1", {
|
||||
sessionData: {
|
||||
user_id: "1",
|
||||
managedGuilds: []
|
||||
},
|
||||
})
|
||||
@@ -43,6 +45,7 @@ test("web invite: access denied with invalid nonce", async t => {
|
||||
test("web guild: can view unbridged guild", async t => {
|
||||
const content = await router.test("get", "/guild?guild_id=66192955777486848", {
|
||||
sessionData: {
|
||||
user_id: "1",
|
||||
managedGuilds: ["66192955777486848"]
|
||||
},
|
||||
api: {
|
||||
|
||||
@@ -16,6 +16,10 @@ const {reg} = require("../../matrix/read-registration")
|
||||
const api = sync.require("../../matrix/api")
|
||||
|
||||
const schema = {
|
||||
linkSpace: z.object({
|
||||
guild_id: z.string(),
|
||||
space_id: z.string()
|
||||
}),
|
||||
link: z.object({
|
||||
guild_id: z.string(),
|
||||
matrix: z.string(),
|
||||
@@ -27,6 +31,48 @@ const schema = {
|
||||
})
|
||||
}
|
||||
|
||||
as.router.post("/api/link-space", defineEventHandler(async event => {
|
||||
const parsedBody = await readValidatedBody(event, schema.linkSpace.parse)
|
||||
const session = await useSession(event, {password: reg.as_token})
|
||||
|
||||
// Check guild ID
|
||||
const guildID = parsedBody.guild_id
|
||||
if (!(session.data.managedGuilds || []).concat(session.data.matrixGuilds || []).includes(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"})
|
||||
const spaceID = parsedBody.space_id
|
||||
const inviteType = select("invite", "type", {mxid: session.data.mxid, room_id: spaceID}).pluck().get()
|
||||
if (inviteType !== "m.space") throw createError({status: 403, message: "Forbidden", data: "No past invitations detected from your Matrix account for that space."})
|
||||
|
||||
// Check they are not already bridged
|
||||
const existing = select("guild_space", "guild_id", {}, "WHERE guild_id = ? OR space_id = ?").get(guildID, spaceID)
|
||||
if (existing) throw createError({status: 400, message: "Bad Request", data: `Guild ID ${guildID} or space ID ${spaceID} are already bridged and cannot be reused`})
|
||||
|
||||
// Check space 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(spaceID, "m.room.member", self)
|
||||
if (memberEvent.membership !== "join") throw createError({status: 400, message: "Bad Request", data: "Matrix space does not exist"})
|
||||
/** @type {Ty.Event.M_Power_Levels} */
|
||||
const powerLevelsStateContent = await api.getStateEvent(spaceID, "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 space"})
|
||||
|
||||
// Check inviting user is a moderator in the space
|
||||
const invitingPowerLevel = powerLevelsStateContent.users?.[session.data.mxid] || powerLevelsStateContent.users_default || 0
|
||||
if (invitingPowerLevel < (powerLevelsStateContent.state_default || 50)) throw createError({status: 403, message: "Forbidden", data: `You need to be at least power level 50 (moderator) in the target Matrix space to use OOYE, but you are currently power level ${invitingPowerLevel}.`})
|
||||
|
||||
// Insert database entry
|
||||
db.transaction(() => {
|
||||
db.prepare("INSERT INTO guild_space (guild_id, space_id) VALUES (?, ?)").run(guildID, spaceID)
|
||||
db.prepare("DELETE FROM invite WHERE room_id = ?").run(spaceID)
|
||||
})()
|
||||
|
||||
setResponseHeader(event, "HX-Refresh", "true")
|
||||
return null // 204
|
||||
}))
|
||||
|
||||
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})
|
||||
|
||||
Reference in New Issue
Block a user