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:
Elliu
2026-02-13 19:12:01 +13:00
committed by Cadence Ember
parent 1defd83fde
commit 35e9c9e1ea
9 changed files with 322 additions and 55 deletions

View File

@@ -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

View File

@@ -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)
}
}

View File

@@ -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)
},
/**

View File

@@ -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

View File

@@ -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`

View File

@@ -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")

View File

@@ -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
}))

View File

@@ -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)
})

View File

@@ -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');