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-05-04 20:25:00 +00:00
const passthrough = require ( "../../passthrough" )
2023-08-23 00:31:31 +00:00
const { discord , sync , db } = 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
/ * *
* @ 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-04 21:59:34 +00:00
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-03 05:13:04 +00:00
/** Efficiently update space name, space avatar, and child room avatars. */
2023-08-22 05:11:07 +00:00
async function syncSpace ( guildID ) {
/** @ts-ignore @type {DiscordTypes.APIGuild} */
const guild = discord . guilds . get ( guildID )
assert . ok ( guild )
2023-08-23 00:31:31 +00:00
/** @type {string?} */
const spaceID = db . prepare ( "SELECT space_id from guild_space WHERE guild_id = ?" ) . pluck ( ) . get ( guildID )
2023-08-22 05:11:07 +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-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-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
const roomsWithCustomAvatars = db . prepare ( "SELECT room_id FROM channel_room WHERE custom_avatar IS NOT NULL" ) . pluck ( ) . all ( )
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-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 )
/** @type {string?} */
const spaceID = db . prepare ( "SELECT space_id from guild_space WHERE guild_id = ?" ) . pluck ( ) . get ( guildID )
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 ) {
const channelID = db . prepare ( "SELECT channel_id FROM channel_room WHERE room_id = ?" ) . pluck ( ) . get ( roomID )
if ( ! channelID ) continue
await createRoom . syncRoom ( channelID )
}
return spaceID
}
2023-05-04 20:25:00 +00:00
module . exports . createSpace = createSpace
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