Support creating v12 rooms

This commit is contained in:
Cadence Ember
2025-12-16 02:15:17 +13:00
parent a6bb248c0a
commit e4d0838af5
10 changed files with 149 additions and 38 deletions

View File

@@ -126,15 +126,21 @@ async function channelToKState(channel, guild, di) {
const everyoneCanSend = dUtils.hasPermission(everyonePermissions, DiscordTypes.PermissionFlagsBits.SendMessages)
const everyoneCanMentionEveryone = dUtils.hasAllPermissions(everyonePermissions, ["MentionEveryone"])
const globalAdmins = select("member_power", ["mxid", "power_level"], {room_id: "*"}).all()
const globalAdminPower = globalAdmins.reduce((a, c) => (a[c.mxid] = c.power_level, a), {})
/** @type {Ty.Event.M_Power_Levels} */
const spacePowerEvent = await di.api.getStateEvent(guildSpaceID, "m.room.power_levels", "")
const spacePower = spacePowerEvent.users
const globalAdmins = select("member_power", ["mxid", "power_level"], {room_id: "*"}).all()
const globalAdminPower = globalAdmins.reduce((a, c) => (a[c.mxid] = c.power_level, a), {})
const additionalCreators = select("member_power", "mxid", {room_id: "*"}, "AND power_level > 100").pluck().all()
const creationContent = {}
creationContent.additional_creators = additionalCreators
if (channel.type === DiscordTypes.ChannelType.GuildForum) creationContent.type = "m.space"
/** @type {any} */
const channelKState = {
"m.room.create/": creationContent,
"m.room.name/": {name: convertedName},
"m.room.topic/": {topic: convertedTopic},
"m.room.avatar/": avatarEventContent,
@@ -193,7 +199,7 @@ async function channelToKState(channel, guild, di) {
/**
* Create a bridge room, store the relationship in the database, and add it to the guild's space.
* @param {DiscordTypes.APIGuildTextChannel} channel
* @param guild
* @param {DiscordTypes.APIGuild} guild
* @param {string} spaceID
* @param {any} kstate
* @param {number} privacyLevel
@@ -203,9 +209,6 @@ async function createRoom(channel, guild, spaceID, kstate, privacyLevel) {
let threadParent = null
if (channel.type === DiscordTypes.ChannelType.PublicThread) threadParent = channel.parent_id
let spaceCreationContent = {}
if (channel.type === DiscordTypes.ChannelType.GuildForum) spaceCreationContent = {creation_content: {type: "m.space"}}
// Name and topic can be done earlier in room creation rather than in initial_state
// https://spec.matrix.org/latest/client-server-api/#creation
const name = kstate["m.room.name/"].name
@@ -215,7 +218,7 @@ async function createRoom(channel, guild, spaceID, kstate, privacyLevel) {
delete kstate["m.room.topic/"]
assert(topic)
const roomID = await postApplyPowerLevels(kstate, async kstate => {
const roomCreate = await postApplyPowerLevels(kstate, async kstate => {
const roomID = await api.createRoom({
name,
topic,
@@ -223,16 +226,20 @@ async function createRoom(channel, guild, spaceID, kstate, privacyLevel) {
visibility: PRIVACY_ENUMS.VISIBILITY[privacyLevel],
invite: [],
initial_state: await ks.kstateToState(kstate),
...spaceCreationContent
creation_content: ks.kstateToCreationContent(kstate)
})
/** @type {Ty.Event.StateOuter<Ty.Event.M_Room_Create>} */
const roomCreate = await api.getStateEventOuter(roomID, "m.room.create", "")
db.transaction(() => {
db.prepare("INSERT INTO channel_room (channel_id, room_id, name, nick, thread_parent) VALUES (?, ?, ?, NULL, ?)").run(channel.id, roomID, channel.name, threadParent)
db.prepare("INSERT INTO channel_room (channel_id, room_id, name, nick, thread_parent, guild_id) VALUES (?, ?, ?, NULL, ?, ?)").run(channel.id, roomID, channel.name, threadParent, guild.id)
db.prepare("INSERT INTO historical_channel_room (reference_channel_id, room_id, upgraded_timestamp) VALUES (?, ?, 0)").run(channel.id, roomID)
})()
return roomID
return roomCreate
})
const roomID = roomCreate.room_id
// Put the newly created child into the space
await _syncSpaceMember(channel, spaceID, roomID, guild.id)
@@ -247,25 +254,30 @@ async function createRoom(channel, guild, spaceID, kstate, privacyLevel) {
* https://github.com/matrix-org/synapse/blob/develop/synapse/handlers/room.py#L1170-L1210
* https://github.com/matrix-org/matrix-spec/issues/492
* @param {any} kstate
* @param {(_: any) => Promise<string>} callback must return room ID
* @returns {Promise<string>} room ID
* @param {(_: any) => Promise<Ty.Event.StateOuter<Ty.Event.M_Room_Create>>} callback must return room ID and room version
* @returns {Promise<Ty.Event.StateOuter<Ty.Event.M_Room_Create>>} room ID
*/
async function postApplyPowerLevels(kstate, callback) {
const powerLevelContent = kstate["m.room.power_levels/"]
const kstateWithoutPowerLevels = {...kstate}
delete kstateWithoutPowerLevels["m.room.power_levels/"]
/** @type {string} */
const roomID = await callback(kstateWithoutPowerLevels)
const roomCreate = await callback(kstateWithoutPowerLevels)
const roomID = roomCreate.room_id
// Now *really* apply the power level overrides on top of what Synapse *really* set
if (powerLevelContent) {
const newRoomKState = await ks.roomToKState(roomID)
const newRoomPowerLevelsDiff = ks.diffKState(newRoomKState, {"m.room.power_levels/": powerLevelContent})
await ks.applyKStateDiffToRoom(roomID, newRoomPowerLevelsDiff)
mUtils.removeCreatorsFromPowerLevels(roomCreate, powerLevelContent)
const originalPowerLevels = await api.getStateEvent(roomID, "m.room.power_levels", "")
const powerLevelsDiff = ks.diffKState(
{"m.room.power_levels/": originalPowerLevels},
{"m.room.power_levels/": powerLevelContent}
)
await ks.applyKStateDiffToRoom(roomID, powerLevelsDiff)
}
return roomID
return roomCreate
}
/**
@@ -392,8 +404,8 @@ async function _syncRoom(channelID, shouldActuallySync) {
// sync channel state to room
const roomKState = await ks.roomToKState(roomID)
if (+roomKState["m.room.create/"].room_version <= 8) {
// join_rule `restricted` is not available in room version < 8 and not working properly in version == 8
if (!mUtils.roomHasAtLeastVersion(roomKState["m.room.create/"].room_version, 9)) {
// join_rule `restricted` is not available in room version < 8 and not working properly in version == 8, so require version 9
// read more: https://spec.matrix.org/v1.8/rooms/v9/
// we have to use `public` instead, otherwise the room will be unjoinable.
channelKState["m.room.join_rules/"] = {join_rule: "public"}

View File

@@ -124,7 +124,9 @@ test("channel2room: read-only discord channel", async t => {
return {}
}
const expected = {
"chat.schildi.hide_ui/read_receipts": {},
"m.room.create/": {
additional_creators: ["@test_auto_invite:example.org"],
},
"m.room.avatar/": {
url: {
$url: "/icons/112760669178241024/a_f83622e09ead74f0c5c527fe241f8f8c.png?size=1024",
@@ -161,7 +163,7 @@ test("channel2room: read-only discord channel", async t => {
room: 20,
},
users: {
"@test_auto_invite:example.org": 100,
"@test_auto_invite:example.org": 150,
},
},
"m.space.parent/!jjmvBegULiLucuWEHU:cadence.moe": {

View File

@@ -35,8 +35,8 @@ async function createSpace(guild, kstate) {
const enablePresenceByDefault = +(memberCount < 50) // scary! all active users in a presence-enabled guild will be pinging the server every <30 seconds to stay online
const globalAdmins = select("member_power", "mxid", {room_id: "*"}).pluck().all()
const roomID = await createRoom.postApplyPowerLevels(kstate, async kstate => {
return api.createRoom({
const roomCreate = await createRoom.postApplyPowerLevels(kstate, async kstate => {
const roomID = await api.createRoom({
name,
preset: createRoom.PRIVACY_ENUMS.PRESET[createRoom.DEFAULT_PRIVACY_LEVEL], // New spaces will have to use the default privacy level; we obviously can't look up the existing entry
visibility: createRoom.PRIVACY_ENUMS.VISIBILITY[createRoom.DEFAULT_PRIVACY_LEVEL],
@@ -46,12 +46,14 @@ async function createSpace(guild, kstate) {
},
invite: globalAdmins,
topic,
creation_content: {
type: "m.space"
},
initial_state: await ks.kstateToState(kstate)
initial_state: await ks.kstateToState(kstate),
creation_content: ks.kstateToCreationContent(kstate)
})
const roomCreate = await api.getStateEventOuter(roomID, "m.room.create", "")
return roomCreate
})
const roomID = roomCreate.room_id
db.prepare("INSERT INTO guild_space (guild_id, space_id, presence) VALUES (?, ?, ?)").run(guild.id, roomID, enablePresenceByDefault)
return roomID
}
@@ -63,7 +65,13 @@ async function createSpace(guild, kstate) {
async function guildToKState(guild, privacyLevel) {
assert.equal(typeof privacyLevel, "number")
const globalAdmins = select("member_power", ["mxid", "power_level"], {room_id: "*"}).all()
const additionalCreators = select("member_power", "mxid", {room_id: "*"}, "AND power_level > 100").pluck().all()
const guildKState = {
"m.room.create/": {
type: "m.space",
additional_creators: additionalCreators
},
"m.room.name/": {name: guild.name},
"m.room.avatar/": {
$if: guild.icon,

View File

@@ -13,6 +13,10 @@ test("guild2space: can generate kstate for a guild, passing privacy level 0", as
t.deepEqual(
await kstateUploadMxc(kstateStripConditionals(await guildToKState(testData.guild.general, 0))),
{
"m.room.create/": {
additional_creators: ["@test_auto_invite:example.org"],
type: "m.space"
},
"m.room.avatar/": {
url: "mxc://cadence.moe/zKXGZhmImMHuGQZWJEFKJbsF"
},
@@ -30,7 +34,7 @@ test("guild2space: can generate kstate for a guild, passing privacy level 0", as
},
"m.room.power_levels/": {
users: {
"@test_auto_invite:example.org": 100
"@test_auto_invite:example.org": 150
},
},
}