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 everyoneCanSend = dUtils.hasPermission(everyonePermissions, DiscordTypes.PermissionFlagsBits.SendMessages)
const everyoneCanMentionEveryone = dUtils.hasAllPermissions(everyonePermissions, ["MentionEveryone"]) 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} */ /** @type {Ty.Event.M_Power_Levels} */
const spacePowerEvent = await di.api.getStateEvent(guildSpaceID, "m.room.power_levels", "") const spacePowerEvent = await di.api.getStateEvent(guildSpaceID, "m.room.power_levels", "")
const spacePower = spacePowerEvent.users 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} */ /** @type {any} */
const channelKState = { const channelKState = {
"m.room.create/": creationContent,
"m.room.name/": {name: convertedName}, "m.room.name/": {name: convertedName},
"m.room.topic/": {topic: convertedTopic}, "m.room.topic/": {topic: convertedTopic},
"m.room.avatar/": avatarEventContent, "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. * Create a bridge room, store the relationship in the database, and add it to the guild's space.
* @param {DiscordTypes.APIGuildTextChannel} channel * @param {DiscordTypes.APIGuildTextChannel} channel
* @param guild * @param {DiscordTypes.APIGuild} guild
* @param {string} spaceID * @param {string} spaceID
* @param {any} kstate * @param {any} kstate
* @param {number} privacyLevel * @param {number} privacyLevel
@ -203,9 +209,6 @@ async function createRoom(channel, guild, spaceID, kstate, privacyLevel) {
let threadParent = null let threadParent = null
if (channel.type === DiscordTypes.ChannelType.PublicThread) threadParent = channel.parent_id 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 // Name and topic can be done earlier in room creation rather than in initial_state
// https://spec.matrix.org/latest/client-server-api/#creation // https://spec.matrix.org/latest/client-server-api/#creation
const name = kstate["m.room.name/"].name const name = kstate["m.room.name/"].name
@ -215,7 +218,7 @@ async function createRoom(channel, guild, spaceID, kstate, privacyLevel) {
delete kstate["m.room.topic/"] delete kstate["m.room.topic/"]
assert(topic) assert(topic)
const roomID = await postApplyPowerLevels(kstate, async kstate => { const roomCreate = await postApplyPowerLevels(kstate, async kstate => {
const roomID = await api.createRoom({ const roomID = await api.createRoom({
name, name,
topic, topic,
@ -223,16 +226,20 @@ async function createRoom(channel, guild, spaceID, kstate, privacyLevel) {
visibility: PRIVACY_ENUMS.VISIBILITY[privacyLevel], visibility: PRIVACY_ENUMS.VISIBILITY[privacyLevel],
invite: [], invite: [],
initial_state: await ks.kstateToState(kstate), 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.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) 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 // Put the newly created child into the space
await _syncSpaceMember(channel, spaceID, roomID, guild.id) 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/synapse/blob/develop/synapse/handlers/room.py#L1170-L1210
* https://github.com/matrix-org/matrix-spec/issues/492 * https://github.com/matrix-org/matrix-spec/issues/492
* @param {any} kstate * @param {any} kstate
* @param {(_: any) => Promise<string>} callback must return room ID * @param {(_: any) => Promise<Ty.Event.StateOuter<Ty.Event.M_Room_Create>>} callback must return room ID and room version
* @returns {Promise<string>} room ID * @returns {Promise<Ty.Event.StateOuter<Ty.Event.M_Room_Create>>} room ID
*/ */
async function postApplyPowerLevels(kstate, callback) { async function postApplyPowerLevels(kstate, callback) {
const powerLevelContent = kstate["m.room.power_levels/"] const powerLevelContent = kstate["m.room.power_levels/"]
const kstateWithoutPowerLevels = {...kstate} const kstateWithoutPowerLevels = {...kstate}
delete kstateWithoutPowerLevels["m.room.power_levels/"] delete kstateWithoutPowerLevels["m.room.power_levels/"]
/** @type {string} */ const roomCreate = await callback(kstateWithoutPowerLevels)
const roomID = await callback(kstateWithoutPowerLevels) const roomID = roomCreate.room_id
// Now *really* apply the power level overrides on top of what Synapse *really* set // Now *really* apply the power level overrides on top of what Synapse *really* set
if (powerLevelContent) { if (powerLevelContent) {
const newRoomKState = await ks.roomToKState(roomID) mUtils.removeCreatorsFromPowerLevels(roomCreate, powerLevelContent)
const newRoomPowerLevelsDiff = ks.diffKState(newRoomKState, {"m.room.power_levels/": powerLevelContent})
await ks.applyKStateDiffToRoom(roomID, newRoomPowerLevelsDiff) 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 // sync channel state to room
const roomKState = await ks.roomToKState(roomID) const roomKState = await ks.roomToKState(roomID)
if (+roomKState["m.room.create/"].room_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 // 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/ // read more: https://spec.matrix.org/v1.8/rooms/v9/
// we have to use `public` instead, otherwise the room will be unjoinable. // we have to use `public` instead, otherwise the room will be unjoinable.
channelKState["m.room.join_rules/"] = {join_rule: "public"} channelKState["m.room.join_rules/"] = {join_rule: "public"}

View file

@ -124,7 +124,9 @@ test("channel2room: read-only discord channel", async t => {
return {} return {}
} }
const expected = { const expected = {
"chat.schildi.hide_ui/read_receipts": {}, "m.room.create/": {
additional_creators: ["@test_auto_invite:example.org"],
},
"m.room.avatar/": { "m.room.avatar/": {
url: { url: {
$url: "/icons/112760669178241024/a_f83622e09ead74f0c5c527fe241f8f8c.png?size=1024", $url: "/icons/112760669178241024/a_f83622e09ead74f0c5c527fe241f8f8c.png?size=1024",
@ -161,7 +163,7 @@ test("channel2room: read-only discord channel", async t => {
room: 20, room: 20,
}, },
users: { users: {
"@test_auto_invite:example.org": 100, "@test_auto_invite:example.org": 150,
}, },
}, },
"m.space.parent/!jjmvBegULiLucuWEHU:cadence.moe": { "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 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 globalAdmins = select("member_power", "mxid", {room_id: "*"}).pluck().all()
const roomID = await createRoom.postApplyPowerLevels(kstate, async kstate => { const roomCreate = await createRoom.postApplyPowerLevels(kstate, async kstate => {
return api.createRoom({ const roomID = await api.createRoom({
name, 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 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], visibility: createRoom.PRIVACY_ENUMS.VISIBILITY[createRoom.DEFAULT_PRIVACY_LEVEL],
@ -46,12 +46,14 @@ async function createSpace(guild, kstate) {
}, },
invite: globalAdmins, invite: globalAdmins,
topic, topic,
creation_content: { initial_state: await ks.kstateToState(kstate),
type: "m.space" creation_content: ks.kstateToCreationContent(kstate)
},
initial_state: await ks.kstateToState(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) db.prepare("INSERT INTO guild_space (guild_id, space_id, presence) VALUES (?, ?, ?)").run(guild.id, roomID, enablePresenceByDefault)
return roomID return roomID
} }
@ -63,7 +65,13 @@ async function createSpace(guild, kstate) {
async function guildToKState(guild, privacyLevel) { async function guildToKState(guild, privacyLevel) {
assert.equal(typeof privacyLevel, "number") assert.equal(typeof privacyLevel, "number")
const globalAdmins = select("member_power", ["mxid", "power_level"], {room_id: "*"}).all() 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 = { const guildKState = {
"m.room.create/": {
type: "m.space",
additional_creators: additionalCreators
},
"m.room.name/": {name: guild.name}, "m.room.name/": {name: guild.name},
"m.room.avatar/": { "m.room.avatar/": {
$if: guild.icon, $if: guild.icon,

View file

@ -13,6 +13,10 @@ test("guild2space: can generate kstate for a guild, passing privacy level 0", as
t.deepEqual( t.deepEqual(
await kstateUploadMxc(kstateStripConditionals(await guildToKState(testData.guild.general, 0))), 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/": { "m.room.avatar/": {
url: "mxc://cadence.moe/zKXGZhmImMHuGQZWJEFKJbsF" 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/": { "m.room.power_levels/": {
users: { users: {
"@test_auto_invite:example.org": 100 "@test_auto_invite:example.org": 150
}, },
}, },
} }

View file

@ -66,5 +66,5 @@ test("orm: select unsafe works (to select complex column names that can't be typ
.and("where member_power.room_id = '*' and member_cache.power_level != member_power.power_level") .and("where member_power.room_id = '*' and member_cache.power_level != member_power.power_level")
.selectUnsafe("mxid", "member_cache.room_id", "member_power.power_level") .selectUnsafe("mxid", "member_cache.room_id", "member_power.power_level")
.all() .all()
t.equal(results[0].power_level, 100) t.equal(results[0].power_level, 150)
}) })

View file

@ -1,7 +1,7 @@
// @ts-check // @ts-check
const assert = require("assert").strict const assert = require("assert").strict
const Ty = require("../../types")
const passthrough = require("../../passthrough") const passthrough = require("../../passthrough")
const {db} = passthrough const {db} = passthrough
@ -232,6 +232,49 @@ function getPublicUrlForMxc(mxc) {
return `${reg.ooye.bridge_origin}/download/matrix/${serverAndMediaID}` return `${reg.ooye.bridge_origin}/download/matrix/${serverAndMediaID}`
} }
/**
* @param {string} roomVersionString
* @param {number} desiredVersion
*/
function roomHasAtLeastVersion(roomVersionString, desiredVersion) {
/*
I hate this.
The spec instructs me to compare room versions ordinally, for example, "In room versions 12 and higher..."
So if the real room version is 13, this should pass the check.
However, the spec also says "room versions are not intended to be parsed and should be treated as opaque identifiers", "due to versions not being ordered or hierarchical".
So versions are unordered and opaque and you can't parse them, but you're still expected to parse them to a number and compare them to another number to measure if it's "12 or higher"?
Theoretically MSC3244 would clean this up, but that isn't happening since Element removed support for MSC3244: https://github.com/element-hq/element-web/commit/644b8415912afb9c5eed54859a444a2ee7224117
Element replaced it with the following function:
*/
// Assumption: all unstable room versions don't support the feature. Calling code can check for unstable
// room versions explicitly if it wants to. The spec reserves [0-9] and `.` for its room versions.
if (!roomVersionString.match(/^[\d.]+$/)) {
return false;
}
// Element dev note: While the spec says room versions are not linear, we can make reasonable assumptions
// until the room versions prove themselves to be non-linear in the spec. We should see this coming
// from a mile away and can course-correct this function if needed.
return Number(roomVersionString) >= Number(desiredVersion);
}
/**
* Starting in room version 12, creators may not be specified in power levels users.
* Modifies the input power levels.
* @param {Ty.Event.StateOuter<Ty.Event.M_Room_Create>} roomCreateOuter
* @param {Ty.Event.M_Power_Levels} powerLevels
*/
function removeCreatorsFromPowerLevels(roomCreateOuter, powerLevels) {
assert(roomCreateOuter.sender)
if (roomHasAtLeastVersion(roomCreateOuter.content.room_version, 12)) {
for (const creator of (roomCreateOuter.content.additional_creators ?? []).concat(roomCreateOuter.sender)) {
delete powerLevels.users[creator]
}
}
return powerLevels
}
module.exports.BLOCK_ELEMENTS = BLOCK_ELEMENTS module.exports.BLOCK_ELEMENTS = BLOCK_ELEMENTS
module.exports.eventSenderIsFromDiscord = eventSenderIsFromDiscord module.exports.eventSenderIsFromDiscord = eventSenderIsFromDiscord
module.exports.getPublicUrlForMxc = getPublicUrlForMxc module.exports.getPublicUrlForMxc = getPublicUrlForMxc
@ -239,3 +282,5 @@ module.exports.getEventIDHash = getEventIDHash
module.exports.MatrixStringBuilder = MatrixStringBuilder module.exports.MatrixStringBuilder = MatrixStringBuilder
module.exports.getViaServers = getViaServers module.exports.getViaServers = getViaServers
module.exports.getViaServersQuery = getViaServersQuery module.exports.getViaServersQuery = getViaServersQuery
module.exports.roomHasAtLeastVersion = roomHasAtLeastVersion
module.exports.removeCreatorsFromPowerLevels = removeCreatorsFromPowerLevels

View file

@ -138,6 +138,16 @@ function getStateEvent(roomID, type, key) {
return mreq.mreq("GET", `/client/v3/rooms/${roomID}/state/${type}/${key}`) return mreq.mreq("GET", `/client/v3/rooms/${roomID}/state/${type}/${key}`)
} }
/**
* @param {string} roomID
* @param {string} type
* @param {string} key
* @returns {Promise<Ty.Event.BaseStateEvent>} the entire state event
*/
function getStateEventOuter(roomID, type, key) {
return mreq.mreq("GET", `/client/v3/rooms/${roomID}/state/${type}/${key}?format=event`)
}
/** /**
* @param {string} roomID * @param {string} roomID
* @returns {Promise<Ty.Event.InviteStrippedState[]>} * @returns {Promise<Ty.Event.InviteStrippedState[]>}
@ -554,6 +564,7 @@ module.exports.getEvent = getEvent
module.exports.getEventForTimestamp = getEventForTimestamp module.exports.getEventForTimestamp = getEventForTimestamp
module.exports.getAllState = getAllState module.exports.getAllState = getAllState
module.exports.getStateEvent = getStateEvent module.exports.getStateEvent = getStateEvent
module.exports.getStateEventOuter = getStateEventOuter
module.exports.getInviteState = getInviteState module.exports.getInviteState = getInviteState
module.exports.getJoinedMembers = getJoinedMembers module.exports.getJoinedMembers = getJoinedMembers
module.exports.getMembers = getMembers module.exports.getMembers = getMembers

View file

@ -10,6 +10,8 @@ const {sync} = passthrough
const file = sync.require("./file") const file = sync.require("./file")
/** @type {import("./api")} */ /** @type {import("./api")} */
const api = sync.require("./api") const api = sync.require("./api")
/** @type {import("../m2d/converters/utils")} */
const utils = sync.require("../m2d/converters/utils")
/** Mutates the input. Not recursive - can only include or exclude entire state events. */ /** Mutates the input. Not recursive - can only include or exclude entire state events. */
function kstateStripConditionals(kstate) { function kstateStripConditionals(kstate) {
@ -45,12 +47,13 @@ async function kstateUploadMxc(obj) {
return obj return obj
} }
/** Automatically strips conditionals and uploads URLs to mxc. */ /** Automatically strips conditionals and uploads URLs to mxc. m.room.create is removed. */
async function kstateToState(kstate) { async function kstateToState(kstate) {
const events = [] const events = []
kstateStripConditionals(kstate) kstateStripConditionals(kstate)
await kstateUploadMxc(kstate) await kstateUploadMxc(kstate)
for (const [k, content] of Object.entries(kstate)) { for (const [k, content] of Object.entries(kstate)) {
if (k === "m.room.create/") continue
const slashIndex = k.indexOf("/") const slashIndex = k.indexOf("/")
assert(slashIndex > 0) assert(slashIndex > 0)
const type = k.slice(0, slashIndex) const type = k.slice(0, slashIndex)
@ -60,6 +63,11 @@ async function kstateToState(kstate) {
return events return events
} }
/** Extracts m.room.create for use in room creation_content. */
function kstateToCreationContent(kstate) {
return kstate["m.room.create/"] || {}
}
/** /**
* @param {import("../types").Event.BaseStateEvent[]} events * @param {import("../types").Event.BaseStateEvent[]} events
* @returns {any} * @returns {any}
@ -68,6 +76,11 @@ function stateToKState(events) {
const kstate = {} const kstate = {}
for (const event of events) { for (const event of events) {
kstate[event.type + "/" + event.state_key] = event.content kstate[event.type + "/" + event.state_key] = event.content
// need to remember m.room.create sender for later...
if (event.type === "m.room.create" && event.state_key === "") {
kstate["m.room.create/outer"] = event
}
} }
return kstate return kstate
} }
@ -81,12 +94,21 @@ function diffKState(actual, target) {
if (key === "m.room.power_levels/") { if (key === "m.room.power_levels/") {
// Special handling for power levels, we want to deep merge the actual and target into the final state. // Special handling for power levels, we want to deep merge the actual and target into the final state.
if (!(key in actual)) throw new Error(`want to apply a power levels diff, but original power level data is missing\nstarted with: ${JSON.stringify(actual)}\nwant to apply: ${JSON.stringify(target)}`) if (!(key in actual)) throw new Error(`want to apply a power levels diff, but original power level data is missing\nstarted with: ${JSON.stringify(actual)}\nwant to apply: ${JSON.stringify(target)}`)
const temp = mixin({}, actual[key], target[key]) const mixedTarget = mixin({}, actual[key], target[key])
if (!isDeepStrictEqual(actual[key], temp)) { if (!isDeepStrictEqual(actual[key], mixedTarget)) {
// they differ. use the newly prepared object as the diff. // they differ. use the newly prepared object as the diff.
diff[key] = temp // if the diff includes users, it needs to be cleaned wrt room version 12
if (target[key].users && Object.keys(target[key].users).length > 0) {
if (!("m.room.create/" in actual)) throw new Error(`want to apply a power levels diff, but original m.room.create/ is missing\nstarted with: ${JSON.stringify(actual)}\nwant to apply: ${JSON.stringify(target)}`)
if (!("m.room.create/outer" in actual)) throw new Error(`want to apply a power levels diff, but original m.room.create/outer is missing\nstarted with: ${JSON.stringify(actual)}\nwant to apply: ${JSON.stringify(target)}`)
utils.removeCreatorsFromPowerLevels(actual["m.room.create/outer"], mixedTarget)
}
diff[key] = mixedTarget
} }
} else if (key === "m.room.create/") {
// can't be modified - only for kstateToCreationContent
} else if (key in actual) { } else if (key in actual) {
// diff // diff
if (!isDeepStrictEqual(actual[key], target[key])) { if (!isDeepStrictEqual(actual[key], target[key])) {
@ -129,6 +151,7 @@ async function applyKStateDiffToRoom(roomID, kstate) {
module.exports.kstateStripConditionals = kstateStripConditionals module.exports.kstateStripConditionals = kstateStripConditionals
module.exports.kstateUploadMxc = kstateUploadMxc module.exports.kstateUploadMxc = kstateUploadMxc
module.exports.kstateToState = kstateToState module.exports.kstateToState = kstateToState
module.exports.kstateToCreationContent = kstateToCreationContent
module.exports.stateToKState = stateToKState module.exports.stateToKState = stateToKState
module.exports.diffKState = diffKState module.exports.diffKState = diffKState
module.exports.roomToKState = roomToKState module.exports.roomToKState = roomToKState

7
src/types.d.ts vendored
View file

@ -174,7 +174,7 @@ export namespace Event {
} }
export type M_Room_Create = { export type M_Room_Create = {
additional_creators: string[] additional_creators?: string[]
"m.federate"?: boolean "m.federate"?: boolean
room_version: string room_version: string
type?: string type?: string
@ -356,6 +356,11 @@ export namespace Event {
}> & { }> & {
redacts: string redacts: string
} }
export type M_Room_Tombstone = {
body: string
replacement_room: string
}
} }
export namespace R { export namespace R {

View file

@ -101,6 +101,7 @@ module.exports = {
}, },
room: { room: {
general: { general: {
"m.room.create/": {additional_creators: ["@test_auto_invite:example.org"]},
"m.room.name/": {name: "main"}, "m.room.name/": {name: "main"},
"m.room.topic/": {topic: "#collective-unconscious | https://docs.google.com/document/d/blah/edit | I spread, pipe, and whip because it is my will. :headstone:\n\nChannel ID: 112760669178241024\nGuild ID: 112760669178241024"}, "m.room.topic/": {topic: "#collective-unconscious | https://docs.google.com/document/d/blah/edit | I spread, pipe, and whip because it is my will. :headstone:\n\nChannel ID: 112760669178241024\nGuild ID: 112760669178241024"},
"m.room.guest_access/": {guest_access: "can_join"}, "m.room.guest_access/": {guest_access: "can_join"},
@ -126,7 +127,7 @@ module.exports = {
"m.room.redaction": 0 "m.room.redaction": 0
}, },
users: { users: {
"@test_auto_invite:example.org": 100 "@test_auto_invite:example.org": 150
}, },
notifications: { notifications: {
room: 0 room: 0