feat(federation): backfill snowflake-named replicated-user stubs
Heals existing legacy stubs (created when resolveOrCreateReplicatedUser used homeUserId@domain) by asking the peer for the canonical username via lookupRemoteUserByHomeId and rewriting the local row. Idempotent and collision-safe. Wired into onPeerActivated (per-origin) so future peer flaps re-attempt for stubs whose home was unreachable on a prior pass, and into a one-shot pass at startupBootstrapSync for all currently-active peers (not just first-time lastSyncedAt=0 peers).
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
import { and, eq, like } from 'drizzle-orm';
|
||||
import { getDb, schema } from '../db/index.js';
|
||||
import { lookupRemoteUserByHomeId } from './federationLookup.js';
|
||||
import { extractDomain } from '../routes/federation.js';
|
||||
|
||||
/**
|
||||
* For each replicated stub on this instance whose username still matches the
|
||||
* legacy `<homeUserId>@<domain>` pattern AND whose home_instance equals the
|
||||
* given peer's domain, ask the peer for the canonical username via
|
||||
* lookupRemoteUserByHomeId and rewrite the stub.
|
||||
*
|
||||
* Idempotent — stubs already migrated (username does not start with their
|
||||
* homeUserId) are skipped without a network call.
|
||||
*
|
||||
* Gated on peer.status='active' — the lookup endpoint requires the requesting
|
||||
* peer to be active on the receiving side. We additionally check our local
|
||||
* peer row here so we don't waste outbound RTTs on peers we know aren't ready.
|
||||
*
|
||||
* Called from onPeerActivated (per-origin, gated on peer status='active') and
|
||||
* from a one-shot startup pass for any peer already active at boot.
|
||||
*/
|
||||
export async function backfillStubUsernamesForPeer(peerOrigin: string): Promise<void> {
|
||||
const db = getDb();
|
||||
const peerDomain = extractDomain(peerOrigin);
|
||||
|
||||
const peer = db
|
||||
.select({ status: schema.federationPeers.status })
|
||||
.from(schema.federationPeers)
|
||||
.where(eq(schema.federationPeers.origin, peerOrigin))
|
||||
.get();
|
||||
if (!peer || peer.status !== 'active') return;
|
||||
|
||||
// Coarse SQL prefilter: stubs from this peer whose username ends with @peerDomain.
|
||||
// We then narrow in JS to the legacy `<homeUserId>@<domain>` shape because Drizzle
|
||||
// can't express that comparison portably.
|
||||
const candidates = db
|
||||
.select()
|
||||
.from(schema.users)
|
||||
.where(
|
||||
and(
|
||||
eq(schema.users.homeInstance, peerDomain),
|
||||
eq(schema.users.isDeleted, 0),
|
||||
like(schema.users.username, '%@' + peerDomain),
|
||||
),
|
||||
)
|
||||
.all();
|
||||
|
||||
for (const stub of candidates) {
|
||||
if (!stub.homeUserId) continue;
|
||||
const expectedLegacy = `${stub.homeUserId}@${peerDomain}`.toLowerCase();
|
||||
if (stub.username !== expectedLegacy) continue; // already migrated or non-legacy shape
|
||||
|
||||
const result = await lookupRemoteUserByHomeId(peerOrigin, stub.homeUserId);
|
||||
if (!result.ok) {
|
||||
// not_found / unreachable / rate_limited — leave untouched.
|
||||
// Will retry on next onPeerActivated for this origin.
|
||||
continue;
|
||||
}
|
||||
|
||||
const newUsername = `${result.username}@${peerDomain}`.toLowerCase();
|
||||
if (newUsername === stub.username) continue; // already correct
|
||||
|
||||
// Collision check: another row at the target username (rare under the new
|
||||
// scheme but possible from prior partial replication).
|
||||
const collision = db
|
||||
.select({ id: schema.users.id })
|
||||
.from(schema.users)
|
||||
.where(eq(schema.users.username, newUsername))
|
||||
.get();
|
||||
if (collision && collision.id !== stub.id) {
|
||||
console.warn(`[stub-backfill] username collision on ${newUsername} — leaving stub ${stub.id} as ${stub.username}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Fill displayName from result.profile if the stub has none, mirroring the
|
||||
// displayName ?? username fallback applied at hydrate / profile_update time.
|
||||
const updates: { username: string; displayName?: string } = { username: newUsername };
|
||||
if (!stub.displayName) {
|
||||
updates.displayName = result.profile.displayName ?? result.username;
|
||||
}
|
||||
|
||||
db.update(schema.users)
|
||||
.set(updates)
|
||||
.where(eq(schema.users.id, stub.id))
|
||||
.run();
|
||||
|
||||
console.log(`[stub-backfill] rewrote stub ${stub.id}: ${stub.username} → ${newUsername}`);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user