Add unlink space feature
Squashed commit of the following: commit bd9fd5cd3cf3f1301df18074c997ec537a81b4f5 Author: Elliu <elliu@hashi.re> Date: Sat Nov 15 15:32:18 2025 +0900 Revert "fix matrix / db resource cleanup on space unlink" This reverts commit ccc10564f1e33ab277bc15f360b8c65f2d0ea867. commit eec559293861305394770343d501389905fe1650 Author: Cadence Ember <cadence@disroot.org> Date: Sat Nov 8 13:01:59 2025 +1300 Dependency inject snow for testing commit b45eeb150e0702c201b8f710a3bdaa8e9f7d90be Author: Elliu <elliu@hashi.re> Date: Wed Nov 5 00:20:20 2025 +0900 manually revert 3597a3b: "Factorize some of the space link/unlink sanity checks" commit 0f2e575df21bf940e4780c30d2701da989f62471 Author: Elliu <elliu@hashi.re> Date: Wed Nov 5 00:04:38 2025 +0900 on unbriding room, also demote powel level of bridge user in matrix room commit ccc10564f1e33ab277bc15f360b8c65f2d0ea867 Author: Elliu <elliu@hashi.re> Date: Wed Nov 5 00:04:13 2025 +0900 fix matrix / db resource cleanup on space unlink commit f4c1ea7c7f7d5a265b84ce464cd8e9e26d934a32 Author: Elliu <elliu@hashi.re> Date: Tue Nov 4 23:54:41 2025 +0900 /unlink-space: properly leave guild and clean DB commit 5f0ec3b2c861cc8b9edc51389d6176c7a22a1135 Author: Cadence Ember <cadence@disroot.org> Date: Sun Nov 2 22:31:14 2025 +1300 Improve HTML to a state I'm happy with commit 16309f26b3dd72927e05454cee8c63504b447b7f Author: Elliu <elliu@hashi.re> Date: Sat Nov 1 22:24:51 2025 +0900 add tests from /unlink-space endpoint commit 5aff6f9048330a86eda3b2d1862f42df8d2bad84 Author: Elliu <elliu@hashi.re> Date: Sat Sep 6 20:05:18 2025 +0900 Add /api/unlink-space implementation commit dfc61594f68db4b52b3553ac7d3561ae9ce13b49 Author: Elliu <elliu@hashi.re> Date: Sat Sep 6 19:59:44 2025 +0900 Extract /api/unlink code to its own function commit 3597a3b5ce9dde3a9ddfe0853253bfda91a38335 Author: Elliu <elliu@hashi.re> Date: Sat Sep 6 19:28:42 2025 +0900 Factorize some of the space link/unlink sanity checks commit 05d788e26394106d9be24cef8b38f6c6f1e4c984 Author: Elliu <elliu@hashi.re> Date: Sat Sep 6 18:23:01 2025 +0900 Add button to unlink a space Co-authored-by: Cadence Ember <cadence@disroot.org>
This commit is contained in:
@@ -439,19 +439,11 @@ function syncRoom(channelID) {
|
||||
return _syncRoom(channelID, true)
|
||||
}
|
||||
|
||||
async function unbridgeChannel(channelID) {
|
||||
/** @ts-ignore @type {DiscordTypes.APIGuildChannel} */
|
||||
const channel = discord.channels.get(channelID)
|
||||
assert.ok(channel)
|
||||
assert.ok(channel.guild_id)
|
||||
return unbridgeDeletedChannel(channel, channel.guild_id)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {{id: string, topic?: string?}} channel channel-ish (just needs an id, topic is optional)
|
||||
* @param {string} guildID
|
||||
*/
|
||||
async function unbridgeDeletedChannel(channel, guildID) {
|
||||
async function unbridgeChannel(channel, guildID) {
|
||||
const roomID = select("channel_room", "room_id", {channel_id: channel.id}).pluck().get()
|
||||
assert.ok(roomID)
|
||||
const row = from("guild_space").join("guild_active", "guild_id").select("space_id", "autocreate").where({guild_id: guildID}).get()
|
||||
@@ -488,14 +480,13 @@ async function unbridgeDeletedChannel(channel, guildID) {
|
||||
|
||||
if (!botInRoom) return
|
||||
|
||||
// demote admins in room
|
||||
/** @type {Ty.Event.M_Power_Levels} */
|
||||
const powerLevelContent = await api.getStateEvent(roomID, "m.room.power_levels", "")
|
||||
powerLevelContent.users ??= {}
|
||||
for (const mxid of Object.keys(powerLevelContent.users)) {
|
||||
if (powerLevelContent.users[mxid] >= 100 && mUtils.eventSenderIsFromDiscord(mxid) && mxid !== mUtils.bot) {
|
||||
delete powerLevelContent.users[mxid]
|
||||
await api.sendState(roomID, "m.room.power_levels", "", powerLevelContent, mxid)
|
||||
// demote discord sim admins in room
|
||||
const {powerLevels, allCreators} = await mUtils.getEffectivePower(roomID, [], api)
|
||||
const powerLevelsUsers = (powerLevels.users ||= {})
|
||||
for (const mxid of Object.keys(powerLevelsUsers)) {
|
||||
if (powerLevelsUsers[mxid] >= (powerLevels.state_default ?? 50) && !allCreators.includes(mxid) && mUtils.eventSenderIsFromDiscord(mxid) && mxid !== mUtils.bot) {
|
||||
delete powerLevelsUsers[mxid]
|
||||
await api.sendState(roomID, "m.room.power_levels", "", powerLevels, mxid) // done individually because each user must demote themselves
|
||||
}
|
||||
}
|
||||
|
||||
@@ -526,6 +517,7 @@ async function unbridgeDeletedChannel(channel, guildID) {
|
||||
}
|
||||
|
||||
// leave room
|
||||
await mUtils.setUserPower(roomID, mUtils.bot, 0, api)
|
||||
await api.leaveRoom(roomID)
|
||||
}
|
||||
|
||||
@@ -589,6 +581,5 @@ module.exports.postApplyPowerLevels = postApplyPowerLevels
|
||||
module.exports._convertNameAndTopic = convertNameAndTopic
|
||||
module.exports._syncSpaceMember = _syncSpaceMember
|
||||
module.exports.unbridgeChannel = unbridgeChannel
|
||||
module.exports.unbridgeDeletedChannel = unbridgeDeletedChannel
|
||||
module.exports.existsOrAutocreatable = existsOrAutocreatable
|
||||
module.exports.assertExistsOrAutocreatable = assertExistsOrAutocreatable
|
||||
|
||||
@@ -203,7 +203,7 @@ async function syncSpaceFully(guildID) {
|
||||
if (discord.channels.has(channelID)) {
|
||||
await createRoom.syncRoom(channelID)
|
||||
} else {
|
||||
await createRoom.unbridgeDeletedChannel({id: channelID}, guildID)
|
||||
await createRoom.unbridgeChannel({id: channelID}, guildID)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -250,7 +250,7 @@ module.exports = {
|
||||
const roomID = select("channel_room", "room_id", {channel_id: channel.id}).pluck().get()
|
||||
if (!roomID) return // channel wasn't being bridged in the first place
|
||||
// @ts-ignore
|
||||
await createRoom.unbridgeDeletedChannel(channel, guildID)
|
||||
await createRoom.unbridgeChannel(channel, guildID)
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
@@ -75,6 +75,7 @@ block body
|
||||
button.s-btn(class=space_id ? "s-btn__muted" : "s-btn__filled" hx-get=rel(`/qr?guild_id=${guild_id}`) hx-indicator="closest button" hx-swap="outerHTML" hx-disabled-elt="this") Show QR
|
||||
|
||||
if space_id
|
||||
h2.mt48.fs-headline1 Server settings
|
||||
h3.mt32.fs-category Privacy level
|
||||
span#privacy-level-loading
|
||||
.s-card
|
||||
@@ -104,7 +105,7 @@ block body
|
||||
p.s-description.m0 Shareable invite links, like Discord
|
||||
p.s-description.m0 Publicly listed in directory, like Discord server discovery
|
||||
|
||||
h2.mt48.fs-headline1 Features
|
||||
h3.mt32.fs-category Features
|
||||
.s-card.d-grid.px0.g16
|
||||
form.d-flex.ai-center.g16
|
||||
#url-preview-loading.p8
|
||||
@@ -138,13 +139,13 @@ block body
|
||||
|
||||
h3.mt32.fs-category Linked channels
|
||||
.s-card.bs-sm.p0
|
||||
form.s-table-container(method="post" action=rel("/api/unlink") hx-confirm="Do you want to unlink these channels?\nIt may take a moment to clean up Matrix resources.")
|
||||
form.s-table-container(method="post" action=rel("/api/unlink"))
|
||||
input(type="hidden" name="guild_id" value=guild_id)
|
||||
table.s-table.s-table__bx-simple
|
||||
each row in linkedChannelsWithDetails
|
||||
tr
|
||||
td.w40: +discord(row.channel)
|
||||
td.p2: button.s-btn.s-btn__muted.s-btn__xs(name="channel_id" value=row.channel.id hx-post=rel("/api/unlink") hx-trigger="click" hx-disabled-elt="this")!= icons.Icons.IconLinkSm
|
||||
td.p2: button.s-btn.s-btn__muted.s-btn__xs(name="channel_id" cx-prevent-default hx-post=rel("/api/unlink") hx-confirm="Do you want to unlink these channels?\nIt may take a moment to clean up Matrix resources." value=row.channel.id hx-indicator="this" hx-disabled-elt="this")!= icons.Icons.IconLinkSm
|
||||
td: +matrix(row)
|
||||
else
|
||||
tr
|
||||
@@ -185,6 +186,19 @@ block body
|
||||
!= icons.Icons.IconMerge
|
||||
= ` Link`
|
||||
|
||||
h3.mt32.fs-category Unlink server
|
||||
form.s-card.d-flex.gx16.ai-center(method="post" action=rel("/api/unlink-space"))
|
||||
input(type="hidden" name="guild_id" value=guild.id)
|
||||
.fl-grow1.s-prose.s-prose__sm.lh-lg
|
||||
p.fc-medium.
|
||||
Not using this bridge, or just made a mistake? You can unlink the whole server and all its channels.#[br]
|
||||
This may take a minute to process. Please be patient and wait until the page refreshes.
|
||||
div
|
||||
button.s-btn.s-btn__icon.s-btn__danger.s-btn__outlined(cx-prevent-default hx-post=rel("/api/unlink-space") hx-confirm="Do you want to unlink this server and all its channels?\nIt may take a minute to clean up Matrix resources." hx-indicator="this" hx-disabled-elt="this")
|
||||
!= icons.Icons.IconUnsync
|
||||
span.ml4= ` Unlink`
|
||||
|
||||
if space_id
|
||||
details.mt48
|
||||
summary Debug room list
|
||||
.d-grid.grid__2.gx24
|
||||
@@ -205,7 +219,7 @@ block body
|
||||
ul.my8.ml24
|
||||
each row in removedWrongTypeChannels
|
||||
li: a(href=`https://discord.com/channels/${guild_id}/${row.id}`) (#{row.type}) #{row.name}
|
||||
h3.mt24 Unavailable channels: Bridge can't access
|
||||
h3.mt24 Unavailable channels: Discord bot can't access
|
||||
.s-card.p0
|
||||
ul.my8.ml24
|
||||
each row in removedPrivateChannels
|
||||
|
||||
@@ -42,12 +42,23 @@ block body
|
||||
| You need to log in with Matrix first.
|
||||
a.s-btn.s-btn__matrix.s-btn__outlined(href=rel(`/log-in-with-matrix`, {next: `./guild?guild_id=${guild_id}`})) Log in with Matrix
|
||||
|
||||
h3.mt48.fs-category Auto-create
|
||||
.s-card
|
||||
h3.mt48.fs-category Other choices
|
||||
.s-card.d-grid.g16
|
||||
form.d-flex.ai-center.g8(method="post" action=rel("/api/autocreate") hx-post=rel("/api/autocreate") hx-indicator="#easy-mode-button")
|
||||
input(type="hidden" name="guild_id" value=guild_id)
|
||||
input(type="hidden" name="autocreate" value="true")
|
||||
label.s-label.fl-grow1
|
||||
| Changed your mind?
|
||||
| Do it automatically
|
||||
p.s-description If you want, OOYE can create and manage the Matrix space so you don't have to.
|
||||
button.s-btn.s-btn__outlined#easy-mode-button Use easy mode
|
||||
button.s-btn.s-btn__icon.s-btn__outlined#easy-mode-button
|
||||
!= icons.Icons.IconWand
|
||||
span.ml4= ` Use easy mode`
|
||||
|
||||
form.d-flex.gx16.ai-center(method="post" action=rel("/api/unlink-space"))
|
||||
input(type="hidden" name="guild_id" value=guild.id)
|
||||
label.s-label.fl-grow1
|
||||
| Cancel
|
||||
p.s-description Don't want to link this server after all? Here's the button for you.
|
||||
button.s-btn.s-btn__icon.s-btn__muted.s-btn__outlined(cx-prevent-default hx-post=rel("/api/unlink-space") hx-indicator="this" hx-disabled-elt="this")
|
||||
!= icons.Icons.IconUnsync
|
||||
span.ml4= ` Unlink`
|
||||
|
||||
@@ -129,6 +129,13 @@ html(lang="en")
|
||||
document.styleSheets[0].insertRule(t, document.styleSheets[0].cssRules.length)
|
||||
})
|
||||
})
|
||||
//- Prevent default
|
||||
script.
|
||||
document.querySelectorAll("[cx-prevent-default]").forEach(e => {
|
||||
e.addEventListener("click", event => {
|
||||
event.preventDefault()
|
||||
})
|
||||
})
|
||||
script(src=rel("/static/htmx.js"))
|
||||
//- Error dialog
|
||||
aside.s-modal#server-error(aria-hidden="true")
|
||||
|
||||
@@ -9,11 +9,8 @@ const DiscordTypes = require("discord-api-types/v10")
|
||||
const {discord, db, as, sync, select, from} = require("../../passthrough")
|
||||
/** @type {import("../auth")} */
|
||||
const auth = sync.require("../auth")
|
||||
/** @type {import("../../matrix/mreq")} */
|
||||
const mreq = sync.require("../../matrix/mreq")
|
||||
/** @type {import("../../matrix/utils")}*/
|
||||
const utils = sync.require("../../matrix/utils")
|
||||
const {reg} = require("../../matrix/read-registration")
|
||||
|
||||
/**
|
||||
* @param {H3Event} event
|
||||
@@ -42,6 +39,15 @@ function getCreateSpace(event) {
|
||||
return event.context.createSpace || sync.require("../../d2m/actions/create-space")
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {H3Event} event
|
||||
* @returns {import("snowtransfer").SnowTransfer}
|
||||
*/
|
||||
function getSnow(event) {
|
||||
/* c8 ignore next */
|
||||
return event.context.snow || discord.snow
|
||||
}
|
||||
|
||||
const schema = {
|
||||
linkSpace: z.object({
|
||||
guild_id: z.string(),
|
||||
@@ -55,7 +61,37 @@ const schema = {
|
||||
unlink: z.object({
|
||||
guild_id: z.string(),
|
||||
channel_id: z.string()
|
||||
})
|
||||
}),
|
||||
unlinkSpace: z.object({
|
||||
guild_id: z.string(),
|
||||
}),
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {H3Event} event
|
||||
* @param {string} channel_id
|
||||
* @param {string} guild_id
|
||||
*/
|
||||
async function validateAndUnbridgeChannel(event, channel_id, guild_id) {
|
||||
const createRoom = getCreateRoom(event)
|
||||
|
||||
// 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`})
|
||||
|
||||
// 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}
|
||||
}
|
||||
|
||||
// Do it
|
||||
await createRoom.unbridgeChannel(channel, guild_id)
|
||||
}
|
||||
|
||||
as.router.post("/api/link-space", defineEventHandler(async event => {
|
||||
@@ -195,7 +231,6 @@ 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 managed = await auth.getManagedGuilds(event)
|
||||
const createRoom = getCreateRoom(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"})
|
||||
@@ -204,24 +239,56 @@ as.router.post("/api/unlink", defineEventHandler(async event => {
|
||||
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"})
|
||||
|
||||
// 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 validateAndUnbridgeChannel(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 managed = await auth.getManagedGuilds(event)
|
||||
const api = getAPI(event)
|
||||
const snow = getSnow(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"})
|
||||
|
||||
const active = select("guild_active", "guild_id", {guild_id: guild_id}).get()
|
||||
if (!active) {
|
||||
throw createError({status: 400, message: "Bad Request", data: "Discord guild has not been considered for bridging"})
|
||||
}
|
||||
|
||||
// Check if there are Matrix resources
|
||||
const spaceID = select("guild_space", "space_id", {guild_id: guild_id}).pluck().get()
|
||||
if (spaceID) {
|
||||
// Unlink all rooms
|
||||
const linkedChannels = select("channel_room", ["channel_id", "room_id", "name", "nick"], {guild_id: guild_id}).all()
|
||||
for (const channel of linkedChannels) {
|
||||
await validateAndUnbridgeChannel(event, channel.channel_id, guild_id)
|
||||
}
|
||||
|
||||
// Verify all rooms were unlinked
|
||||
const remainingLinkedChannels = select("channel_room", ["channel_id", "room_id", "name", "nick"], {guild_id: guild_id}).all()
|
||||
if (remainingLinkedChannels.length) {
|
||||
throw createError({status: 500, message: "Internal Server Error", data: "Failed to unlink some rooms. Please try doing it manually, or report a bug. The space will not be unlinked until all rooms are."})
|
||||
}
|
||||
|
||||
// Unlink space
|
||||
await utils.setUserPower(spaceID, utils.bot, 0, api)
|
||||
await api.leaveRoom(spaceID)
|
||||
db.prepare("DELETE FROM guild_space WHERE guild_id = ? AND space_id = ?").run(guild_id, spaceID)
|
||||
}
|
||||
|
||||
// Mark as not considered for bridging
|
||||
db.prepare("DELETE FROM guild_active WHERE guild_id = ?").run(guild_id)
|
||||
db.prepare("DELETE FROM invite WHERE room_id = ?").run(spaceID)
|
||||
await snow.user.leaveGuild(guild_id)
|
||||
|
||||
setResponseHeader(event, "HX-Redirect", "/")
|
||||
return null
|
||||
}))
|
||||
|
||||
@@ -613,7 +613,7 @@ test("web unlink room: checks that the channel is part of the guild", async t =>
|
||||
t.equal(error.data, "Channel ID 112760669178241024 is not part of guild 665289423482519565")
|
||||
})
|
||||
|
||||
test("web unlink room: successfully calls unbridgeDeletedChannel when the channel does exist", async t => {
|
||||
test("web unlink room: successfully calls unbridgeChannel when the channel does exist", async t => {
|
||||
let called = 0
|
||||
await router.test("post", "/api/unlink", {
|
||||
sessionData: {
|
||||
@@ -624,7 +624,7 @@ test("web unlink room: successfully calls unbridgeDeletedChannel when the channe
|
||||
guild_id: "665289423482519565"
|
||||
},
|
||||
createRoom: {
|
||||
async unbridgeDeletedChannel(channel) {
|
||||
async unbridgeChannel(channel) {
|
||||
called++
|
||||
t.equal(channel.id, "665310973967597573")
|
||||
}
|
||||
@@ -633,7 +633,7 @@ test("web unlink room: successfully calls unbridgeDeletedChannel when the channe
|
||||
t.equal(called, 1)
|
||||
})
|
||||
|
||||
test("web unlink room: successfully calls unbridgeDeletedChannel when the channel does not exist", async t => {
|
||||
test("web unlink room: successfully calls unbridgeChannel when the channel does not exist", async t => {
|
||||
let called = 0
|
||||
await router.test("post", "/api/unlink", {
|
||||
sessionData: {
|
||||
@@ -644,7 +644,7 @@ test("web unlink room: successfully calls unbridgeDeletedChannel when the channe
|
||||
guild_id: "112760669178241024"
|
||||
},
|
||||
createRoom: {
|
||||
async unbridgeDeletedChannel(channel) {
|
||||
async unbridgeChannel(channel) {
|
||||
called++
|
||||
t.equal(channel.id, "489237891895768942")
|
||||
}
|
||||
@@ -654,7 +654,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"]
|
||||
@@ -665,4 +667,179 @@ 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 => {
|
||||
db.exec("BEGIN TRANSACTION")
|
||||
db.prepare("DELETE FROM guild_active 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, "Discord guild has not been considered for bridging")
|
||||
|
||||
db.exec("ROLLBACK") // ぬ
|
||||
})
|
||||
|
||||
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 unbridgeChannel(channel, guildID) {
|
||||
unbridgedChannel = true
|
||||
t.ok(["1438284564815548418", "665310973967597573"].includes(channel.id))
|
||||
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, "Failed to unlink some rooms. Please try doing it manually, or report a bug. The space will not be unlinked until all rooms are.")
|
||||
t.equal(unbridgedChannel, true)
|
||||
})
|
||||
|
||||
test("web unlink space: successfully calls unbridgeChannel 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 unbridgeChannel(channel, guildID) {
|
||||
unbridgedChannel = true
|
||||
t.ok(["1438284564815548418", "665310973967597573"].includes(channel.id))
|
||||
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 channel_id = ?").run(channel.id)
|
||||
}
|
||||
},
|
||||
snow: {
|
||||
user: {
|
||||
// @ts-ignore - snowtransfer or discord-api-types broken, 204 No Content should be mapped to void but is actually mapped to never
|
||||
async leaveGuild(guildID) {
|
||||
t.equal(guildID, "665289423482519565")
|
||||
}
|
||||
}
|
||||
},
|
||||
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 getStateEvent(roomID, type, key) { // getting power levels from space to apply to room
|
||||
t.equal(type, "m.room.power_levels")
|
||||
t.equal(key, "")
|
||||
return {users: {"@_ooye_bot:cadence.moe": 100, "@example:matrix.org": 50}, events: {"m.room.tombstone": 100}}
|
||||
},
|
||||
|
||||
async getStateEventOuter(roomID, type, key) {
|
||||
t.equal(roomID, "!zTMspHVUBhFLLSdmnS:cadence.moe")
|
||||
t.equal(type, "m.room.create")
|
||||
t.equal(key, "")
|
||||
return {
|
||||
type: "m.room.create",
|
||||
state_key: "",
|
||||
sender: "@_ooye_bot:cadence.moe",
|
||||
room_id: "!zTMspHVUBhFLLSdmnS:cadence.moe",
|
||||
event_id: "$create",
|
||||
origin_server_ts: 0,
|
||||
content: {
|
||||
room_version: "11"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
async sendState(roomID, type, key, content) {
|
||||
downgradedPowerLevel = true
|
||||
t.equal(roomID, "!zTMspHVUBhFLLSdmnS:cadence.moe")
|
||||
t.equal(type, "m.room.power_levels")
|
||||
t.notOk(me in content.users, `got ${JSON.stringify(content)} but expected bot user to not be present`)
|
||||
return ""
|
||||
},
|
||||
|
||||
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)
|
||||
})
|
||||
|
||||
@@ -22,7 +22,7 @@ INSERT INTO channel_room (channel_id, room_id, name, nick, thread_parent, custom
|
||||
('176333891320283136', '!qzDBLKlildpzrrOnFZ:cadence.moe', '🌈丨davids-horse_she-took-the-kids', 'wonderland', NULL, 'mxc://cadence.moe/EVvrSkKIRONHjtRJsMLmHWLS', '112760669178241024'),
|
||||
('489237891895768942', '!tnedrGVYKFNUdnegvf:tchncs.de', 'ex-room-doesnt-exist-any-more', NULL, NULL, NULL, '66192955777486848'),
|
||||
('1160894080998461480', '!TqlyQmifxGUggEmdBN:cadence.moe', 'ooyexperiment', NULL, NULL, NULL, '66192955777486848'),
|
||||
('1161864271370666075', '!mHmhQQPwXNananMUqq:cadence.moe', 'updates', NULL, NULL, NULL, '665289423482519565'),
|
||||
('1161864271370666075', '!mHmhQQPwXNananMUqq:cadence.moe', 'updates', NULL, NULL, NULL, '112760669178241024'),
|
||||
('1438284564815548418', '!MHxNpwtgVqWOrmyoTn:cadence.moe', 'sin-cave', NULL, NULL, NULL, '665289423482519565'),
|
||||
('598707048112193536', '!JBxeGYnzQwLnaooOLD:cadence.moe', 'winners', NULL, NULL, NULL, '1345641201902288987');
|
||||
|
||||
|
||||
Reference in New Issue
Block a user