2023-04-30 12:57:30 +00:00
// @ts-check
2023-05-07 20:27:42 +00:00
const assert = require ( "assert" )
2023-05-08 12:58:46 +00:00
const reg = require ( "../../matrix/read-registration" )
2023-04-30 12:57:30 +00:00
2023-05-07 20:27:42 +00:00
const passthrough = require ( "../../passthrough" )
2023-09-18 10:51:59 +00:00
const { discord , sync , db , select } = passthrough
2023-05-08 05:22:20 +00:00
/** @type {import("../../matrix/api")} */
const api = sync . require ( "../../matrix/api" )
2023-05-07 20:27:42 +00:00
/** @type {import("../../matrix/file")} */
const file = sync . require ( "../../matrix/file" )
2023-05-08 11:37:51 +00:00
/** @type {import("../converters/user-to-mxid")} */
const userToMxid = sync . require ( "../converters/user-to-mxid" )
2023-09-30 12:24:05 +00:00
/** @type {import("xxhash-wasm").XXHashAPI} */ // @ts-ignore
let hasher = null
// @ts-ignore
require ( "xxhash-wasm" ) ( ) . then ( h => hasher = h )
2023-05-07 20:27:42 +00:00
/ * *
* A sim is an account that is being simulated by the bridge to copy events from the other side .
* @ param { import ( "discord-api-types/v10" ) . APIUser } user
2023-05-08 12:58:46 +00:00
* @ returns mxid
2023-05-07 20:27:42 +00:00
* /
async function createSim ( user ) {
2023-05-08 12:58:46 +00:00
// Choose sim name
2023-05-08 11:37:51 +00:00
const simName = userToMxid . userToSimName ( user )
2023-07-13 05:11:24 +00:00
const localpart = reg . ooye . namespace _prefix + simName
2023-08-23 00:45:19 +00:00
const mxid = ` @ ${ localpart } : ${ reg . ooye . server _name } `
2023-05-08 12:58:46 +00:00
// Save chosen name in the database forever
2023-05-09 03:29:46 +00:00
// Making this database change right away so that in a concurrent registration, the 2nd registration will already have generated a different localpart because it can see this row when it generates
2023-10-04 23:32:05 +00:00
db . prepare ( "INSERT INTO sim (user_id, sim_name, localpart, mxid) VALUES (?, ?, ?, ?)" ) . run ( user . id , simName , localpart , mxid )
2023-05-08 12:58:46 +00:00
// Register matrix user with that name
2023-05-09 03:29:46 +00:00
try {
await api . register ( localpart )
} catch ( e ) {
// If user creation fails, manually undo the database change. Still isn't perfect, but should help.
2023-10-16 03:47:42 +00:00
// (I would prefer a transaction, but it's not safe to leave transactions open across event loop ticks.)
2023-10-04 23:32:05 +00:00
db . prepare ( "DELETE FROM sim WHERE user_id = ?" ) . run ( user . id )
2023-05-09 03:29:46 +00:00
throw e
}
2023-05-08 12:58:46 +00:00
return mxid
2023-05-08 05:22:20 +00:00
}
2023-05-08 12:58:46 +00:00
/ * *
* Ensure a sim is registered for the user .
* If there is already a sim , use that one . If there isn ' t one yet , register a new sim .
2023-05-09 05:13:59 +00:00
* @ param { import ( "discord-api-types/v10" ) . APIUser } user
2023-08-17 04:41:28 +00:00
* @ returns { Promise < string > } mxid
2023-05-08 12:58:46 +00:00
* /
async function ensureSim ( user ) {
let mxid = null
2023-10-05 23:31:10 +00:00
const existing = select ( "sim" , "mxid" , { user _id : user . id } ) . pluck ( ) . get ( )
2023-05-08 12:58:46 +00:00
if ( existing ) {
mxid = existing
} else {
mxid = await createSim ( user )
}
return mxid
}
/ * *
* Ensure a sim is registered for the user and is joined to the room .
2023-05-09 05:13:59 +00:00
* @ param { import ( "discord-api-types/v10" ) . APIUser } user
2023-08-16 05:03:05 +00:00
* @ param { string } roomID
2023-08-17 04:41:28 +00:00
* @ returns { Promise < string > } mxid
2023-05-08 12:58:46 +00:00
* /
async function ensureSimJoined ( user , roomID ) {
// Ensure room ID is really an ID, not an alias
assert . ok ( roomID [ 0 ] === "!" )
// Ensure user
const mxid = await ensureSim ( user )
// Ensure joined
2023-10-05 23:31:10 +00:00
const existing = select ( "sim_member" , "mxid" , { room _id : roomID , mxid } ) . pluck ( ) . get ( )
2023-05-08 12:58:46 +00:00
if ( ! existing ) {
2023-09-02 22:47:01 +00:00
try {
await api . inviteToRoom ( roomID , mxid )
await api . joinRoom ( roomID , mxid )
} catch ( e ) {
if ( e . message . includes ( "is already in the room." ) ) {
// Sweet!
} else {
throw e
}
}
2023-09-17 08:15:20 +00:00
db . prepare ( "INSERT OR IGNORE INTO sim_member (room_id, mxid) VALUES (?, ?)" ) . run ( roomID , mxid )
2023-05-08 12:58:46 +00:00
}
return mxid
}
2023-05-10 05:40:31 +00:00
/ * *
* @ param { import ( "discord-api-types/v10" ) . APIUser } user
2023-05-10 10:15:20 +00:00
* @ param { Omit < import ( "discord-api-types/v10" ) . APIGuildMember , "user" > } member
2023-05-10 05:40:31 +00:00
* /
2023-05-10 10:15:20 +00:00
async function memberToStateContent ( user , member , guildID ) {
let displayname = user . username
2023-10-13 12:00:28 +00:00
if ( user . global _name ) displayname = user . global _name
2023-08-19 06:40:37 +00:00
if ( member . nick ) displayname = member . nick
2023-05-10 10:15:20 +00:00
const content = {
displayname ,
membership : "join" ,
"moe.cadence.ooye.member" : {
} ,
"uk.half-shot.discord.member" : {
bot : ! ! user . bot ,
displayColor : user . accent _color ,
id : user . id ,
username : user . discriminator . length === 4 ? ` ${ user . username } # ${ user . discriminator } ` : ` @ ${ user . username } `
}
}
if ( member . avatar || user . avatar ) {
// const avatarPath = file.userAvatar(user) // the user avatar only
const avatarPath = file . memberAvatar ( guildID , user , member ) // the member avatar or the user avatar
content [ "moe.cadence.ooye.member" ] . avatar = avatarPath
content . avatar _url = await file . uploadDiscordFileToMxc ( avatarPath )
}
return content
}
2024-01-20 11:54:18 +00:00
function _hashProfileContent ( content ) {
2023-09-30 12:24:05 +00:00
const unsignedHash = hasher . h64 ( ` ${ content . displayname } \u 0000 ${ content . avatar _url } ` )
const signedHash = unsignedHash - 0x8000000000000000 n // shifting down to signed 64-bit range
return signedHash
2023-05-10 10:15:20 +00:00
}
/ * *
2023-08-18 04:58:46 +00:00
* Sync profile data for a sim user . This function follows the following process :
* 1. Join the sim to the room if needed
* 2. Make an object of what the new room member state content would be , including uploading the profile picture if it hasn ' t been done before
* 3. Compare against the previously known state content , which is helpfully stored in the database
2023-10-07 09:47:31 +00:00
* 4. If the state content has changed , send it to Matrix and update it in the database for next time
2023-05-10 10:15:20 +00:00
* @ param { import ( "discord-api-types/v10" ) . APIUser } user
* @ param { Omit < import ( "discord-api-types/v10" ) . APIGuildMember , "user" > } member
2023-08-18 04:58:46 +00:00
* @ returns { Promise < string > } mxid of the updated sim
2023-05-10 10:15:20 +00:00
* /
async function syncUser ( user , member , guildID , roomID ) {
const mxid = await ensureSimJoined ( user , roomID )
const content = await memberToStateContent ( user , member , guildID )
2024-01-20 11:54:18 +00:00
const currentHash = _hashProfileContent ( content )
2023-10-05 23:31:10 +00:00
const existingHash = select ( "sim_member" , "hashed_profile_content" , { room _id : roomID , mxid } ) . safeIntegers ( ) . pluck ( ) . get ( )
2023-05-10 10:15:20 +00:00
// only do the actual sync if the hash has changed since we last looked
2023-09-20 04:12:19 +00:00
if ( existingHash !== currentHash ) {
2023-05-10 10:15:20 +00:00
await api . sendState ( roomID , "m.room.member" , mxid , content , mxid )
2023-09-30 12:24:05 +00:00
db . prepare ( "UPDATE sim_member SET hashed_profile_content = ? WHERE room_id = ? AND mxid = ?" ) . run ( currentHash , roomID , mxid )
2023-05-10 10:15:20 +00:00
}
2023-08-18 04:58:46 +00:00
return mxid
2023-05-10 10:15:20 +00:00
}
async function syncAllUsersInRoom ( roomID ) {
2023-10-05 23:31:10 +00:00
const mxids = select ( "sim_member" , "mxid" , { room _id : roomID } ) . pluck ( ) . all ( )
2023-05-10 10:15:20 +00:00
2023-10-05 23:31:10 +00:00
const channelID = select ( "channel_room" , "channel_id" , { room _id : roomID } ) . pluck ( ) . get ( )
2023-05-10 10:15:20 +00:00
assert . ok ( typeof channelID === "string" )
2023-09-18 10:51:59 +00:00
2023-05-10 10:15:20 +00:00
/** @ts-ignore @type {import("discord-api-types/v10").APIGuildChannel} */
const channel = discord . channels . get ( channelID )
const guildID = channel . guild _id
assert . ok ( typeof guildID === "string" )
for ( const mxid of mxids ) {
2023-10-05 23:31:10 +00:00
const userID = select ( "sim" , "user_id" , { mxid } ) . pluck ( ) . get ( )
2023-05-10 10:15:20 +00:00
assert . ok ( typeof userID === "string" )
/** @ts-ignore @type {Required<import("discord-api-types/v10").APIGuildMember>} */
const member = await discord . snow . guild . getGuildMember ( guildID , userID )
/** @ts-ignore @type {Required<import("discord-api-types/v10").APIUser>} user */
const user = member . user
assert . ok ( user )
console . log ( ` [user sync] to matrix: ${ user . username } in ${ channel . name } ` )
await syncUser ( user , member , guildID , roomID )
2023-05-10 05:40:31 +00:00
}
}
2023-06-30 03:15:23 +00:00
module . exports . _memberToStateContent = memberToStateContent
2024-01-20 11:54:18 +00:00
module . exports . _hashProfileContent = _hashProfileContent
2023-05-08 12:58:46 +00:00
module . exports . ensureSim = ensureSim
module . exports . ensureSimJoined = ensureSimJoined
2023-05-10 10:15:20 +00:00
module . exports . syncUser = syncUser
module . exports . syncAllUsersInRoom = syncAllUsersInRoom