2023-05-04 20:25:00 +00:00
// @ts-check
2023-08-25 05:23:51 +00:00
const assert = require ( "assert" ) . strict
2023-08-22 05:11:07 +00:00
const DiscordTypes = require ( "discord-api-types/v10" )
2023-09-12 11:08:33 +00:00
const reg = require ( "../../matrix/read-registration" )
2023-08-22 05:11:07 +00:00
2023-05-04 20:25:00 +00:00
const passthrough = require ( "../../passthrough" )
2023-09-18 10:51:59 +00:00
const { discord , sync , db , select } = passthrough
2023-05-08 11:37:51 +00:00
/** @type {import("../../matrix/api")} */
const api = sync . require ( "../../matrix/api" )
2023-08-22 05:11:07 +00:00
/** @type {import("../../matrix/file")} */
const file = sync . require ( "../../matrix/file" )
/** @type {import("./create-room")} */
const createRoom = sync . require ( "./create-room" )
2023-08-23 00:31:31 +00:00
/** @type {import("../../matrix/kstate")} */
const ks = sync . require ( "../../matrix/kstate" )
2023-05-04 20:25:00 +00:00
2023-09-12 11:15:55 +00:00
/** @type {Map<string, Promise<string>>} guild ID -> Promise<space ID> */
const inflightSpaceCreate = new Map ( )
2023-05-04 20:25:00 +00:00
/ * *
* @ param { import ( "discord-api-types/v10" ) . RESTGetAPIGuildResult } guild
2023-08-22 05:11:07 +00:00
* @ param { any } kstate
2023-05-04 20:25:00 +00:00
* /
2023-08-22 05:11:07 +00:00
async function createSpace ( guild , kstate ) {
const name = kstate [ "m.room.name/" ] . name
const topic = kstate [ "m.room.topic/" ] ? . topic || undefined
assert ( name )
2023-08-23 05:08:20 +00:00
const roomID = await createRoom . postApplyPowerLevels ( kstate , async kstate => {
return api . createRoom ( {
name ,
preset : "private_chat" , // cannot join space unless invited
visibility : "private" ,
power _level _content _override : {
events _default : 100 , // space can only be managed by bridge
invite : 0 // any existing member can invite others
} ,
2023-09-12 11:08:33 +00:00
invite : reg . ooye . invite ,
2023-08-23 05:08:20 +00:00
topic ,
creation _content : {
type : "m.space"
} ,
initial _state : ks . kstateToState ( kstate )
} )
2023-05-04 20:25:00 +00:00
} )
2023-05-08 12:58:46 +00:00
db . prepare ( "INSERT INTO guild_space (guild_id, space_id) VALUES (?, ?)" ) . run ( guild . id , roomID )
return roomID
2023-05-04 20:25:00 +00:00
}
2023-08-22 05:11:07 +00:00
/ * *
* @ param { DiscordTypes . APIGuild } guild ]
* /
async function guildToKState ( guild ) {
const avatarEventContent = { }
if ( guild . icon ) {
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
}
let history _visibility = "invited"
if ( guild [ "thread_metadata" ] ) history _visibility = "world_readable"
const guildKState = {
"m.room.name/" : { name : guild . name } ,
"m.room.avatar/" : avatarEventContent ,
"m.room.guest_access/" : { guest _access : "can_join" } , // guests can join space if other conditions are met
2023-08-23 00:31:31 +00:00
"m.room.history_visibility/" : { history _visibility : "invited" } // any events sent after user was invited are visible
2023-08-22 05:11:07 +00:00
}
return guildKState
}
2023-09-12 11:15:55 +00:00
/ * *
* @ param { string } guildID
* @ param { boolean } shouldActuallySync false if just need to ensure nspace exists ( which is a quick database check ) ,
* true if also want to efficiently sync space name , space avatar , and child room avatars
* @ returns { Promise < string > } room ID
* /
async function _syncSpace ( guildID , shouldActuallySync ) {
2023-08-22 05:11:07 +00:00
/** @ts-ignore @type {DiscordTypes.APIGuild} */
const guild = discord . guilds . get ( guildID )
assert . ok ( guild )
2023-09-12 11:15:55 +00:00
if ( inflightSpaceCreate . has ( guildID ) ) {
await inflightSpaceCreate . get ( guildID ) // just waiting, and then doing a new db query afterwards, is the simplest way of doing it
}
2023-09-18 10:51:59 +00:00
const spaceID = select ( "guild_space" , "space_id" , "WHERE guild_id = ?" ) . pluck ( ) . get ( guildID )
2023-08-22 05:11:07 +00:00
2023-09-03 13:38:30 +00:00
if ( ! spaceID ) {
2023-09-12 11:15:55 +00:00
const creation = ( async ( ) => {
const guildKState = await guildToKState ( guild )
const spaceID = await createSpace ( guild , guildKState )
inflightSpaceCreate . delete ( guildID )
return spaceID
} ) ( )
inflightSpaceCreate . set ( guildID , creation )
return creation // Naturally, the newly created space is already up to date, so we can always skip syncing here.
}
if ( ! shouldActuallySync ) {
return spaceID // only need to ensure space exists, and it does. return the space ID
2023-09-03 13:38:30 +00:00
}
2023-08-22 05:11:07 +00:00
2023-08-23 00:31:31 +00:00
console . log ( ` [space sync] to matrix: ${ guild . name } ` )
2023-08-22 05:11:07 +00:00
2023-09-12 11:15:55 +00:00
const guildKState = await guildToKState ( guild ) // calling this in both branches because we don't want to calculate this if not syncing
2023-08-25 05:23:51 +00:00
// sync guild state to space
2023-08-23 00:31:31 +00:00
const spaceKState = await createRoom . roomToKState ( spaceID )
const spaceDiff = ks . diffKState ( spaceKState , guildKState )
await createRoom . applyKStateDiffToRoom ( spaceID , spaceDiff )
2023-08-22 05:11:07 +00:00
2023-08-25 05:23:51 +00:00
// guild icon was changed, so room avatars need to be updated as well as the space ones
// doing it this way rather than calling syncRoom for great efficiency gains
const newAvatarState = spaceDiff [ "m.room.avatar/" ]
if ( guild . icon && newAvatarState ? . url ) {
// don't try to update rooms with custom avatars though
2023-09-18 10:51:59 +00:00
const roomsWithCustomAvatars = select ( "channel_room" , "room_id" , "WHERE custom_avatar IS NOT NULL" ) . pluck ( ) . all ( )
2023-08-25 05:23:51 +00:00
const childRooms = ks . kstateToState ( spaceKState ) . filter ( ( { type , state _key , content } ) => {
2023-08-25 05:35:34 +00:00
return type === "m.space.child" && "via" in content && ! roomsWithCustomAvatars . includes ( state _key )
2023-08-25 05:23:51 +00:00
} ) . map ( ( { state _key } ) => state _key )
for ( const roomID of childRooms ) {
const avatarEventContent = await api . getStateEvent ( roomID , "m.room.avatar" , "" )
if ( avatarEventContent . url !== newAvatarState . url ) {
await api . sendState ( roomID , "m.room.avatar" , "" , newAvatarState )
}
}
}
2023-08-23 00:31:31 +00:00
return spaceID
}
2023-08-22 05:11:07 +00:00
2023-09-12 11:15:55 +00:00
/** Ensures the space exists. If it doesn't, creates the space with an accurate initial state. */
function ensureSpace ( guildID ) {
return _syncSpace ( guildID , false )
}
/** Actually syncs. Efficiently updates the space name, space avatar, and child room avatars. */
function syncSpace ( guildID ) {
return _syncSpace ( guildID , true )
}
2023-09-03 05:13:04 +00:00
/ * *
* Inefficiently force the space and its existing child rooms to be fully updated .
* Should not need to be called as part of the bridge ' s normal operation .
* /
async function syncSpaceFully ( guildID ) {
/** @ts-ignore @type {DiscordTypes.APIGuild} */
const guild = discord . guilds . get ( guildID )
assert . ok ( guild )
2023-09-18 10:51:59 +00:00
const spaceID = select ( "guild_space" , "space_id" , "WHERE guild_id = ?" ) . pluck ( ) . get ( guildID )
2023-09-03 05:13:04 +00:00
const guildKState = await guildToKState ( guild )
2023-09-03 13:38:30 +00:00
if ( ! spaceID ) {
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.
}
2023-09-03 05:13:04 +00:00
console . log ( ` [space sync] to matrix: ${ guild . name } ` )
// sync guild state to space
const spaceKState = await createRoom . roomToKState ( spaceID )
const spaceDiff = ks . diffKState ( spaceKState , guildKState )
await createRoom . applyKStateDiffToRoom ( spaceID , spaceDiff )
const childRooms = ks . kstateToState ( spaceKState ) . filter ( ( { type , content } ) => {
return type === "m.space.child" && "via" in content
} ) . map ( ( { state _key } ) => state _key )
for ( const roomID of childRooms ) {
2023-09-18 10:51:59 +00:00
const channelID = select ( "channel_room" , "channel_id" , "WHERE room_id = ?" ) . pluck ( ) . get ( roomID )
2023-09-03 05:13:04 +00:00
if ( ! channelID ) continue
2023-09-07 11:20:48 +00:00
if ( discord . channels . has ( channelID ) ) {
await createRoom . syncRoom ( channelID )
} else {
await createRoom . unbridgeDeletedChannel ( channelID , guildID )
}
2023-09-03 05:13:04 +00:00
}
return spaceID
}
2023-05-04 20:25:00 +00:00
module . exports . createSpace = createSpace
2023-09-12 11:15:55 +00:00
module . exports . ensureSpace = ensureSpace
2023-08-23 00:31:31 +00:00
module . exports . syncSpace = syncSpace
2023-09-03 05:13:04 +00:00
module . exports . syncSpaceFully = syncSpaceFully
2023-08-23 00:31:31 +00:00
module . exports . guildToKState = guildToKState