From c7313035a45396a9b0e9fdb1a73c9c3237714ab7 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Mon, 1 Dec 2025 16:48:11 +1300 Subject: [PATCH] Update global profiles for sims --- src/d2m/actions/register-pk-user.js | 11 +---- src/d2m/actions/register-user.js | 59 +++++++++++++++++++++--- src/d2m/actions/register-webhook-user.js | 11 +---- src/matrix/api.js | 35 +++++++++++--- 4 files changed, 85 insertions(+), 31 deletions(-) diff --git a/src/d2m/actions/register-pk-user.js b/src/d2m/actions/register-pk-user.js index e17f061..bf573e7 100644 --- a/src/d2m/actions/register-pk-user.js +++ b/src/d2m/actions/register-pk-user.js @@ -151,16 +151,9 @@ async function syncUser(messageID, author, roomID, shouldActuallySync) { const mxid = await ensureSimJoined(pkMessage, roomID) if (shouldActuallySync) { - // Build current profile data + // Build current profile data and sync if the hash has changed const content = await memberToStateContent(pkMessage, author) - const currentHash = registerUser._hashProfileContent(content, 0) - const existingHash = select("sim_member", "hashed_profile_content", {room_id: roomID, mxid}).safeIntegers().pluck().get() - - // Only do the actual sync if the hash has changed since we last looked - if (existingHash !== currentHash) { - await api.sendState(roomID, "m.room.member", mxid, content, mxid) - db.prepare("UPDATE sim_member SET hashed_profile_content = ? WHERE room_id = ? AND mxid = ?").run(currentHash, roomID, mxid) - } + await registerUser._sendSyncUser(roomID, mxid, content, null) } return mxid diff --git a/src/d2m/actions/register-user.js b/src/d2m/actions/register-user.js index 674853a..b58ad66 100644 --- a/src/d2m/actions/register-user.js +++ b/src/d2m/actions/register-user.js @@ -23,6 +23,8 @@ let hasher = null // @ts-ignore require("xxhash-wasm")().then(h => hasher = h) +const supportsMsc4069 = api.versions().then(v => !!v?.unstable_features?.["org.matrix.msc4069"]).catch(() => false) + /** * A sim is an account that is being simulated by the bridge to copy events from the other side. * @param {DiscordTypes.APIUser} user @@ -98,6 +100,23 @@ async function ensureSimJoined(user, roomID) { return mxid } +/** + * @param {DiscordTypes.APIUser} user + */ +async function userToGlobalProfile(user) { + const globalProfile = {} + + globalProfile.displayname = user.username + if (user.global_name) globalProfile.displayname = user.global_name + + if (user.avatar) { + const avatarPath = file.userAvatar(user) // the user avatar only + globalProfile.avatar_url = await file.uploadDiscordFileToMxc(avatarPath) + } + + return globalProfile +} + /** * @param {DiscordTypes.APIUser} user * @param {Omit | undefined} member @@ -201,21 +220,45 @@ async function syncUser(user, member, channel, guild, roomID) { const mxid = await ensureSimJoined(user, roomID) const content = await memberToStateContent(user, member, guild.id) const powerLevel = memberToPowerLevel(user, member, guild, channel) - const currentHash = _hashProfileContent(content, powerLevel) + await _sendSyncUser(roomID, mxid, content, powerLevel, { + // do not overwrite pre-existing data if we already have data and `member` is not accessible, because this would replace good data with bad data + allowOverwrite: !!member, + globalProfile: await userToGlobalProfile(user) + }) + return mxid +} + +/** + * @param {string} roomID + * @param {string} mxid + * @param {{displayname: string, avatar_url?: string}} content + * @param {number | null} powerLevel + * @param {{allowOverwrite?: boolean, globalProfile?: {displayname: string, avatar_url?: string}}} [options] + */ +async function _sendSyncUser(roomID, mxid, content, powerLevel, options) { + const currentHash = _hashProfileContent(content, powerLevel ?? 0) const existingHash = select("sim_member", "hashed_profile_content", {room_id: roomID, mxid}).safeIntegers().pluck().get() // only do the actual sync if the hash has changed since we last looked const hashHasChanged = existingHash !== currentHash - // however, do not overwrite pre-existing data if we already have data and `member` is not accessible, because this would replace good data with bad data - const wouldOverwritePreExisting = existingHash && !member - if (hashHasChanged && !wouldOverwritePreExisting) { + // always okay to add new data. for overwriting, restrict based on options.allowOverwrite, if present + const overwriteOkay = !existingHash || (options?.allowOverwrite ?? true) + if (hashHasChanged && overwriteOkay) { + const actions = [] // Update room member state - await api.sendState(roomID, "m.room.member", mxid, content, mxid) + actions.push(api.sendState(roomID, "m.room.member", mxid, content, mxid)) // Update power levels - await api.setUserPower(roomID, mxid, powerLevel) + if (powerLevel != null) { + actions.push(api.setUserPower(roomID, mxid, powerLevel)) + } + // Update global profile (if supported by server) + if (await supportsMsc4069) { + actions.push(api.profileSetDisplayname(mxid, options?.globalProfile?.displayname || content.displayname, true)) + actions.push(api.profileSetAvatarUrl(mxid, options?.globalProfile?.avatar_url || content.avatar_url, true)) + } + await Promise.all(actions) // Update cached hash db.prepare("UPDATE sim_member SET hashed_profile_content = ? WHERE room_id = ? AND mxid = ?").run(currentHash, roomID, mxid) } - return mxid } /** @@ -254,5 +297,7 @@ module.exports._hashProfileContent = _hashProfileContent module.exports.ensureSim = ensureSim module.exports.ensureSimJoined = ensureSimJoined module.exports.syncUser = syncUser +module.exports._sendSyncUser = _sendSyncUser module.exports.syncAllUsersInRoom = syncAllUsersInRoom module.exports._memberToPowerLevel = memberToPowerLevel +module.exports.supportsMsc4069 = supportsMsc4069 diff --git a/src/d2m/actions/register-webhook-user.js b/src/d2m/actions/register-webhook-user.js index 869d7d8..309a120 100644 --- a/src/d2m/actions/register-webhook-user.js +++ b/src/d2m/actions/register-webhook-user.js @@ -128,16 +128,9 @@ async function syncUser(author, roomID, shouldActuallySync) { const mxid = await ensureSimJoined(fakeUserID, author, roomID) if (shouldActuallySync) { - // Build current profile data + // Build current profile data and sync if the hash has changed const content = await authorToStateContent(author) - const currentHash = registerUser._hashProfileContent(content, 0) - const existingHash = select("sim_member", "hashed_profile_content", {room_id: roomID, mxid}).safeIntegers().pluck().get() - - // Only do the actual sync if the hash has changed since we last looked - if (existingHash !== currentHash) { - await api.sendState(roomID, "m.room.member", mxid, content, mxid) - db.prepare("UPDATE sim_member SET hashed_profile_content = ? WHERE room_id = ? AND mxid = ?").run(currentHash, roomID, mxid) - } + await registerUser._sendSyncUser(roomID, mxid, content, null) } return mxid diff --git a/src/matrix/api.js b/src/matrix/api.js index d0892ff..d746b25 100644 --- a/src/matrix/api.js +++ b/src/matrix/api.js @@ -317,16 +317,34 @@ async function sendTyping(roomID, isTyping, mxid, duration) { }) } -async function profileSetDisplayname(mxid, displayname) { - await mreq.mreq("PUT", path(`/client/v3/profile/${mxid}/displayname`, mxid), { +/** + * @param {string} mxid + * @param {string} displayname + * @param {boolean} [inhibitPropagate] + */ +async function profileSetDisplayname(mxid, displayname, inhibitPropagate) { + const params = {} + if (inhibitPropagate) params["org.matrix.msc4069.propagate"] = false + await mreq.mreq("PUT", path(`/client/v3/profile/${mxid}/displayname`, mxid, params), { displayname }) } -async function profileSetAvatarUrl(mxid, avatar_url) { - await mreq.mreq("PUT", path(`/client/v3/profile/${mxid}/avatar_url`, mxid), { - avatar_url - }) +/** + * @param {string} mxid + * @param {string} avatar_url + * @param {boolean} [inhibitPropagate] + */ +async function profileSetAvatarUrl(mxid, avatar_url, inhibitPropagate) { + const params = {} + if (inhibitPropagate) params["org.matrix.msc4069.propagate"] = false + if (avatar_url) { + await mreq.mreq("PUT", path(`/client/v3/profile/${mxid}/avatar_url`, mxid, params), { + avatar_url + }) + } else { + await mreq.mreq("DELETE", path(`/client/v3/profile/${mxid}/avatar_url`, mxid, params)) + } } /** @@ -490,6 +508,10 @@ function getProfile(mxid) { return mreq.mreq("GET", `/client/v3/profile/${mxid}`) } +function versions() { + return mreq.mreq("GET", "/client/versions") +} + module.exports.path = path module.exports.register = register module.exports.createRoom = createRoom @@ -526,3 +548,4 @@ module.exports.getAccountData = getAccountData module.exports.setAccountData = setAccountData module.exports.setPresence = setPresence module.exports.getProfile = getProfile +module.exports.versions = versions