From 8e589f41fd410f09b44f1b38498b5a1ae9ad9ed3 Mon Sep 17 00:00:00 2001 From: Jannis Braun <151788261+TheZwiss@users.noreply.github.com> Date: Fri, 27 Mar 2026 05:35:29 +0100 Subject: [PATCH] fix: send dm_channel_created to local users when bootstrapping federated group DM MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When processMemberAddEvent bootstrapped a new group DM channel for the first time on a receiving instance, local users were never notified via WebSocket — only dm_member_added was broadcast, which requires the client to already know about the channel. Now, after bootstrap, dm_channel_created is sent directly to each local connected member, and the redundant dm_member_added broadcast is skipped for the bootstrap path. --- packages/server/src/routes/federation.ts | 46 ++++++++++++++++++++---- 1 file changed, 39 insertions(+), 7 deletions(-) diff --git a/packages/server/src/routes/federation.ts b/packages/server/src/routes/federation.ts index fe94a046..175d8f84 100644 --- a/packages/server/src/routes/federation.ts +++ b/packages/server/src/routes/federation.ts @@ -1,6 +1,6 @@ import type { FastifyInstance } from 'fastify'; import { randomBytes } from 'node:crypto'; -import { eq, and, or, isNull } from 'drizzle-orm'; +import { eq, and, or, isNull, inArray } from 'drizzle-orm'; import { authenticate, requireAdmin } from '../utils/auth.js'; import { generateHmacSecret, getOurOrigin, parseFederationHeaders, verifySignature } from '../utils/federationAuth.js'; import { generateSnowflake } from '../utils/snowflake.js'; @@ -1637,6 +1637,8 @@ function processMemberAddEvent( .where(eq(schema.dmChannels.federatedId, event.federatedId)) .get(); + let bootstrapped = false; + // Bootstrap: channel doesn't exist yet — create from group metadata if (!channel && event.group) { const channelId = generateSnowflake(); @@ -1682,6 +1684,34 @@ function processMemberAddEvent( .where(eq(schema.dmChannels.id, channelId)).get(); console.log(`[federation] Bootstrapped group DM channel ${channelId} (federated_id: ${event.federatedId})`); + + bootstrapped = true; + + // Build full DmChannel response for dm_channel_created + const memberRows = db.select() + .from(schema.dmMembers) + .where(eq(schema.dmMembers.dmChannelId, channelId)) + .all(); + const memberUserIds = memberRows.map(m => m.userId); + const memberUsers = memberUserIds.length > 0 + ? db.select().from(schema.users).where(inArray(schema.users.id, memberUserIds)).all() + : []; + + const bootstrapResult = { + id: channelId, + ownerId, + createdAt: now, + members: memberUsers.map(u => sanitizeUser(u)), + lastMessage: null, + }; + + // Send dm_channel_created to all local WebSocket-connected members + for (const mu of memberUsers) { + connectionManager.sendToUser(mu.id, { + type: 'dm_channel_created', + dmChannel: bootstrapResult, + }); + } } if (!channel) { @@ -1735,12 +1765,14 @@ function processMemberAddEvent( }).run(); } - // Broadcast to local WebSocket clients - connectionManager.sendToDmMembers(channel.id, { - type: 'dm_member_added', - dmChannelId: channel.id, - user: sanitizeUser(localUser), - }); + if (!bootstrapped) { + // Broadcast to local WebSocket clients + connectionManager.sendToDmMembers(channel.id, { + type: 'dm_member_added', + dmChannelId: channel.id, + user: sanitizeUser(localUser), + }); + } accepted.push(event.messageId); }