feat: update DM call handlers and processors for federatedCallId lookup

This commit is contained in:
Jannis Braun
2026-04-08 03:18:25 +02:00
parent 1690295db5
commit 07edb25d12
2 changed files with 210 additions and 263 deletions
+40 -55
View File
@@ -13,7 +13,7 @@ import { deleteAttachmentFiles } from '../utils/fileCleanup.js';
import { tombstoneUser, collectDeletionBroadcastTargets, collectProfileBroadcastTargetIds } from '../utils/userDeletion.js';
import { computeFederatedId, getDmParticipants, sendCallRelay } from '../utils/federationOutbox.js';
import { getDmMessageWithUser } from './dm.js';
import type { FederationRelayRequest, FederationRelayResponse, FederationRelayEvent, FederationRelayAttachment, FederationSyncRequest, FederationSyncResponse, DmMessageWithUser, DmChannel, FederationRelayProfileSnapshot, FederationIdentityDeleteS2SRequest, FederationProfileUpdatePayload } from '@backspace/shared';
import type { FederationRelayRequest, FederationRelayResponse, FederationRelayEvent, FederationRelayAttachment, FederationSyncRequest, FederationSyncResponse, DmMessageWithUser, DmChannel, FederationRelayProfileSnapshot, FederationIdentityDeleteS2SRequest, FederationProfileUpdatePayload, ServerEvent } from '@backspace/shared';
/** Fields safe to expose to admin callers (everything except hmacSecret). */
interface SanitizedPeer {
@@ -3568,56 +3568,51 @@ function processDmCallAcceptEvent(
.from(schema.dmChannels)
.where(eq(schema.dmChannels.federatedId, event.federatedId))
.get();
if (!channel) {
rejected.push({ messageId: event.messageId, reason: 'channel_not_found' });
return;
}
const dmChannelId = channel.id;
const dmChannelId = channel?.id;
// Check if we're the HOST (have a VoiceRoom)
const room = connectionManager.getRoom(dmChannelId);
const room = dmChannelId ? connectionManager.getRoom(dmChannelId) : undefined;
if (room && room.roomType === 'dm') {
const meta = room.metadata as DmRoomMeta;
if (meta.state === 'ringing') {
connectionManager.activateDmRoom(dmChannelId);
connectionManager.activateDmRoom(dmChannelId!);
// Join caller to room
connectionManager.leaveCurrentRoom(meta.callerId);
connectionManager.joinRoom(dmChannelId, meta.callerId);
connectionManager.joinRoom(dmChannelId!, meta.callerId);
connectionManager.sendToDmMembers(dmChannelId, {
connectionManager.sendToDmMembers(dmChannelId!, {
type: 'voice_state_update',
channelId: dmChannelId,
channelId: dmChannelId!,
userId: meta.callerId,
action: 'join',
});
}
// Broadcast accepted locally
connectionManager.sendToDmMembers(dmChannelId, {
connectionManager.sendToDmMembers(dmChannelId!, {
type: 'dm_call_accepted',
dmChannelId,
dmChannelId: dmChannelId!,
});
// Fan out to ALL other remote instances (exclude the one that sent the accept)
const normalizedSource = sourceInstance.startsWith('http') ? sourceInstance : `https://${sourceInstance}`;
fanOutCallEvent(dmChannelId, event.federatedId, 'dm_call_accept', {
fanOutCallEvent(dmChannelId!, event.federatedId, 'dm_call_accept', {
call: { acceptor: event.call.acceptor },
}, normalizedSource, db).catch(err =>
console.error('[federation] Fan-out dm_call_accept failed:', err)
);
} else {
// We're a REMOTE instance receiving fan-out — transition local state
const fedCall = connectionManager.getFederatedCall(dmChannelId);
const fedCall = connectionManager.getFederatedCall(event.federatedId);
if (fedCall) {
connectionManager.activateFederatedCall(dmChannelId);
connectionManager.sendToDmMembers(dmChannelId, {
connectionManager.activateFederatedCall(event.federatedId);
connectionManager.sendToFederatedCallUsers(event.federatedId, {
type: 'dm_call_accepted',
dmChannelId,
});
dmChannelId: fedCall.dmChannelId,
federatedCallId: event.federatedId,
} as ServerEvent);
}
}
@@ -3645,39 +3640,34 @@ function processDmCallRejectEvent(
.from(schema.dmChannels)
.where(eq(schema.dmChannels.federatedId, event.federatedId))
.get();
const dmChannelId = channel?.id;
if (!channel) {
rejected.push({ messageId: event.messageId, reason: 'channel_not_found' });
return;
}
const dmChannelId = channel.id;
const room = connectionManager.getRoom(dmChannelId);
const room = dmChannelId ? connectionManager.getRoom(dmChannelId) : undefined;
if (room && room.roomType === 'dm') {
const meta = room.metadata as DmRoomMeta;
connectionManager.clearVoiceWs(meta.callerId);
connectionManager.destroyRoom(dmChannelId);
connectionManager.destroyRoom(dmChannelId!);
connectionManager.sendToDmMembers(dmChannelId, {
connectionManager.sendToDmMembers(dmChannelId!, {
type: 'dm_call_rejected',
dmChannelId,
dmChannelId: dmChannelId!,
});
const normalizedSource = sourceInstance.startsWith('http') ? sourceInstance : `https://${sourceInstance}`;
fanOutCallEvent(dmChannelId, event.federatedId, 'dm_call_end', {
fanOutCallEvent(dmChannelId!, event.federatedId, 'dm_call_end', {
call: { endedBy: event.call.rejector },
}, normalizedSource, db).catch(err =>
console.error('[federation] Fan-out dm_call_end (reject) failed:', err)
);
} else {
const fedCall = connectionManager.getFederatedCall(dmChannelId);
const fedCall = connectionManager.getFederatedCall(event.federatedId);
if (fedCall) {
connectionManager.clearFederatedCall(dmChannelId);
connectionManager.sendToDmMembers(dmChannelId, {
connectionManager.sendToFederatedCallUsers(event.federatedId, {
type: 'dm_call_rejected',
dmChannelId,
});
dmChannelId: fedCall.dmChannelId,
federatedCallId: event.federatedId,
} as ServerEvent);
connectionManager.clearFederatedCall(event.federatedId);
}
}
@@ -3705,15 +3695,9 @@ function processDmCallEndEvent(
.from(schema.dmChannels)
.where(eq(schema.dmChannels.federatedId, event.federatedId))
.get();
const dmChannelId = channel?.id;
if (!channel) {
rejected.push({ messageId: event.messageId, reason: 'channel_not_found' });
return;
}
const dmChannelId = channel.id;
const room = connectionManager.getRoom(dmChannelId);
const room = dmChannelId ? connectionManager.getRoom(dmChannelId) : undefined;
if (room && room.roomType === 'dm') {
const meta = room.metadata as DmRoomMeta;
connectionManager.clearVoiceWs(meta.callerId);
@@ -3721,27 +3705,28 @@ function processDmCallEndEvent(
connectionManager.clearVoiceUserStatus(pid);
connectionManager.clearVoiceWs(pid);
}
connectionManager.destroyRoom(dmChannelId);
connectionManager.destroyRoom(dmChannelId!);
connectionManager.sendToDmMembers(dmChannelId, {
connectionManager.sendToDmMembers(dmChannelId!, {
type: 'dm_call_ended',
dmChannelId,
dmChannelId: dmChannelId!,
});
const normalizedSource = sourceInstance.startsWith('http') ? sourceInstance : `https://${sourceInstance}`;
fanOutCallEvent(dmChannelId, event.federatedId, 'dm_call_end', {
fanOutCallEvent(dmChannelId!, event.federatedId, 'dm_call_end', {
call: { endedBy: event.call.endedBy },
}, normalizedSource, db).catch(err =>
console.error('[federation] Fan-out dm_call_end failed:', err)
);
} else {
const fedCall = connectionManager.getFederatedCall(dmChannelId);
const fedCall = connectionManager.getFederatedCall(event.federatedId);
if (fedCall) {
connectionManager.clearFederatedCall(dmChannelId);
connectionManager.sendToDmMembers(dmChannelId, {
connectionManager.sendToFederatedCallUsers(event.federatedId, {
type: 'dm_call_ended',
dmChannelId,
});
dmChannelId: fedCall.dmChannelId,
federatedCallId: event.federatedId,
} as ServerEvent);
connectionManager.clearFederatedCall(event.federatedId);
}
}
+116 -154
View File
@@ -6,7 +6,7 @@ import { connectionManager } from './handler.js';
import type { VoiceRoom, DmRoomMeta, SpaceRoomMeta } from './handler.js';
import { isMember, getChannelSpaceId, isDmMember, hasPermission, computePermissions, PermissionBits } from '../utils/permissions.js';
import { broadcastDmMessage, getDmMessageWithUser } from '../routes/dm.js';
import { MAX_MESSAGE_LENGTH, type MessageWithUser, type Attachment, type DmMessageWithUser, type Embed, type Activity, type ActivityType, type ActivityTimestamps, type ActivityAssets } from '@backspace/shared';
import { MAX_MESSAGE_LENGTH, type MessageWithUser, type Attachment, type DmMessageWithUser, type Embed, type Activity, type ActivityType, type ActivityTimestamps, type ActivityAssets, type ServerEvent } from '@backspace/shared';
import { ACTIVITY_LIMITS } from '@backspace/shared/src/activities.js';
import { sanitizeUser } from '../utils/sanitize.js';
import { deleteAttachmentFiles } from '../utils/fileCleanup.js';
@@ -1442,32 +1442,70 @@ function handleDmCallStart(event: Record<string, unknown>, userId: string, usern
}
function handleDmCallAccept(event: Record<string, unknown>, userId: string, ws: WebSocket): void {
const dmChannelId = event.dmChannelId as string;
if (!dmChannelId || typeof dmChannelId !== 'string') {
connectionManager.sendToUser(userId, { type: 'error', message: 'dmChannelId is required' });
const dmChannelId = (event.dmChannelId as string) || null;
const federatedCallId = (event.federatedCallId as string) || null;
if (!dmChannelId && !federatedCallId) {
connectionManager.sendToUser(userId, { type: 'error', message: 'dmChannelId or federatedCallId is required' });
return;
}
// Path 1: Local room (we're the host) — only possible with dmChannelId
if (dmChannelId) {
if (!isDmMember(dmChannelId, userId)) {
connectionManager.sendToUser(userId, { type: 'error', message: 'You are not a member of this DM channel' });
return;
}
const room = connectionManager.getRoom(dmChannelId);
if (!room || room.roomType !== 'dm') {
// Check if this is a federated call (this instance is not the host)
const fedCall = connectionManager.getFederatedCall(dmChannelId);
if (fedCall) {
// Transition local state
connectionManager.activateFederatedCall(dmChannelId);
if (room && room.roomType === 'dm') {
const meta = room.metadata as DmRoomMeta;
// Broadcast locally
if (meta.state === 'ringing') {
connectionManager.activateDmRoom(dmChannelId);
const callerLeft = connectionManager.leaveCurrentRoom(meta.callerId);
if (callerLeft) broadcastRoomLeave(callerLeft.roomId, callerLeft.room, meta.callerId);
connectionManager.joinRoom(dmChannelId, meta.callerId);
connectionManager.sendToDmMembers(dmChannelId, {
type: 'dm_call_accepted',
dmChannelId,
type: 'voice_state_update',
channelId: dmChannelId,
userId: meta.callerId,
action: 'join',
});
}
const acceptorLeft = connectionManager.leaveCurrentRoom(userId);
if (acceptorLeft) broadcastRoomLeave(acceptorLeft.roomId, acceptorLeft.room, userId);
connectionManager.joinRoom(dmChannelId, userId);
connectionManager.setVoiceWs(userId, ws);
connectionManager.sendToDmMembers(dmChannelId, { type: 'dm_call_accepted', dmChannelId });
connectionManager.sendToDmMembers(dmChannelId, {
type: 'voice_state_update',
channelId: dmChannelId,
userId,
action: 'join',
});
sendFederatedCallAccept(dmChannelId, userId)
.catch(err => console.error('[federation] sendFederatedCallAccept error:', err));
return;
}
}
// Path 2: Federated call (we're a remote instance)
const fedCall = federatedCallId
? connectionManager.getFederatedCall(federatedCallId)
: dmChannelId
? connectionManager.getFederatedCallByDmChannel(dmChannelId)
: undefined;
if (fedCall) {
connectionManager.activateFederatedCall(fedCall.federatedId);
connectionManager.sendToFederatedCallUsers(fedCall.federatedId, {
type: 'dm_call_accepted',
dmChannelId: fedCall.dmChannelId,
federatedCallId: fedCall.federatedId,
} as ServerEvent);
// Send accept to host (fire-and-forget)
const db = getDb();
const user = db.select({ homeUserId: schema.users.homeUserId })
.from(schema.users)
@@ -1485,93 +1523,48 @@ function handleDmCallAccept(event: Record<string, unknown>, userId: string, ws:
acceptor: { homeUserId, homeInstance: getOurOrigin() },
},
}]).catch(err => console.error('[federation] Failed to send dm_call_accept:', err));
return;
}
connectionManager.sendToUser(userId, { type: 'error', message: 'No active call in this DM channel' });
return;
}
const meta = room.metadata as DmRoomMeta;
if (meta.state === 'ringing') {
// First accept — transition ringing→active and join the caller
connectionManager.activateDmRoom(dmChannelId);
// Leave caller's current room if in one
const callerLeft = connectionManager.leaveCurrentRoom(meta.callerId);
if (callerLeft) {
broadcastRoomLeave(callerLeft.roomId, callerLeft.room, meta.callerId);
}
// Join caller into the DM room
connectionManager.joinRoom(dmChannelId, meta.callerId);
// Broadcast voice_state_update join for caller
connectionManager.sendToDmMembers(dmChannelId, {
type: 'voice_state_update',
channelId: dmChannelId,
userId: meta.callerId,
action: 'join',
});
}
// If already active, this is a late-join (e.g. 3rd member joining group call).
// Skip the ringing→active transition and caller join — just join the acceptor below.
// Leave acceptor's current room if in one
const acceptorLeft = connectionManager.leaveCurrentRoom(userId);
if (acceptorLeft) {
broadcastRoomLeave(acceptorLeft.roomId, acceptorLeft.room, userId);
}
// Join acceptor into the DM room
connectionManager.joinRoom(dmChannelId, userId);
// Bind voice session to this socket for the acceptor
connectionManager.setVoiceWs(userId, ws);
// Notify all DM members that the call was accepted
connectionManager.sendToDmMembers(dmChannelId, {
type: 'dm_call_accepted',
dmChannelId,
});
// Broadcast voice_state_update join for acceptor
connectionManager.sendToDmMembers(dmChannelId, {
type: 'voice_state_update',
channelId: dmChannelId,
userId,
action: 'join',
});
// Federation: notify remote instances that the call was accepted
sendFederatedCallAccept(dmChannelId, userId)
.catch(err => console.error('[federation] sendFederatedCallAccept error:', err));
}
function handleDmCallReject(event: Record<string, unknown>, userId: string): void {
const dmChannelId = event.dmChannelId as string;
if (!dmChannelId || typeof dmChannelId !== 'string') {
connectionManager.sendToUser(userId, { type: 'error', message: 'dmChannelId is required' });
return;
}
const dmChannelId = (event.dmChannelId as string) || null;
const federatedCallId = (event.federatedCallId as string) || null;
if (!isDmMember(dmChannelId, userId)) {
connectionManager.sendToUser(userId, { type: 'error', message: 'You are not a member of this DM channel' });
return;
}
if (!dmChannelId && !federatedCallId) return;
// Path 1: Local room (we're the host)
if (dmChannelId) {
if (!isDmMember(dmChannelId, userId)) return;
const room = connectionManager.getRoom(dmChannelId);
if (!room) {
// Check federated call registry
const fedCall = connectionManager.getFederatedCall(dmChannelId);
if (room) {
const meta = room.metadata as DmRoomMeta;
connectionManager.clearVoiceWs(meta.callerId);
connectionManager.destroyRoom(dmChannelId);
connectionManager.sendToDmMembers(dmChannelId, { type: 'dm_call_rejected', dmChannelId });
sendFederatedCallEnd(dmChannelId, userId)
.catch(err => console.error('[federation] sendFederatedCallEnd error:', err));
return;
}
}
// Path 2: Federated call
const fedCall = federatedCallId
? connectionManager.getFederatedCall(federatedCallId)
: dmChannelId
? connectionManager.getFederatedCallByDmChannel(dmChannelId)
: undefined;
if (fedCall) {
connectionManager.clearFederatedCall(dmChannelId);
connectionManager.sendToDmMembers(dmChannelId, {
connectionManager.sendToFederatedCallUsers(fedCall.federatedId, {
type: 'dm_call_rejected',
dmChannelId,
});
dmChannelId: fedCall.dmChannelId,
federatedCallId: fedCall.federatedId,
} as ServerEvent);
connectionManager.clearFederatedCall(fedCall.federatedId);
const db = getDb();
const user = db.select({ homeUserId: schema.users.homeUserId })
@@ -1590,53 +1583,49 @@ function handleDmCallReject(event: Record<string, unknown>, userId: string): voi
rejector: { homeUserId, homeInstance: getOurOrigin() },
},
}]).catch(err => console.error('[federation] Failed to send dm_call_reject:', err));
return;
}
return; // No active call, silently ignore
}
// Extract caller ID before destroying the room
const meta = room.metadata as DmRoomMeta;
// Clear caller's voice ws binding (set during handleDmCallStart)
connectionManager.clearVoiceWs(meta.callerId);
// Destroy room
connectionManager.destroyRoom(dmChannelId);
// Notify all DM members that the call was rejected
connectionManager.sendToDmMembers(dmChannelId, {
type: 'dm_call_rejected',
dmChannelId,
});
// Federation: notify remote instances the call was rejected (= ended for 1-on-1)
sendFederatedCallEnd(dmChannelId, userId)
.catch(err => console.error('[federation] sendFederatedCallEnd error:', err));
}
function handleDmCallEnd(event: Record<string, unknown>, userId: string): void {
const dmChannelId = event.dmChannelId as string;
if (!dmChannelId || typeof dmChannelId !== 'string') {
connectionManager.sendToUser(userId, { type: 'error', message: 'dmChannelId is required' });
return;
}
const dmChannelId = (event.dmChannelId as string) || null;
const federatedCallId = (event.federatedCallId as string) || null;
if (!isDmMember(dmChannelId, userId)) {
connectionManager.sendToUser(userId, { type: 'error', message: 'You are not a member of this DM channel' });
return;
}
if (!dmChannelId && !federatedCallId) return;
// Path 1: Local room (we're the host)
if (dmChannelId) {
if (!isDmMember(dmChannelId, userId)) return;
const room = connectionManager.getRoom(dmChannelId);
if (!room) {
const fedCall = connectionManager.getFederatedCall(dmChannelId);
if (room) {
const meta = room.metadata as DmRoomMeta;
connectionManager.clearVoiceWs(meta.callerId);
for (const participantId of room.participants) {
connectionManager.clearVoiceUserStatus(participantId);
connectionManager.clearVoiceWs(participantId);
}
connectionManager.destroyRoom(dmChannelId);
connectionManager.sendToDmMembers(dmChannelId, { type: 'dm_call_ended', dmChannelId });
sendFederatedCallEnd(dmChannelId, userId)
.catch(err => console.error('[federation] sendFederatedCallEnd error:', err));
return;
}
}
// Path 2: Federated call
const fedCall = federatedCallId
? connectionManager.getFederatedCall(federatedCallId)
: dmChannelId
? connectionManager.getFederatedCallByDmChannel(dmChannelId)
: undefined;
if (fedCall) {
connectionManager.clearFederatedCall(dmChannelId);
connectionManager.sendToDmMembers(dmChannelId, {
connectionManager.sendToFederatedCallUsers(fedCall.federatedId, {
type: 'dm_call_ended',
dmChannelId,
});
dmChannelId: fedCall.dmChannelId,
federatedCallId: fedCall.federatedId,
} as ServerEvent);
connectionManager.clearFederatedCall(fedCall.federatedId);
const db = getDb();
const user = db.select({ homeUserId: schema.users.homeUserId })
@@ -1655,34 +1644,7 @@ function handleDmCallEnd(event: Record<string, unknown>, userId: string): void {
endedBy: { homeUserId, homeInstance: getOurOrigin() },
},
}]).catch(err => console.error('[federation] Failed to send dm_call_end:', err));
return;
}
return; // No active call, silently ignore
}
// Clear voice ws binding for the caller (may not be in participants if still ringing)
const meta = room.metadata as DmRoomMeta;
connectionManager.clearVoiceWs(meta.callerId);
// Clear voice user states and voice ws for all participants
for (const participantId of room.participants) {
connectionManager.clearVoiceUserStatus(participantId);
connectionManager.clearVoiceWs(participantId);
}
// Destroy room (removes all participants from userToRoom)
connectionManager.destroyRoom(dmChannelId);
// Notify all DM members that the call ended
connectionManager.sendToDmMembers(dmChannelId, {
type: 'dm_call_ended',
dmChannelId,
});
// Federation: notify remote instances the call ended
sendFederatedCallEnd(dmChannelId, userId)
.catch(err => console.error('[federation] sendFederatedCallEnd error:', err));
}
/**