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
}
/ * *
2023-08-19 06:39:23 +00:00
* @ param { string } roomID
* @ param { any } kstate
2023-05-05 13:25:15 +00:00
* /
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
}
/ * *
2023-08-19 06:39:23 +00:00
* @ param { DiscordTypes . APIGuildTextChannel | DiscordTypes . APIThreadChannel } channel
2023-07-04 05:35:29 +00:00
* @ 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-08-19 06:39:23 +00:00
const [ convertedName , convertedTopic ] = convertNameAndTopic ( channel , guild , null )
2023-05-08 20:03:57 +00:00
const roomID = await api . createRoom ( {
2023-08-19 06:39:23 +00:00
name : convertedName ,
topic : convertedTopic ,
2023-05-04 20:25:00 +00:00
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-08-19 06:39:23 +00:00
let threadParent = null
if ( channel . type === DiscordTypes . ChannelType . PublicThread ) {
/** @type {DiscordTypes.APIThreadChannel} */ // @ts-ignore
const thread = channel
threadParent = thread . parent _id
}
db . prepare ( "INSERT INTO channel_room (channel_id, room_id, name, nick, thread_parent) VALUES (?, ?, ?, NULL, ?)" ) . run ( channel . id , roomID , channel . name , threadParent )
2023-05-04 20:25:00 +00:00
// Put the newly created child into the space
2023-08-19 06:39:23 +00:00
_syncSpaceMember ( channel , spaceID , roomID )
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
2023-08-19 06:39:23 +00:00
/** @type {{room_id: string, thread_parent: string?}} */
const existing = db . prepare ( "SELECT room_id, thread_parent from channel_room WHERE channel_id = ?" ) . get ( channelID )
2023-05-05 05:29:08 +00:00
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 ) {
2023-08-19 06:39:23 +00:00
return existing . room _id // only need to ensure room exists, and it does. return the room ID
2023-05-08 20:03:57 +00:00
}
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
2023-08-19 06:39:23 +00:00
const roomKState = await roomToKState ( existing . room _id )
2023-05-10 05:40:31 +00:00
const roomDiff = ks . diffKState ( roomKState , channelKState )
2023-08-19 06:39:23 +00:00
const roomApply = applyKStateDiffToRoom ( existing . room _id , roomDiff )
2023-05-05 13:25:15 +00:00
// sync room as space member
2023-08-19 06:39:23 +00:00
const spaceApply = _syncSpaceMember ( channel , spaceID , existing . room _id )
2023-05-08 20:03:57 +00:00
await Promise . all ( [ roomApply , spaceApply ] )
2023-08-19 06:39:23 +00:00
return existing . room _id
2023-05-05 05:29:08 +00:00
}
}
2023-08-19 10:54:23 +00:00
async function _unbridgeRoom ( channelID ) {
/** @ts-ignore @type {DiscordTypes.APIGuildChannel} */
const channel = discord . channels . get ( channelID )
assert . ok ( channel )
const roomID = db . prepare ( "SELECT room_id from channel_room WHERE channel_id = ?" ) . pluck ( ) . get ( channelID )
assert . ok ( roomID )
const spaceID = db . prepare ( "SELECT space_id FROM guild_space WHERE guild_id = ?" ) . pluck ( ) . get ( channel . guild _id )
assert . ok ( spaceID )
// remove room from being a space member
await api . sendState ( spaceID , "m.space.child" , roomID , { } )
// send a notification in the room
await api . sendEvent ( roomID , "m.room.message" , {
msgtype : "m.notice" ,
body : "⚠️ This room was removed from the bridge."
} )
// leave room
await api . leaveRoom ( roomID )
// delete room from database
const { changes } = db . prepare ( "DELETE FROM channel_room WHERE room_id = ? AND channel_id = ?" ) . run ( roomID , channelID )
assert . equal ( changes , 1 )
}
2023-08-19 06:39:23 +00:00
/ * *
* @ param { DiscordTypes . APIGuildTextChannel } channel
* @ param { string } spaceID
* @ param { string } roomID
* @ returns { Promise < string [ ] > }
* /
async function _syncSpaceMember ( channel , spaceID , roomID ) {
const spaceKState = await roomToKState ( spaceID )
let spaceEventContent = { }
if (
channel . type !== DiscordTypes . ChannelType . PrivateThread // private threads do not belong in the space (don't offer people something they can't join)
|| channel [ "thread_metadata" ] ? . archived // archived threads do not belong in the space (don't offer people conversations that are no longer relevant)
) {
spaceEventContent = {
via : [ "cadence.moe" ] // TODO: use the proper server
}
}
const spaceDiff = ks . diffKState ( spaceKState , {
[ ` m.space.child/ ${ roomID } ` ] : spaceEventContent
} )
return applyKStateDiffToRoom ( spaceID , spaceDiff )
}
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
2023-08-19 10:54:23 +00:00
module . exports . _unbridgeRoom = _unbridgeRoom