feat: add processProfileUpdateEvent S2S relay processor
This commit is contained in:
@@ -10,10 +10,10 @@ import { connectionManager } from '../ws/handler.js';
|
|||||||
import type { FederatedCallEntry, DmRoomMeta } from '../ws/handler.js';
|
import type { FederatedCallEntry, DmRoomMeta } from '../ws/handler.js';
|
||||||
import { sanitizeUser } from '../utils/sanitize.js';
|
import { sanitizeUser } from '../utils/sanitize.js';
|
||||||
import { deleteAttachmentFiles } from '../utils/fileCleanup.js';
|
import { deleteAttachmentFiles } from '../utils/fileCleanup.js';
|
||||||
import { tombstoneUser, collectDeletionBroadcastTargets } from '../utils/userDeletion.js';
|
import { tombstoneUser, collectDeletionBroadcastTargets, collectProfileBroadcastTargetIds } from '../utils/userDeletion.js';
|
||||||
import { computeFederatedId, getDmParticipants, sendCallRelay } from '../utils/federationOutbox.js';
|
import { computeFederatedId, getDmParticipants, sendCallRelay } from '../utils/federationOutbox.js';
|
||||||
import { getDmMessageWithUser } from './dm.js';
|
import { getDmMessageWithUser } from './dm.js';
|
||||||
import type { FederationRelayRequest, FederationRelayResponse, FederationRelayEvent, FederationRelayAttachment, FederationSyncRequest, FederationSyncResponse, DmMessageWithUser, FederationRelayProfileSnapshot, FederationIdentityDeleteS2SRequest } from '@backspace/shared';
|
import type { FederationRelayRequest, FederationRelayResponse, FederationRelayEvent, FederationRelayAttachment, FederationSyncRequest, FederationSyncResponse, DmMessageWithUser, FederationRelayProfileSnapshot, FederationIdentityDeleteS2SRequest, FederationProfileUpdatePayload } from '@backspace/shared';
|
||||||
|
|
||||||
/** Fields safe to expose to admin callers (everything except hmacSecret). */
|
/** Fields safe to expose to admin callers (everything except hmacSecret). */
|
||||||
interface SanitizedPeer {
|
interface SanitizedPeer {
|
||||||
@@ -1329,6 +1329,9 @@ export function processRelayEvents(
|
|||||||
case 'dm_typing_stop':
|
case 'dm_typing_stop':
|
||||||
processDmTypingStopEvent(event, sourceInstance, db, accepted, rejected);
|
processDmTypingStopEvent(event, sourceInstance, db, accepted, rejected);
|
||||||
break;
|
break;
|
||||||
|
case 'profile_update':
|
||||||
|
processProfileUpdateEvent(event, sourceInstance, db, accepted, rejected);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
rejected.push({ messageId: event.messageId, reason: 'unknown_event_type' });
|
rejected.push({ messageId: event.messageId, reason: 'unknown_event_type' });
|
||||||
break;
|
break;
|
||||||
@@ -3680,6 +3683,90 @@ function processDmTypingStopEvent(
|
|||||||
accepted.push(event.messageId);
|
accepted.push(event.messageId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function processProfileUpdateEvent(
|
||||||
|
event: FederationRelayEvent,
|
||||||
|
sourceInstance: string,
|
||||||
|
db: ReturnType<typeof getDb>,
|
||||||
|
accepted: string[],
|
||||||
|
rejected: Array<{ messageId: string; reason: string }>,
|
||||||
|
): void {
|
||||||
|
const payload = event.profileUpdate;
|
||||||
|
if (!payload) {
|
||||||
|
rejected.push({ messageId: event.messageId, reason: 'missing_profile_update_payload' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strict attribution: profile updates MUST originate from the home instance.
|
||||||
|
// No homeward relay exception — unlike DMs, profile updates always come from home.
|
||||||
|
const payloadDomain = extractDomain(payload.homeInstance);
|
||||||
|
const sourceDomain = extractDomain(sourceInstance);
|
||||||
|
if (payloadDomain !== sourceDomain) {
|
||||||
|
console.warn(`[federation] Attribution mismatch in profile_update: homeInstance=${payloadDomain} source=${sourceDomain}`);
|
||||||
|
rejected.push({ messageId: event.messageId, reason: 'attribution_mismatch' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look up the local replicated user by canonical identity
|
||||||
|
const localUser = db
|
||||||
|
.select()
|
||||||
|
.from(schema.users)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(schema.users.homeUserId, payload.homeUserId),
|
||||||
|
eq(schema.users.isDeleted, 0),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.get();
|
||||||
|
|
||||||
|
if (!localUser) {
|
||||||
|
// This peer has no replica of this user — silently accept
|
||||||
|
accepted.push(event.messageId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify the homeInstance domain matches (guard against homeUserId collisions)
|
||||||
|
if (localUser.homeInstance && extractDomain(localUser.homeInstance) !== payloadDomain) {
|
||||||
|
accepted.push(event.messageId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Version check: reject stale/duplicate events
|
||||||
|
const storedTs = localUser.profileUpdatedAt ?? 0;
|
||||||
|
const incomingTs = payload.profileUpdatedAt ?? 0;
|
||||||
|
if (incomingTs <= storedTs) {
|
||||||
|
accepted.push(event.messageId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Authoritative overwrite — home instance is always right
|
||||||
|
db.update(schema.users)
|
||||||
|
.set({
|
||||||
|
displayName: payload.displayName,
|
||||||
|
avatar: payload.avatar,
|
||||||
|
banner: payload.banner,
|
||||||
|
accentColor: payload.accentColor,
|
||||||
|
avatarColor: payload.avatarColor,
|
||||||
|
bio: payload.bio,
|
||||||
|
profileUpdatedAt: payload.profileUpdatedAt,
|
||||||
|
})
|
||||||
|
.where(eq(schema.users.id, localUser.id))
|
||||||
|
.run();
|
||||||
|
|
||||||
|
// Broadcast user_updated to local clients
|
||||||
|
const updatedUser = db.select().from(schema.users).where(eq(schema.users.id, localUser.id)).get();
|
||||||
|
if (updatedUser) {
|
||||||
|
const sanitized = sanitizeUser(updatedUser, false);
|
||||||
|
const targetUserIds = collectProfileBroadcastTargetIds(localUser.id);
|
||||||
|
targetUserIds.add(localUser.id); // Include self (other tabs/connections)
|
||||||
|
const userUpdatedEvent = { type: 'user_updated' as const, user: sanitized };
|
||||||
|
for (const uid of targetUserIds) {
|
||||||
|
connectionManager.sendToUser(uid, userUpdatedEvent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
accepted.push(event.messageId);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fan out a call event to all remote instances with DM members,
|
* Fan out a call event to all remote instances with DM members,
|
||||||
* optionally excluding the instance that triggered the event.
|
* optionally excluding the instance that triggered the event.
|
||||||
|
|||||||
Reference in New Issue
Block a user