Compare commits

..

No commits in common. "613a1dc0866067f597b691b71d07bb72e70dfda5" and "5c41b95919582999e8c2aa4dedf3c94bd8e80497" have entirely different histories.

10 changed files with 41 additions and 86 deletions

View file

@ -15,24 +15,6 @@ 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()
@ -87,9 +69,7 @@ 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(typeof spaceID === "string") assert.ok(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
@ -104,33 +84,27 @@ 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 = PRIVACY_ENUMS.ROOM_HISTORY_VISIBILITY[privacyLevel] let history_visibility = "shared"
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: PRIVACY_ENUMS.GUEST_ACCESS[privacyLevel]}, "m.room.guest_access/": {guest_access: "can_join"},
"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/": join_rules, "m.room.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
@ -158,7 +132,7 @@ async function channelToKState(channel, guild) {
} }
} }
return {spaceID, privacyLevel, channelKState} return {spaceID, channelKState}
} }
/** /**
@ -167,10 +141,9 @@ 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, privacyLevel) { async function createRoom(channel, guild, spaceID, kstate) {
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
@ -187,8 +160,8 @@ async function createRoom(channel, guild, spaceID, kstate, privacyLevel) {
const roomID = await api.createRoom({ const roomID = await api.createRoom({
name, name,
topic, topic,
preset: PRIVACY_ENUMS.ROOM_HISTORY_VISIBILITY[privacyLevel], // This is closest to what we want, but properties from kstate override it anyway preset: "private_chat", // This is closest to what we want, but properties from kstate override it anyway
visibility: PRIVACY_ENUMS.VISIBILITY[privacyLevel], visibility: "private", // Not shown in the room directory
invite: [], invite: [],
initial_state: ks.kstateToState(kstate) initial_state: ks.kstateToState(kstate)
}) })
@ -279,8 +252,8 @@ async function _syncRoom(channelID, shouldActuallySync) {
if (!existing) { if (!existing) {
const creation = (async () => { const creation = (async () => {
const {spaceID, privacyLevel, channelKState} = await channelToKState(channel, guild) const {spaceID, channelKState} = await channelToKState(channel, guild)
const roomID = await createRoom(channel, guild, spaceID, channelKState, privacyLevel) const roomID = await createRoom(channel, guild, spaceID, channelKState)
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
})() })()
@ -398,8 +371,6 @@ 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

View file

@ -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: 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: "private_chat", // cannot join space unless invited
visibility: createRoom.PRIVACY_ENUMS.VISIBILITY[createRoom.DEFAULT_PRIVACY_LEVEL], visibility: "private",
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,21 +51,23 @@ async function createSpace(guild, kstate) {
} }
/** /**
* @param {DiscordTypes.APIGuild} guild * @param {DiscordTypes.APIGuild} guild]
* @param {number} privacyLevel
*/ */
async function guildToKState(guild, privacyLevel) { async function guildToKState(guild) {
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: createRoom.PRIVACY_ENUMS.GUEST_ACCESS[privacyLevel]}, "m.room.guest_access/": {guest_access: "can_join"}, // guests can join space if other conditions are met
"m.room.history_visibility/": {history_visibility: createRoom.PRIVACY_ENUMS.GUEST_ACCESS[privacyLevel]} "m.room.history_visibility/": {history_visibility: "invited"} // any events sent after user was invited are visible
} }
return guildKState return guildKState
@ -84,11 +86,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 row = select("guild_space", ["space_id", "privacy_level"], {guild_id: guild.id}).get() const spaceID = select("guild_space", "space_id", {guild_id: guild.id}).pluck().get()
if (!row) { if (!spaceID) {
const creation = (async () => { const creation = (async () => {
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 guildKState = await guildToKState(guild)
const spaceID = await createSpace(guild, guildKState) const spaceID = await createSpace(guild, guildKState)
inflightSpaceCreate.delete(guild.id) inflightSpaceCreate.delete(guild.id)
return spaceID return spaceID
@ -97,15 +99,13 @@ 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, privacy_level) // calling this in both branches because we don't want to calculate this if not syncing const guildKState = await guildToKState(guild) // 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,20 +159,17 @@ async function syncSpaceFully(guildID) {
const guild = discord.guilds.get(guildID) const guild = discord.guilds.get(guildID)
assert.ok(guild) assert.ok(guild)
const row = select("guild_space", ["space_id", "privacy_level"], {guild_id: guildID}).get() const spaceID = select("guild_space", "space_id", {guild_id: guildID}).pluck().get()
if (!row) { const guildKState = await guildToKState(guild)
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)

View file

@ -40,11 +40,9 @@ async function sendMessage(message, guild) {
} }
for (const event of events) { for (const event of events) {
const eventType = event.$type const eventType = event.$type
if (event.$sender) senderMxid = event.$sender /** @type {Pick<typeof event, Exclude<keyof event, "$type">> & { $type?: string }} */
/** @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)

View file

@ -1,5 +0,0 @@
BEGIN TRANSACTION;
DELETE FROM member_cache;
COMMIT;

View file

@ -1,5 +0,0 @@
BEGIN TRANSACTION;
ALTER TABLE guild_space ADD COLUMN privacy_level TEXT NOT NULL DEFAULT 0;
COMMIT;

1
db/orm-defs.d.ts vendored
View file

@ -25,7 +25,6 @@ export type Models = {
guild_space: { guild_space: {
guild_id: string guild_id: string
space_id: string space_id: string
privacy_level: number
} }
lottie: { lottie: {

View file

@ -147,6 +147,6 @@ sync.addTemporaryListener(as, "type:m.room.member", guard("m.room.member",
*/ */
async event => { async event => {
if (event.state_key[0] !== "@") return if (event.state_key[0] !== "@") return
if (utils.eventSenderIsFromDiscord(event.state_key)) return if (utils.eventSenderIsFromDiscord(event.sender)) return
db.prepare("REPLACE INTO member_cache (room_id, mxid, displayname, avatar_url) VALUES (?, ?, ?, ?)").run(event.room_id, event.state_key, event.content.displayname || null, event.content.avatar_url || null) db.prepare("REPLACE INTO member_cache (room_id, mxid, displayname, avatar_url) VALUES (?, ?, ?, ?)").run(event.room_id, event.sender, event.content.displayname || null, event.content.avatar_url || null)
})) }))

4
package-lock.json generated
View file

@ -1,12 +1,12 @@
{ {
"name": "out-of-your-element", "name": "out-of-your-element",
"version": "1.1.1", "version": "1.1.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "out-of-your-element", "name": "out-of-your-element",
"version": "1.1.1", "version": "1.1.0",
"license": "AGPL-3.0-or-later", "license": "AGPL-3.0-or-later",
"dependencies": { "dependencies": {
"@chriscdn/promise-semaphore": "^2.0.1", "@chriscdn/promise-semaphore": "^2.0.1",

View file

@ -1,6 +1,6 @@
{ {
"name": "out-of-your-element", "name": "out-of-your-element",
"version": "1.1.1", "version": "1.1.0",
"description": "A bridge between Matrix and Discord", "description": "A bridge between Matrix and Discord",
"main": "index.js", "main": "index.js",
"repository": { "repository": {

View file

@ -1,7 +1,7 @@
BEGIN TRANSACTION; BEGIN TRANSACTION;
INSERT INTO guild_space (guild_id, space_id, privacy_level) VALUES INSERT INTO guild_space (guild_id, space_id) VALUES
('112760669178241024', '!jjWAGMeQdNrVZSSfvz:cadence.moe', 0); ('112760669178241024', '!jjWAGMeQdNrVZSSfvz:cadence.moe');
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),