fix(federation): add participants array to relay events and fix recipient resolution

The relay was failing because processCreateEvent relied on the friends
table to discover the DM recipient, but friendships aren't federated
across instances. Also, resolveLocalUser matched deleted replicated
users before active ones.

- Add participants[] to FederationRelayEvent with homeUserId/homeInstance
  for all DM channel members
- Add getDmParticipants() helper to look up member identities
- Include participants in outbox payloads (create/update) and sync events
- Rewrite processCreateEvent to resolve participants directly, compute
  canonicalDmPairId, and findOrCreateDmChannel — removing the entire
  friends-list fallback (60+ lines)
- Fix resolveLocalUser to filter out deleted users (is_deleted = 0)
  and prefer the replicated user match when multiple candidates exist
This commit is contained in:
Jannis Braun
2026-03-26 05:36:41 +01:00
parent cb9a70d7a6
commit cf9fcb78ed
6 changed files with 94 additions and 126 deletions
+28 -1
View File
@@ -3,7 +3,7 @@ import * as schema from '../db/schema.js';
import { eq, and } from 'drizzle-orm';
import { generateSnowflake } from './snowflake.js';
import crypto from 'node:crypto';
import type { FederationRelayEvent } from '@backspace/shared';
import type { FederationRelayEvent, FederationRelayParticipant } from '@backspace/shared';
import { config } from '../config.js';
// ─── Settings Cache ──────────────────────────────────────────────────────────
@@ -210,6 +210,33 @@ export function canonicalDmPairId(homeUserIdA: string, homeUserIdB: string): str
return crypto.createHash('sha256').update(sorted.join(':')).digest('hex').slice(0, 32);
}
/**
* Look up all members of a DM channel and return their federated identities.
* Used to include participants in relay events so the receiving instance can
* resolve both parties without relying on the friends list.
*/
export function getDmParticipants(dmChannelId: string): FederationRelayParticipant[] {
const db = getDb();
const members = db
.select({
userId: schema.dmMembers.userId,
homeUserId: schema.users.homeUserId,
homeInstance: schema.users.homeInstance,
id: schema.users.id,
})
.from(schema.dmMembers)
.innerJoin(schema.users, eq(schema.dmMembers.userId, schema.users.id))
.where(eq(schema.dmMembers.dmChannelId, dmChannelId))
.all();
const domainOrigin = config.domain ? `https://${config.domain}` : '';
return members.map(m => ({
homeUserId: m.homeUserId || m.id,
homeInstance: m.homeInstance || domainOrigin,
}));
}
/**
* Build the relay payload object for a DM message.
* The caller may augment the returned object with attachments before serialization.
@@ -172,6 +172,7 @@ async function processOutboxTick(): Promise<void> {
messageId: entry.messageId,
encryptionVersion: (entry.encryptionVersion ?? 0) as 0,
timestamp: entry.createdAt,
...(parsed.participants ? { participants: parsed.participants } : {}),
...(parsed.message ? { message: parsed.message } : {}),
...(parsed.reactions ? { reactions: parsed.reactions } : {}),
...(parsed.reaction ? { reaction: parsed.reaction } : {}),