Add private/linkable/public privacy rules
This commit is contained in:
parent
1620aae27c
commit
613a1dc086
6 changed files with 76 additions and 36 deletions
|
@ -15,6 +15,24 @@ const ks = sync.require("../../matrix/kstate")
|
||||||
/** @type {import("./create-space")}) */
|
/** @type {import("./create-space")}) */
|
||||||
const createSpace = sync.require("./create-space") // watch out for the require loop
|
const createSpace = sync.require("./create-space") // watch out for the require loop
|
||||||
|
|
||||||
|
/**
|
||||||
|
* There are 3 levels of room privacy:
|
||||||
|
* 0: Room is invite-only.
|
||||||
|
* 1: Anybody can use a link to join.
|
||||||
|
* 2: Room is published in room directory.
|
||||||
|
*/
|
||||||
|
const PRIVACY_ENUMS = {
|
||||||
|
PRESET: ["private_chat", "public_chat", "public_chat"],
|
||||||
|
VISIBILITY: ["private", "private", "public"],
|
||||||
|
SPACE_HISTORY_VISIBILITY: ["invited", "world_readable", "world_readable"], // copying from element client
|
||||||
|
ROOM_HISTORY_VISIBILITY: ["shared", "shared", "world_readable"], // any events sent after <value> are visible, but for world_readable anybody can read without even joining
|
||||||
|
GUEST_ACCESS: ["can_join", "forbidden", "forbidden"], // whether guests can join space if other conditions are met
|
||||||
|
SPACE_JOIN_RULES: ["invite", "public", "public"],
|
||||||
|
ROOM_JOIN_RULES: ["restricted", "public", "public"]
|
||||||
|
}
|
||||||
|
|
||||||
|
const DEFAULT_PRIVACY_LEVEL = 0
|
||||||
|
|
||||||
/** @type {Map<string, Promise<string>>} channel ID -> Promise<room ID> */
|
/** @type {Map<string, Promise<string>>} channel ID -> Promise<room ID> */
|
||||||
const inflightRoomCreate = new Map()
|
const inflightRoomCreate = new Map()
|
||||||
|
|
||||||
|
@ -69,7 +87,9 @@ function convertNameAndTopic(channel, guild, customName) {
|
||||||
*/
|
*/
|
||||||
async function channelToKState(channel, guild) {
|
async function channelToKState(channel, guild) {
|
||||||
const spaceID = await createSpace.ensureSpace(guild)
|
const spaceID = await createSpace.ensureSpace(guild)
|
||||||
assert.ok(typeof spaceID === "string")
|
assert(typeof spaceID === "string")
|
||||||
|
const privacyLevel = select("guild_space", "privacy_level", {space_id: spaceID}).pluck().get()
|
||||||
|
assert(privacyLevel)
|
||||||
|
|
||||||
const row = select("channel_room", ["nick", "custom_avatar"], {channel_id: channel.id}).get()
|
const row = select("channel_room", ["nick", "custom_avatar"], {channel_id: channel.id}).get()
|
||||||
const customName = row?.nick
|
const customName = row?.nick
|
||||||
|
@ -84,27 +104,33 @@ async function channelToKState(channel, guild) {
|
||||||
avatarEventContent.url = await file.uploadDiscordFileToMxc(avatarEventContent.discord_path) // TODO: somehow represent future values in kstate (callbacks?), while still allowing for diffing, so test cases don't need to touch the media API
|
avatarEventContent.url = await file.uploadDiscordFileToMxc(avatarEventContent.discord_path) // TODO: somehow represent future values in kstate (callbacks?), while still allowing for diffing, so test cases don't need to touch the media API
|
||||||
}
|
}
|
||||||
|
|
||||||
let history_visibility = "shared"
|
let history_visibility = PRIVACY_ENUMS.ROOM_HISTORY_VISIBILITY[privacyLevel]
|
||||||
if (channel["thread_metadata"]) history_visibility = "world_readable"
|
if (channel["thread_metadata"]) history_visibility = "world_readable"
|
||||||
|
|
||||||
|
/** @type {{join_rule: string, allow?: any}} */
|
||||||
|
let join_rules = {
|
||||||
|
join_rule: "restricted",
|
||||||
|
allow: [{
|
||||||
|
type: "m.room_membership",
|
||||||
|
room_id: spaceID
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
if (PRIVACY_ENUMS.ROOM_JOIN_RULES[privacyLevel] !== "restricted") {
|
||||||
|
join_rules = {join_rule: PRIVACY_ENUMS.ROOM_JOIN_RULES[privacyLevel]}
|
||||||
|
}
|
||||||
|
|
||||||
const channelKState = {
|
const channelKState = {
|
||||||
"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,
|
||||||
"m.room.guest_access/": {guest_access: "can_join"},
|
"m.room.guest_access/": {guest_access: PRIVACY_ENUMS.GUEST_ACCESS[privacyLevel]},
|
||||||
"m.room.history_visibility/": {history_visibility},
|
"m.room.history_visibility/": {history_visibility},
|
||||||
[`m.space.parent/${spaceID}`]: {
|
[`m.space.parent/${spaceID}`]: {
|
||||||
via: [reg.ooye.server_name],
|
via: [reg.ooye.server_name],
|
||||||
canonical: true
|
canonical: true
|
||||||
},
|
},
|
||||||
/** @type {{join_rule: string, [x: string]: any}} */
|
/** @type {{join_rule: string, [x: string]: any}} */
|
||||||
"m.room.join_rules/": {
|
"m.room.join_rules/": join_rules,
|
||||||
join_rule: "restricted",
|
|
||||||
allow: [{
|
|
||||||
type: "m.room_membership",
|
|
||||||
room_id: spaceID
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
"m.room.power_levels/": {
|
"m.room.power_levels/": {
|
||||||
events: {
|
events: {
|
||||||
"m.room.avatar": 0
|
"m.room.avatar": 0
|
||||||
|
@ -132,7 +158,7 @@ async function channelToKState(channel, guild) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {spaceID, channelKState}
|
return {spaceID, privacyLevel, channelKState}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -141,9 +167,10 @@ async function channelToKState(channel, guild) {
|
||||||
* @param guild
|
* @param guild
|
||||||
* @param {string} spaceID
|
* @param {string} spaceID
|
||||||
* @param {any} kstate
|
* @param {any} kstate
|
||||||
|
* @param {number} privacyLevel
|
||||||
* @returns {Promise<string>} room ID
|
* @returns {Promise<string>} room ID
|
||||||
*/
|
*/
|
||||||
async function createRoom(channel, guild, spaceID, kstate) {
|
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
|
||||||
|
|
||||||
|
@ -160,8 +187,8 @@ async function createRoom(channel, guild, spaceID, kstate) {
|
||||||
const roomID = await api.createRoom({
|
const roomID = await api.createRoom({
|
||||||
name,
|
name,
|
||||||
topic,
|
topic,
|
||||||
preset: "private_chat", // This is closest to what we want, but properties from kstate override it anyway
|
preset: PRIVACY_ENUMS.ROOM_HISTORY_VISIBILITY[privacyLevel], // This is closest to what we want, but properties from kstate override it anyway
|
||||||
visibility: "private", // Not shown in the room directory
|
visibility: PRIVACY_ENUMS.VISIBILITY[privacyLevel],
|
||||||
invite: [],
|
invite: [],
|
||||||
initial_state: ks.kstateToState(kstate)
|
initial_state: ks.kstateToState(kstate)
|
||||||
})
|
})
|
||||||
|
@ -252,8 +279,8 @@ async function _syncRoom(channelID, shouldActuallySync) {
|
||||||
|
|
||||||
if (!existing) {
|
if (!existing) {
|
||||||
const creation = (async () => {
|
const creation = (async () => {
|
||||||
const {spaceID, channelKState} = await channelToKState(channel, guild)
|
const {spaceID, privacyLevel, channelKState} = await channelToKState(channel, guild)
|
||||||
const roomID = await createRoom(channel, guild, spaceID, channelKState)
|
const roomID = await createRoom(channel, guild, spaceID, channelKState, privacyLevel)
|
||||||
inflightRoomCreate.delete(channelID) // OK to release inflight waiters now. they will read the correct `existing` row
|
inflightRoomCreate.delete(channelID) // OK to release inflight waiters now. they will read the correct `existing` row
|
||||||
return roomID
|
return roomID
|
||||||
})()
|
})()
|
||||||
|
@ -371,6 +398,8 @@ async function createAllForGuild(guildID) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
module.exports.DEFAULT_PRIVACY_LEVEL = DEFAULT_PRIVACY_LEVEL
|
||||||
|
module.exports.PRIVACY_ENUMS = PRIVACY_ENUMS
|
||||||
module.exports.createRoom = createRoom
|
module.exports.createRoom = createRoom
|
||||||
module.exports.ensureRoom = ensureRoom
|
module.exports.ensureRoom = ensureRoom
|
||||||
module.exports.syncRoom = syncRoom
|
module.exports.syncRoom = syncRoom
|
||||||
|
|
|
@ -32,8 +32,8 @@ async function createSpace(guild, kstate) {
|
||||||
const roomID = await createRoom.postApplyPowerLevels(kstate, async kstate => {
|
const roomID = await createRoom.postApplyPowerLevels(kstate, async kstate => {
|
||||||
return api.createRoom({
|
return api.createRoom({
|
||||||
name,
|
name,
|
||||||
preset: "private_chat", // cannot join space unless invited
|
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: "private",
|
visibility: createRoom.PRIVACY_ENUMS.VISIBILITY[createRoom.DEFAULT_PRIVACY_LEVEL],
|
||||||
power_level_content_override: {
|
power_level_content_override: {
|
||||||
events_default: 100, // space can only be managed by bridge
|
events_default: 100, // space can only be managed by bridge
|
||||||
invite: 0 // any existing member can invite others
|
invite: 0 // any existing member can invite others
|
||||||
|
@ -51,23 +51,21 @@ async function createSpace(guild, kstate) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {DiscordTypes.APIGuild} guild]
|
* @param {DiscordTypes.APIGuild} guild
|
||||||
|
* @param {number} privacyLevel
|
||||||
*/
|
*/
|
||||||
async function guildToKState(guild) {
|
async function guildToKState(guild, privacyLevel) {
|
||||||
const avatarEventContent = {}
|
const avatarEventContent = {}
|
||||||
if (guild.icon) {
|
if (guild.icon) {
|
||||||
avatarEventContent.discord_path = file.guildIcon(guild)
|
avatarEventContent.discord_path = file.guildIcon(guild)
|
||||||
avatarEventContent.url = await file.uploadDiscordFileToMxc(avatarEventContent.discord_path) // TODO: somehow represent future values in kstate (callbacks?), while still allowing for diffing, so test cases don't need to touch the media API
|
avatarEventContent.url = await file.uploadDiscordFileToMxc(avatarEventContent.discord_path) // TODO: somehow represent future values in kstate (callbacks?), while still allowing for diffing, so test cases don't need to touch the media API
|
||||||
}
|
}
|
||||||
|
|
||||||
let history_visibility = "invited"
|
|
||||||
if (guild["thread_metadata"]) history_visibility = "world_readable"
|
|
||||||
|
|
||||||
const guildKState = {
|
const guildKState = {
|
||||||
"m.room.name/": {name: guild.name},
|
"m.room.name/": {name: guild.name},
|
||||||
"m.room.avatar/": avatarEventContent,
|
"m.room.avatar/": avatarEventContent,
|
||||||
"m.room.guest_access/": {guest_access: "can_join"}, // guests can join space if other conditions are met
|
"m.room.guest_access/": {guest_access: createRoom.PRIVACY_ENUMS.GUEST_ACCESS[privacyLevel]},
|
||||||
"m.room.history_visibility/": {history_visibility: "invited"} // any events sent after user was invited are visible
|
"m.room.history_visibility/": {history_visibility: createRoom.PRIVACY_ENUMS.GUEST_ACCESS[privacyLevel]}
|
||||||
}
|
}
|
||||||
|
|
||||||
return guildKState
|
return guildKState
|
||||||
|
@ -86,11 +84,11 @@ async function _syncSpace(guild, shouldActuallySync) {
|
||||||
await inflightSpaceCreate.get(guild.id) // just waiting, and then doing a new db query afterwards, is the simplest way of doing it
|
await inflightSpaceCreate.get(guild.id) // just waiting, and then doing a new db query afterwards, is the simplest way of doing it
|
||||||
}
|
}
|
||||||
|
|
||||||
const spaceID = select("guild_space", "space_id", {guild_id: guild.id}).pluck().get()
|
const row = select("guild_space", ["space_id", "privacy_level"], {guild_id: guild.id}).get()
|
||||||
|
|
||||||
if (!spaceID) {
|
if (!row) {
|
||||||
const creation = (async () => {
|
const creation = (async () => {
|
||||||
const guildKState = await guildToKState(guild)
|
const guildKState = await guildToKState(guild, createRoom.DEFAULT_PRIVACY_LEVEL) // New spaces will have to use the default privacy level; we obviously can't look up the existing entry
|
||||||
const spaceID = await createSpace(guild, guildKState)
|
const spaceID = await createSpace(guild, guildKState)
|
||||||
inflightSpaceCreate.delete(guild.id)
|
inflightSpaceCreate.delete(guild.id)
|
||||||
return spaceID
|
return spaceID
|
||||||
|
@ -99,13 +97,15 @@ async function _syncSpace(guild, shouldActuallySync) {
|
||||||
return creation // Naturally, the newly created space is already up to date, so we can always skip syncing here.
|
return creation // Naturally, the newly created space is already up to date, so we can always skip syncing here.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const {space_id: spaceID, privacy_level} = row
|
||||||
|
|
||||||
if (!shouldActuallySync) {
|
if (!shouldActuallySync) {
|
||||||
return spaceID // only need to ensure space exists, and it does. return the space ID
|
return spaceID // only need to ensure space exists, and it does. return the space ID
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`[space sync] to matrix: ${guild.name}`)
|
console.log(`[space sync] to matrix: ${guild.name}`)
|
||||||
|
|
||||||
const guildKState = await guildToKState(guild) // calling this in both branches because we don't want to calculate this if not syncing
|
const guildKState = await guildToKState(guild, privacy_level) // calling this in both branches because we don't want to calculate this if not syncing
|
||||||
|
|
||||||
// sync guild state to space
|
// sync guild state to space
|
||||||
const spaceKState = await createRoom.roomToKState(spaceID)
|
const spaceKState = await createRoom.roomToKState(spaceID)
|
||||||
|
@ -159,17 +159,20 @@ async function syncSpaceFully(guildID) {
|
||||||
const guild = discord.guilds.get(guildID)
|
const guild = discord.guilds.get(guildID)
|
||||||
assert.ok(guild)
|
assert.ok(guild)
|
||||||
|
|
||||||
const spaceID = select("guild_space", "space_id", {guild_id: guildID}).pluck().get()
|
const row = select("guild_space", ["space_id", "privacy_level"], {guild_id: guildID}).get()
|
||||||
|
|
||||||
const guildKState = await guildToKState(guild)
|
if (!row) {
|
||||||
|
const guildKState = await guildToKState(guild, createRoom.DEFAULT_PRIVACY_LEVEL)
|
||||||
if (!spaceID) {
|
|
||||||
const spaceID = await createSpace(guild, guildKState)
|
const spaceID = await createSpace(guild, guildKState)
|
||||||
return spaceID // Naturally, the newly created space is already up to date, so we can always skip syncing here.
|
return spaceID // Naturally, the newly created space is already up to date, so we can always skip syncing here.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const {space_id: spaceID, privacy_level} = row
|
||||||
|
|
||||||
console.log(`[space sync] to matrix: ${guild.name}`)
|
console.log(`[space sync] to matrix: ${guild.name}`)
|
||||||
|
|
||||||
|
const guildKState = await guildToKState(guild, privacy_level)
|
||||||
|
|
||||||
// sync guild state to space
|
// sync guild state to space
|
||||||
const spaceKState = await createRoom.roomToKState(spaceID)
|
const spaceKState = await createRoom.roomToKState(spaceID)
|
||||||
const spaceDiff = ks.diffKState(spaceKState, guildKState)
|
const spaceDiff = ks.diffKState(spaceKState, guildKState)
|
||||||
|
|
|
@ -40,9 +40,11 @@ async function sendMessage(message, guild) {
|
||||||
}
|
}
|
||||||
for (const event of events) {
|
for (const event of events) {
|
||||||
const eventType = event.$type
|
const eventType = event.$type
|
||||||
/** @type {Pick<typeof event, Exclude<keyof event, "$type">> & { $type?: string }} */
|
if (event.$sender) senderMxid = event.$sender
|
||||||
|
/** @type {Pick<typeof event, Exclude<keyof event, "$type" | "$sender">> & { $type?: string, $sender?: string }} */
|
||||||
const eventWithoutType = {...event}
|
const eventWithoutType = {...event}
|
||||||
delete eventWithoutType.$type
|
delete eventWithoutType.$type
|
||||||
|
delete eventWithoutType.$sender
|
||||||
|
|
||||||
const useTimestamp = message["backfill"] ? new Date(message.timestamp).getTime() : undefined
|
const useTimestamp = message["backfill"] ? new Date(message.timestamp).getTime() : undefined
|
||||||
const eventID = await api.sendEvent(roomID, eventType, eventWithoutType, senderMxid, useTimestamp)
|
const eventID = await api.sendEvent(roomID, eventType, eventWithoutType, senderMxid, useTimestamp)
|
||||||
|
|
5
db/migrations/0006-add-privacy-to-space.sql
Normal file
5
db/migrations/0006-add-privacy-to-space.sql
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
BEGIN TRANSACTION;
|
||||||
|
|
||||||
|
ALTER TABLE guild_space ADD COLUMN privacy_level TEXT NOT NULL DEFAULT 0;
|
||||||
|
|
||||||
|
COMMIT;
|
1
db/orm-defs.d.ts
vendored
1
db/orm-defs.d.ts
vendored
|
@ -25,6 +25,7 @@ export type Models = {
|
||||||
guild_space: {
|
guild_space: {
|
||||||
guild_id: string
|
guild_id: string
|
||||||
space_id: string
|
space_id: string
|
||||||
|
privacy_level: number
|
||||||
}
|
}
|
||||||
|
|
||||||
lottie: {
|
lottie: {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
BEGIN TRANSACTION;
|
BEGIN TRANSACTION;
|
||||||
|
|
||||||
INSERT INTO guild_space (guild_id, space_id) VALUES
|
INSERT INTO guild_space (guild_id, space_id, privacy_level) VALUES
|
||||||
('112760669178241024', '!jjWAGMeQdNrVZSSfvz:cadence.moe');
|
('112760669178241024', '!jjWAGMeQdNrVZSSfvz:cadence.moe', 0);
|
||||||
|
|
||||||
INSERT INTO channel_room (channel_id, room_id, name, nick, thread_parent, custom_avatar) VALUES
|
INSERT INTO channel_room (channel_id, room_id, name, nick, thread_parent, custom_avatar) VALUES
|
||||||
('112760669178241024', '!kLRqKKUQXcibIMtOpl:cadence.moe', 'heave', 'main', NULL, NULL),
|
('112760669178241024', '!kLRqKKUQXcibIMtOpl:cadence.moe', 'heave', 'main', NULL, NULL),
|
||||||
|
|
Loading…
Reference in a new issue