forked from cadence/out-of-your-element
Compare commits
4 commits
6ecc8167b2
...
10d14bbdaa
| Author | SHA1 | Date | |
|---|---|---|---|
| 10d14bbdaa | |||
| 677d899a92 | |||
| 9492137ca6 | |||
| 6aa2bfad13 |
4 changed files with 222 additions and 21 deletions
|
|
@ -24,3 +24,7 @@ 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")
|
||||
})
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ 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")}
|
||||
|
|
@ -66,6 +68,33 @@ async function validateGuildAccess(event, guild_id) {
|
|||
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(),
|
||||
|
|
@ -79,7 +108,10 @@ 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 => {
|
||||
|
|
@ -109,7 +141,6 @@ 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 {
|
||||
|
|
@ -203,27 +234,44 @@ 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)
|
||||
const createRoom = getCreateRoom(event)
|
||||
|
||||
await validateGuildAccess(event, 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}
|
||||
}
|
||||
|
||||
// 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)
|
||||
await doRoomUnlink(event, channel_id, guild_id)
|
||||
|
||||
setResponseHeader(event, "HX-Refresh", "true")
|
||||
return null // 204
|
||||
}))
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
const remainingLinkedChannels = select("channel_room", ["channel_id", "room_id", "name", "nick"], {guild_id: guild_id}).all()
|
||||
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)
|
||||
|
||||
// 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
|
||||
|
|
|
|||
|
|
@ -618,7 +618,9 @@ test("web unlink room: successfully calls unbridgeDeletedChannel when the channe
|
|||
})
|
||||
|
||||
test("web unlink room: checks that the channel is bridged", async t => {
|
||||
const row = db.prepare("SELECT * FROM channel_room WHERE channel_id = '665310973967597573'").get()
|
||||
db.prepare("DELETE FROM channel_room WHERE channel_id = '665310973967597573'").run()
|
||||
|
||||
const [error] = await tryToCatch(() => router.test("post", "/api/unlink", {
|
||||
sessionData: {
|
||||
managedGuilds: ["665289423482519565"]
|
||||
|
|
@ -629,4 +631,149 @@ test("web unlink room: checks that the channel is bridged", async t => {
|
|||
}
|
||||
}))
|
||||
t.equal(error.data, "Channel ID 665310973967597573 is not currently bridged")
|
||||
|
||||
db.prepare("INSERT INTO channel_room (channel_id, room_id, name, nick, thread_parent, custom_avatar, last_bridged_pin_timestamp, speedbump_id, speedbump_checked, speedbump_webhook_id, guild_id, custom_topic) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)").run(row.channel_id, row.room_id, row.name, row.nick, row.thread_parent, row.custom_avatar, row.last_bridged_pin_timestamp, row.speedbump_id, row.speedbump_checked, row.speedbump_webhook_id, row.guild_id, row.custom_topic)
|
||||
const new_row = db.prepare("SELECT * FROM channel_room WHERE channel_id = '665310973967597573'").get()
|
||||
t.deepEqual(row, new_row)
|
||||
})
|
||||
|
||||
// *****
|
||||
|
||||
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 before trying to unlink the space", 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: correctly abort unlinking if some linked channels remain after trying to unlink them all", async t => {
|
||||
let unbridgedChannel = false
|
||||
|
||||
const [error] = await tryToCatch(() => router.test("post", "/api/unlink-space", {
|
||||
sessionData: {
|
||||
managedGuilds: ["665289423482519565"]
|
||||
},
|
||||
body: {
|
||||
guild_id: "665289423482519565",
|
||||
},
|
||||
createRoom: {
|
||||
async unbridgeDeletedChannel(channel, guildID) {
|
||||
unbridgedChannel = true
|
||||
t.equal(channel.id, "665310973967597573")
|
||||
t.equal(guildID, "665289423482519565")
|
||||
// Do not actually delete the link from DB, should trigger error later in check
|
||||
}
|
||||
},
|
||||
api: {
|
||||
async *generateFullHierarchy(spaceID) {
|
||||
t.equal(spaceID, "!zTMspHVUBhFLLSdmnS:cadence.moe")
|
||||
yield {
|
||||
room_id: "!NDbIqNpJyPvfKRnNcr:cadence.moe",
|
||||
children_state: {},
|
||||
guest_can_join: false,
|
||||
num_joined_members: 2
|
||||
}
|
||||
/* c8 ignore next */
|
||||
},
|
||||
}
|
||||
}))
|
||||
|
||||
t.equal(error.data, "Some linked room still exists after trying to unlink all of them. Aborting the space unlinking...")
|
||||
t.equal(unbridgedChannel, true)
|
||||
})
|
||||
|
||||
test("web unlink space: successfully calls unbridgeDeletedChannel on linked channels in space, self-downgrade power level, leave space, and delete link from DB", async t => {
|
||||
const {reg} = require("../../matrix/read-registration")
|
||||
const me = `@${reg.sender_localpart}:${reg.ooye.server_name}`
|
||||
|
||||
const getLinkRowQuery = "SELECT * FROM guild_space WHERE guild_id = '665289423482519565'"
|
||||
|
||||
const row = db.prepare(getLinkRowQuery).get()
|
||||
t.equal(row.space_id, "!zTMspHVUBhFLLSdmnS:cadence.moe")
|
||||
|
||||
let unbridgedChannel = false
|
||||
let downgradedPowerLevel = false
|
||||
let leftRoom = false
|
||||
await router.test("post", "/api/unlink-space", {
|
||||
sessionData: {
|
||||
managedGuilds: ["665289423482519565"]
|
||||
},
|
||||
body: {
|
||||
guild_id: "665289423482519565",
|
||||
},
|
||||
createRoom: {
|
||||
async unbridgeDeletedChannel(channel, guildID) {
|
||||
unbridgedChannel = true
|
||||
t.equal(channel.id, "665310973967597573")
|
||||
t.equal(guildID, "665289423482519565")
|
||||
|
||||
// In order to not simulate channel deletion and not trigger the post unlink channels, pre-unlink space check
|
||||
db.prepare("DELETE FROM channel_room WHERE guild_id = '665289423482519565' AND channel_id = '665310973967597573'").run()
|
||||
}
|
||||
},
|
||||
api: {
|
||||
async *generateFullHierarchy(spaceID) {
|
||||
t.equal(spaceID, "!zTMspHVUBhFLLSdmnS:cadence.moe")
|
||||
yield {
|
||||
room_id: "!NDbIqNpJyPvfKRnNcr:cadence.moe",
|
||||
children_state: {},
|
||||
guest_can_join: false,
|
||||
num_joined_members: 2
|
||||
}
|
||||
/* c8 ignore next */
|
||||
},
|
||||
|
||||
async setUserPower(spaceID, targetUser, powerLevel) {
|
||||
downgradedPowerLevel = true
|
||||
t.equal(spaceID, "!zTMspHVUBhFLLSdmnS:cadence.moe")
|
||||
t.equal(targetUser, me)
|
||||
t.equal(powerLevel, 0)
|
||||
},
|
||||
|
||||
async leaveRoom(spaceID) {
|
||||
leftRoom = true
|
||||
t.equal(spaceID, "!zTMspHVUBhFLLSdmnS:cadence.moe")
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
t.equal(unbridgedChannel, true)
|
||||
t.equal(downgradedPowerLevel, true)
|
||||
t.equal(leftRoom, true)
|
||||
|
||||
const missed_row = db.prepare(getLinkRowQuery).get()
|
||||
t.equal(missed_row, undefined)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -17,6 +17,8 @@ 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"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue