2023-04-30 12:57:30 +00:00
// @ts-check
2023-05-04 20:25:00 +00:00
const assert = require ( "assert" ) . strict
const DiscordTypes = require ( "discord-api-types/v10" )
const passthrough = require ( "../../passthrough" )
const { discord , sync , db } = passthrough
/** @type {import("../../matrix/file")} */
const file = sync . require ( "../../matrix/file" )
2023-05-08 11:37:51 +00:00
/** @type {import("../../matrix/api")} */
const api = sync . require ( "../../matrix/api" )
2023-05-10 05:40:31 +00:00
/** @type {import("../../matrix/kstate")} */
const ks = sync . require ( "../../matrix/kstate" )
2023-05-05 13:25:15 +00:00
/ * *
* @ param { string } roomID
* /
async function roomToKState ( roomID ) {
2023-05-08 11:37:51 +00:00
const root = await api . getAllState ( roomID )
2023-05-10 05:40:31 +00:00
return ks . stateToKState ( root )
2023-05-05 13:25:15 +00:00
}
/ * *
* @ params { string } roomID
* @ params { any } kstate
* /
function applyKStateDiffToRoom ( roomID , kstate ) {
2023-05-10 05:40:31 +00:00
const events = ks . kstateToState ( kstate )
2023-05-05 13:25:15 +00:00
return Promise . all ( events . map ( ( { type , state _key , content } ) =>
2023-05-08 11:37:51 +00:00
api . sendState ( roomID , type , state _key , content )
2023-05-05 13:25:15 +00:00
) )
}
2023-05-05 05:29:08 +00:00
2023-05-04 20:25:00 +00:00
/ * *
2023-07-04 05:35:29 +00:00
* @ param { { id : string , name : string , topic ? : string ? } } channel
* @ param { { id : string } } guild
* @ param { string ? } customName
2023-05-04 20:25:00 +00:00
* /
2023-07-04 05:35:29 +00:00
function convertNameAndTopic ( channel , guild , customName ) {
2023-07-05 00:04:28 +00:00
const convertedName = customName || channel . name ;
const maybeTopicWithPipe = channel . topic ? ` | ${ channel . topic } ` : '' ;
const maybeTopicWithNewlines = channel . topic ? ` ${ channel . topic } \n \n ` : '' ;
const channelIDPart = ` Channel ID: ${ channel . id } ` ;
const guildIDPart = ` Guild ID: ${ guild . id } ` ;
2023-06-28 12:06:56 +00:00
2023-07-05 00:04:28 +00:00
const convertedTopic = customName
? ` # ${ channel . name } ${ maybeTopicWithPipe } \n \n ${ channelIDPart } \n ${ guildIDPart } `
: ` ${ maybeTopicWithNewlines } ${ channelIDPart } \n ${ guildIDPart } ` ;
return [ convertedName , convertedTopic ] ;
2023-07-04 05:35:29 +00:00
}
/ * *
* @ param { DiscordTypes . APIGuildTextChannel } channel
* @ param { DiscordTypes . APIGuild } guild
* /
async function channelToKState ( channel , guild ) {
const spaceID = db . prepare ( "SELECT space_id FROM guild_space WHERE guild_id = ?" ) . pluck ( ) . get ( guild . id )
assert . ok ( typeof spaceID === "string" )
const customName = db . prepare ( "SELECT nick FROM channel_room WHERE channel_id = ?" ) . pluck ( ) . get ( channel . id )
const [ convertedName , convertedTopic ] = convertNameAndTopic ( channel , guild , customName )
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
}
2023-05-05 13:25:15 +00:00
const channelKState = {
2023-06-28 12:06:56 +00:00
"m.room.name/" : { name : convertedName } ,
"m.room.topic/" : { topic : convertedTopic } ,
2023-05-05 05:29:08 +00:00
"m.room.avatar/" : avatarEventContent ,
"m.room.guest_access/" : { guest _access : "can_join" } ,
"m.room.history_visibility/" : { history _visibility : "invited" } ,
2023-05-05 13:25:15 +00:00
[ ` m.space.parent/ ${ spaceID } ` ] : {
via : [ "cadence.moe" ] , // TODO: put the proper server here
2023-05-05 05:29:08 +00:00
canonical : true
} ,
"m.room.join_rules/" : {
join _rule : "restricted" ,
allow : [ {
2023-06-28 11:38:58 +00:00
type : "m.room_membership" ,
2023-05-05 05:29:08 +00:00
room _id : spaceID
} ]
}
2023-04-30 12:57:30 +00:00
}
2023-05-04 20:25:00 +00:00
2023-05-05 13:25:15 +00:00
return { spaceID , channelKState }
2023-05-05 05:29:08 +00:00
}
/ * *
2023-05-08 20:03:57 +00:00
* Create a bridge room , store the relationship in the database , and add it to the guild ' s space .
2023-06-28 12:06:56 +00:00
* @ param { DiscordTypes . APIGuildTextChannel } channel
2023-05-05 05:29:08 +00:00
* @ param guild
* @ param { string } spaceID
* @ param { any } kstate
2023-05-08 20:03:57 +00:00
* @ returns { Promise < string > } room ID
2023-05-05 05:29:08 +00:00
* /
async function createRoom ( channel , guild , spaceID , kstate ) {
2023-05-08 20:03:57 +00:00
const roomID = await api . createRoom ( {
2023-05-04 20:25:00 +00:00
name : channel . name ,
topic : channel . topic || undefined ,
preset : "private_chat" ,
visibility : "private" ,
invite : [ "@cadence:cadence.moe" ] , // TODO
2023-05-10 05:40:31 +00:00
initial _state : ks . kstateToState ( kstate )
2023-05-04 20:25:00 +00:00
} )
2023-05-08 20:03:57 +00:00
db . prepare ( "INSERT INTO channel_room (channel_id, room_id) VALUES (?, ?)" ) . run ( channel . id , roomID )
2023-05-04 20:25:00 +00:00
// Put the newly created child into the space
2023-05-08 20:03:57 +00:00
await api . sendState ( spaceID , "m.space.child" , roomID , { // TODO: should I deduplicate with the equivalent code from syncRoom?
2023-05-04 20:25:00 +00:00
via : [ "cadence.moe" ] // TODO: use the proper server
} )
2023-05-08 20:03:57 +00:00
return roomID
2023-05-04 20:25:00 +00:00
}
2023-05-05 05:29:08 +00:00
/ * *
2023-06-28 12:06:56 +00:00
* @ param { DiscordTypes . APIGuildChannel } channel
2023-05-05 05:29:08 +00:00
* /
2023-05-05 13:25:15 +00:00
function channelToGuild ( channel ) {
2023-05-05 05:29:08 +00:00
const guildID = channel . guild _id
assert ( guildID )
const guild = discord . guilds . get ( guildID )
assert ( guild )
2023-05-05 13:25:15 +00:00
return guild
}
2023-05-08 20:03:57 +00:00
/ *
Ensure flow :
1. Get IDs
2. Does room exist ? If so great !
( it doesn ' t , so it needs to be created )
3. Get kstate for channel
4. Create room , return new ID
New combined flow with ensure / sync :
1. Get IDs
2. Does room exist ?
2.5 : If room does exist AND don ' t need to sync : return here
3. Get kstate for channel
4. Create room with kstate if room doesn ' t exist
5. Get and update room state with kstate if room does exist
* /
2023-05-05 13:25:15 +00:00
/ * *
* @ param { string } channelID
2023-05-08 20:03:57 +00:00
* @ param { boolean } shouldActuallySync false if just need to ensure room exists ( which is a quick database check ) , true if also want to sync room data when it does exist ( slow )
* @ returns { Promise < string > } room ID
2023-05-05 13:25:15 +00:00
* /
2023-05-08 20:03:57 +00:00
async function _syncRoom ( channelID , shouldActuallySync ) {
2023-06-28 12:06:56 +00:00
/** @ts-ignore @type {DiscordTypes.APIGuildChannel} */
2023-05-05 13:25:15 +00:00
const channel = discord . channels . get ( channelID )
assert . ok ( channel )
const guild = channelToGuild ( channel )
2023-05-05 05:29:08 +00:00
/** @type {string?} */
const existing = db . prepare ( "SELECT room_id from channel_room WHERE channel_id = ?" ) . pluck ( ) . get ( channel . id )
if ( ! existing ) {
2023-05-08 20:03:57 +00:00
const { spaceID , channelKState } = await channelToKState ( channel , guild )
2023-05-05 13:25:15 +00:00
return createRoom ( channel , guild , spaceID , channelKState )
} else {
2023-05-08 20:03:57 +00:00
if ( ! shouldActuallySync ) {
return existing // only need to ensure room exists, and it does. return the room ID
}
2023-05-10 10:15:20 +00:00
console . log ( ` [room sync] to matrix: ${ channel . name } ` )
2023-05-08 20:03:57 +00:00
const { spaceID , channelKState } = await channelToKState ( channel , guild )
2023-05-05 13:25:15 +00:00
// sync channel state to room
const roomKState = await roomToKState ( existing )
2023-05-10 05:40:31 +00:00
const roomDiff = ks . diffKState ( roomKState , channelKState )
2023-05-05 13:25:15 +00:00
const roomApply = applyKStateDiffToRoom ( existing , roomDiff )
// sync room as space member
const spaceKState = await roomToKState ( spaceID )
2023-05-10 05:40:31 +00:00
const spaceDiff = ks . diffKState ( spaceKState , {
2023-05-05 13:25:15 +00:00
[ ` m.space.child/ ${ existing } ` ] : {
via : [ "cadence.moe" ] // TODO: use the proper server
}
} )
const spaceApply = applyKStateDiffToRoom ( spaceID , spaceDiff )
2023-05-08 20:03:57 +00:00
await Promise . all ( [ roomApply , spaceApply ] )
return existing
2023-05-05 05:29:08 +00:00
}
}
2023-05-08 20:03:57 +00:00
function ensureRoom ( channelID ) {
return _syncRoom ( channelID , false )
}
function syncRoom ( channelID ) {
return _syncRoom ( channelID , true )
}
2023-05-04 20:25:00 +00:00
async function createAllForGuild ( guildID ) {
const channelIDs = discord . guildChannelMap . get ( guildID )
assert . ok ( channelIDs )
for ( const channelID of channelIDs ) {
2023-06-28 11:38:58 +00:00
if ( discord . channels . get ( channelID ) ? . type === DiscordTypes . ChannelType . GuildText ) { // TODO: guild sync thread channels and such. maybe make a helper function to check if a given channel is syncable?
await syncRoom ( channelID ) . then ( r => console . log ( ` synced ${ channelID } : ` , r ) )
}
2023-05-04 20:25:00 +00:00
}
}
module . exports . createRoom = createRoom
2023-05-08 20:03:57 +00:00
module . exports . ensureRoom = ensureRoom
2023-05-05 13:25:15 +00:00
module . exports . syncRoom = syncRoom
2023-05-04 20:25:00 +00:00
module . exports . createAllForGuild = createAllForGuild
2023-05-05 13:25:15 +00:00
module . exports . channelToKState = channelToKState
2023-07-04 05:35:29 +00:00
module . exports . _convertNameAndTopic = convertNameAndTopic