fix: federation DM identity corruption — sync federatedId, guard backfill, remove bad merge criterion
Three bugs that combined to corrupt DM identities during initial sync:
1. Sync endpoint omitted federatedId for group DMs, causing the receiver
to treat them as 1-on-1 DMs and compute wrong pair hashes — creating
phantom channels that collide with real ones.
2. backfillHomeUserId unconditionally overwrote existing homeUserIds,
so a single wrong match would permanently corrupt a user's identity
and cascade to all subsequent lookups.
3. Migration duplicate-stub Criterion 1 ("shared 1-on-1 DM membership")
incorrectly merged different users from the same domain who were
simply having a conversation, destroying one user's identity.
This commit is contained in:
@@ -789,19 +789,9 @@ export function runMigrations(db: Database.Database): void {
|
|||||||
|
|
||||||
let reason: string | null = null;
|
let reason: string | null = null;
|
||||||
|
|
||||||
// Criterion 1: shared 1-on-1 DM membership
|
// Criterion 1 removed: "shared 1-on-1 DM membership" was wrong —
|
||||||
if (!reason) {
|
// two users from the same domain sharing a DM channel doesn't mean
|
||||||
const shared = db.prepare(`
|
// they're duplicates, it means they're having a conversation.
|
||||||
SELECT m1.dm_channel_id
|
|
||||||
FROM dm_members m1
|
|
||||||
JOIN dm_members m2 ON m1.dm_channel_id = m2.dm_channel_id
|
|
||||||
JOIN dm_channels c ON c.id = m1.dm_channel_id
|
|
||||||
WHERE m1.user_id = ? AND m2.user_id = ?
|
|
||||||
AND c.owner_id IS NULL
|
|
||||||
LIMIT 1
|
|
||||||
`).get(a.id, b.id) as { dm_channel_id: string } | undefined;
|
|
||||||
if (shared) reason = `shared 1-on-1 DM channel ${shared.dm_channel_id}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Criterion 2: username cross-reference
|
// Criterion 2: username cross-reference
|
||||||
if (!reason) {
|
if (!reason) {
|
||||||
|
|||||||
@@ -1208,8 +1208,17 @@ export async function federationRoutes(app: FastifyInstance): Promise<void> {
|
|||||||
sourceUrl: `${localOrigin}/api/uploads/${a.filename}`,
|
sourceUrl: `${localOrigin}/api/uploads/${a.filename}`,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
// Include federatedId for group DMs so the peer uses the correct
|
||||||
|
// channel lookup path instead of computing a 1-on-1 pair hash.
|
||||||
|
const syncChannel = db
|
||||||
|
.select({ federatedId: schema.dmChannels.federatedId, ownerId: schema.dmChannels.ownerId })
|
||||||
|
.from(schema.dmChannels)
|
||||||
|
.where(eq(schema.dmChannels.id, mutation.context_id))
|
||||||
|
.get();
|
||||||
|
|
||||||
events.push({
|
events.push({
|
||||||
eventType: mutationType,
|
eventType: mutationType,
|
||||||
|
...(syncChannel?.federatedId && syncChannel.ownerId ? { federatedId: syncChannel.federatedId } : {}),
|
||||||
dmChannelId: mutation.context_id,
|
dmChannelId: mutation.context_id,
|
||||||
messageId: message.id,
|
messageId: message.id,
|
||||||
encryptionVersion: 0,
|
encryptionVersion: 0,
|
||||||
@@ -1550,6 +1559,13 @@ function backfillHomeUserId(
|
|||||||
db: ReturnType<typeof getDb>,
|
db: ReturnType<typeof getDb>,
|
||||||
): typeof schema.users.$inferSelect {
|
): typeof schema.users.$inferSelect {
|
||||||
if (user.homeUserId === homeUserId) return user;
|
if (user.homeUserId === homeUserId) return user;
|
||||||
|
// Only backfill if the user has no homeUserId yet. If they already have a
|
||||||
|
// DIFFERENT non-null homeUserId, this means the wrong user was matched —
|
||||||
|
// overwriting would corrupt their identity.
|
||||||
|
if (user.homeUserId) {
|
||||||
|
console.warn(`[federation] Refusing to overwrite homeUserId on user ${user.id} (${user.username}): existing=${user.homeUserId}, incoming=${homeUserId}`);
|
||||||
|
return user;
|
||||||
|
}
|
||||||
db.update(schema.users)
|
db.update(schema.users)
|
||||||
.set({ homeUserId })
|
.set({ homeUserId })
|
||||||
.where(eq(schema.users.id, user.id))
|
.where(eq(schema.users.id, user.id))
|
||||||
|
|||||||
Reference in New Issue
Block a user