feat: update DM call handlers and processors for federatedCallId lookup
This commit is contained in:
@@ -13,7 +13,7 @@ import { deleteAttachmentFiles } from '../utils/fileCleanup.js';
|
|||||||
import { tombstoneUser, collectDeletionBroadcastTargets, collectProfileBroadcastTargetIds } from '../utils/userDeletion.js';
|
import { tombstoneUser, collectDeletionBroadcastTargets, collectProfileBroadcastTargetIds } from '../utils/userDeletion.js';
|
||||||
import { computeFederatedId, getDmParticipants, sendCallRelay } from '../utils/federationOutbox.js';
|
import { computeFederatedId, getDmParticipants, sendCallRelay } from '../utils/federationOutbox.js';
|
||||||
import { getDmMessageWithUser } from './dm.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). */
|
/** Fields safe to expose to admin callers (everything except hmacSecret). */
|
||||||
interface SanitizedPeer {
|
interface SanitizedPeer {
|
||||||
@@ -3568,56 +3568,51 @@ function processDmCallAcceptEvent(
|
|||||||
.from(schema.dmChannels)
|
.from(schema.dmChannels)
|
||||||
.where(eq(schema.dmChannels.federatedId, event.federatedId))
|
.where(eq(schema.dmChannels.federatedId, event.federatedId))
|
||||||
.get();
|
.get();
|
||||||
|
const dmChannelId = channel?.id;
|
||||||
if (!channel) {
|
|
||||||
rejected.push({ messageId: event.messageId, reason: 'channel_not_found' });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const dmChannelId = channel.id;
|
|
||||||
|
|
||||||
// Check if we're the HOST (have a VoiceRoom)
|
// 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') {
|
if (room && room.roomType === 'dm') {
|
||||||
const meta = room.metadata as DmRoomMeta;
|
const meta = room.metadata as DmRoomMeta;
|
||||||
|
|
||||||
if (meta.state === 'ringing') {
|
if (meta.state === 'ringing') {
|
||||||
connectionManager.activateDmRoom(dmChannelId);
|
connectionManager.activateDmRoom(dmChannelId!);
|
||||||
|
|
||||||
// Join caller to room
|
// Join caller to room
|
||||||
connectionManager.leaveCurrentRoom(meta.callerId);
|
connectionManager.leaveCurrentRoom(meta.callerId);
|
||||||
connectionManager.joinRoom(dmChannelId, meta.callerId);
|
connectionManager.joinRoom(dmChannelId!, meta.callerId);
|
||||||
|
|
||||||
connectionManager.sendToDmMembers(dmChannelId, {
|
connectionManager.sendToDmMembers(dmChannelId!, {
|
||||||
type: 'voice_state_update',
|
type: 'voice_state_update',
|
||||||
channelId: dmChannelId,
|
channelId: dmChannelId!,
|
||||||
userId: meta.callerId,
|
userId: meta.callerId,
|
||||||
action: 'join',
|
action: 'join',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Broadcast accepted locally
|
// Broadcast accepted locally
|
||||||
connectionManager.sendToDmMembers(dmChannelId, {
|
connectionManager.sendToDmMembers(dmChannelId!, {
|
||||||
type: 'dm_call_accepted',
|
type: 'dm_call_accepted',
|
||||||
dmChannelId,
|
dmChannelId: dmChannelId!,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Fan out to ALL other remote instances (exclude the one that sent the accept)
|
// Fan out to ALL other remote instances (exclude the one that sent the accept)
|
||||||
const normalizedSource = sourceInstance.startsWith('http') ? sourceInstance : `https://${sourceInstance}`;
|
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 },
|
call: { acceptor: event.call.acceptor },
|
||||||
}, normalizedSource, db).catch(err =>
|
}, normalizedSource, db).catch(err =>
|
||||||
console.error('[federation] Fan-out dm_call_accept failed:', err)
|
console.error('[federation] Fan-out dm_call_accept failed:', err)
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
// We're a REMOTE instance receiving fan-out — transition local state
|
// We're a REMOTE instance receiving fan-out — transition local state
|
||||||
const fedCall = connectionManager.getFederatedCall(dmChannelId);
|
const fedCall = connectionManager.getFederatedCall(event.federatedId);
|
||||||
if (fedCall) {
|
if (fedCall) {
|
||||||
connectionManager.activateFederatedCall(dmChannelId);
|
connectionManager.activateFederatedCall(event.federatedId);
|
||||||
connectionManager.sendToDmMembers(dmChannelId, {
|
connectionManager.sendToFederatedCallUsers(event.federatedId, {
|
||||||
type: 'dm_call_accepted',
|
type: 'dm_call_accepted',
|
||||||
dmChannelId,
|
dmChannelId: fedCall.dmChannelId,
|
||||||
});
|
federatedCallId: event.federatedId,
|
||||||
|
} as ServerEvent);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3645,39 +3640,34 @@ function processDmCallRejectEvent(
|
|||||||
.from(schema.dmChannels)
|
.from(schema.dmChannels)
|
||||||
.where(eq(schema.dmChannels.federatedId, event.federatedId))
|
.where(eq(schema.dmChannels.federatedId, event.federatedId))
|
||||||
.get();
|
.get();
|
||||||
|
const dmChannelId = channel?.id;
|
||||||
|
|
||||||
if (!channel) {
|
const room = dmChannelId ? connectionManager.getRoom(dmChannelId) : undefined;
|
||||||
rejected.push({ messageId: event.messageId, reason: 'channel_not_found' });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const dmChannelId = channel.id;
|
|
||||||
|
|
||||||
const room = connectionManager.getRoom(dmChannelId);
|
|
||||||
if (room && room.roomType === 'dm') {
|
if (room && room.roomType === 'dm') {
|
||||||
const meta = room.metadata as DmRoomMeta;
|
const meta = room.metadata as DmRoomMeta;
|
||||||
connectionManager.clearVoiceWs(meta.callerId);
|
connectionManager.clearVoiceWs(meta.callerId);
|
||||||
connectionManager.destroyRoom(dmChannelId);
|
connectionManager.destroyRoom(dmChannelId!);
|
||||||
|
|
||||||
connectionManager.sendToDmMembers(dmChannelId, {
|
connectionManager.sendToDmMembers(dmChannelId!, {
|
||||||
type: 'dm_call_rejected',
|
type: 'dm_call_rejected',
|
||||||
dmChannelId,
|
dmChannelId: dmChannelId!,
|
||||||
});
|
});
|
||||||
|
|
||||||
const normalizedSource = sourceInstance.startsWith('http') ? sourceInstance : `https://${sourceInstance}`;
|
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 },
|
call: { endedBy: event.call.rejector },
|
||||||
}, normalizedSource, db).catch(err =>
|
}, normalizedSource, db).catch(err =>
|
||||||
console.error('[federation] Fan-out dm_call_end (reject) failed:', err)
|
console.error('[federation] Fan-out dm_call_end (reject) failed:', err)
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
const fedCall = connectionManager.getFederatedCall(dmChannelId);
|
const fedCall = connectionManager.getFederatedCall(event.federatedId);
|
||||||
if (fedCall) {
|
if (fedCall) {
|
||||||
connectionManager.clearFederatedCall(dmChannelId);
|
connectionManager.sendToFederatedCallUsers(event.federatedId, {
|
||||||
connectionManager.sendToDmMembers(dmChannelId, {
|
|
||||||
type: 'dm_call_rejected',
|
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)
|
.from(schema.dmChannels)
|
||||||
.where(eq(schema.dmChannels.federatedId, event.federatedId))
|
.where(eq(schema.dmChannels.federatedId, event.federatedId))
|
||||||
.get();
|
.get();
|
||||||
|
const dmChannelId = channel?.id;
|
||||||
|
|
||||||
if (!channel) {
|
const room = dmChannelId ? connectionManager.getRoom(dmChannelId) : undefined;
|
||||||
rejected.push({ messageId: event.messageId, reason: 'channel_not_found' });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const dmChannelId = channel.id;
|
|
||||||
|
|
||||||
const room = connectionManager.getRoom(dmChannelId);
|
|
||||||
if (room && room.roomType === 'dm') {
|
if (room && room.roomType === 'dm') {
|
||||||
const meta = room.metadata as DmRoomMeta;
|
const meta = room.metadata as DmRoomMeta;
|
||||||
connectionManager.clearVoiceWs(meta.callerId);
|
connectionManager.clearVoiceWs(meta.callerId);
|
||||||
@@ -3721,27 +3705,28 @@ function processDmCallEndEvent(
|
|||||||
connectionManager.clearVoiceUserStatus(pid);
|
connectionManager.clearVoiceUserStatus(pid);
|
||||||
connectionManager.clearVoiceWs(pid);
|
connectionManager.clearVoiceWs(pid);
|
||||||
}
|
}
|
||||||
connectionManager.destroyRoom(dmChannelId);
|
connectionManager.destroyRoom(dmChannelId!);
|
||||||
|
|
||||||
connectionManager.sendToDmMembers(dmChannelId, {
|
connectionManager.sendToDmMembers(dmChannelId!, {
|
||||||
type: 'dm_call_ended',
|
type: 'dm_call_ended',
|
||||||
dmChannelId,
|
dmChannelId: dmChannelId!,
|
||||||
});
|
});
|
||||||
|
|
||||||
const normalizedSource = sourceInstance.startsWith('http') ? sourceInstance : `https://${sourceInstance}`;
|
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 },
|
call: { endedBy: event.call.endedBy },
|
||||||
}, normalizedSource, db).catch(err =>
|
}, normalizedSource, db).catch(err =>
|
||||||
console.error('[federation] Fan-out dm_call_end failed:', err)
|
console.error('[federation] Fan-out dm_call_end failed:', err)
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
const fedCall = connectionManager.getFederatedCall(dmChannelId);
|
const fedCall = connectionManager.getFederatedCall(event.federatedId);
|
||||||
if (fedCall) {
|
if (fedCall) {
|
||||||
connectionManager.clearFederatedCall(dmChannelId);
|
connectionManager.sendToFederatedCallUsers(event.federatedId, {
|
||||||
connectionManager.sendToDmMembers(dmChannelId, {
|
|
||||||
type: 'dm_call_ended',
|
type: 'dm_call_ended',
|
||||||
dmChannelId,
|
dmChannelId: fedCall.dmChannelId,
|
||||||
});
|
federatedCallId: event.federatedId,
|
||||||
|
} as ServerEvent);
|
||||||
|
connectionManager.clearFederatedCall(event.federatedId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+116
-154
@@ -6,7 +6,7 @@ import { connectionManager } from './handler.js';
|
|||||||
import type { VoiceRoom, DmRoomMeta, SpaceRoomMeta } from './handler.js';
|
import type { VoiceRoom, DmRoomMeta, SpaceRoomMeta } from './handler.js';
|
||||||
import { isMember, getChannelSpaceId, isDmMember, hasPermission, computePermissions, PermissionBits } from '../utils/permissions.js';
|
import { isMember, getChannelSpaceId, isDmMember, hasPermission, computePermissions, PermissionBits } from '../utils/permissions.js';
|
||||||
import { broadcastDmMessage, getDmMessageWithUser } from '../routes/dm.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 { ACTIVITY_LIMITS } from '@backspace/shared/src/activities.js';
|
||||||
import { sanitizeUser } from '../utils/sanitize.js';
|
import { sanitizeUser } from '../utils/sanitize.js';
|
||||||
import { deleteAttachmentFiles } from '../utils/fileCleanup.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 {
|
function handleDmCallAccept(event: Record<string, unknown>, userId: string, ws: WebSocket): void {
|
||||||
const dmChannelId = event.dmChannelId as string;
|
const dmChannelId = (event.dmChannelId as string) || null;
|
||||||
if (!dmChannelId || typeof dmChannelId !== 'string') {
|
const federatedCallId = (event.federatedCallId as string) || null;
|
||||||
connectionManager.sendToUser(userId, { type: 'error', message: 'dmChannelId is required' });
|
|
||||||
|
if (!dmChannelId && !federatedCallId) {
|
||||||
|
connectionManager.sendToUser(userId, { type: 'error', message: 'dmChannelId or federatedCallId is required' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Path 1: Local room (we're the host) — only possible with dmChannelId
|
||||||
|
if (dmChannelId) {
|
||||||
if (!isDmMember(dmChannelId, userId)) {
|
if (!isDmMember(dmChannelId, userId)) {
|
||||||
connectionManager.sendToUser(userId, { type: 'error', message: 'You are not a member of this DM channel' });
|
connectionManager.sendToUser(userId, { type: 'error', message: 'You are not a member of this DM channel' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const room = connectionManager.getRoom(dmChannelId);
|
const room = connectionManager.getRoom(dmChannelId);
|
||||||
if (!room || room.roomType !== 'dm') {
|
if (room && room.roomType === 'dm') {
|
||||||
// Check if this is a federated call (this instance is not the host)
|
const meta = room.metadata as DmRoomMeta;
|
||||||
const fedCall = connectionManager.getFederatedCall(dmChannelId);
|
|
||||||
if (fedCall) {
|
|
||||||
// Transition local state
|
|
||||||
connectionManager.activateFederatedCall(dmChannelId);
|
|
||||||
|
|
||||||
// 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, {
|
connectionManager.sendToDmMembers(dmChannelId, {
|
||||||
type: 'dm_call_accepted',
|
type: 'voice_state_update',
|
||||||
dmChannelId,
|
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 db = getDb();
|
||||||
const user = db.select({ homeUserId: schema.users.homeUserId })
|
const user = db.select({ homeUserId: schema.users.homeUserId })
|
||||||
.from(schema.users)
|
.from(schema.users)
|
||||||
@@ -1485,93 +1523,48 @@ function handleDmCallAccept(event: Record<string, unknown>, userId: string, ws:
|
|||||||
acceptor: { homeUserId, homeInstance: getOurOrigin() },
|
acceptor: { homeUserId, homeInstance: getOurOrigin() },
|
||||||
},
|
},
|
||||||
}]).catch(err => console.error('[federation] Failed to send dm_call_accept:', err));
|
}]).catch(err => console.error('[federation] Failed to send dm_call_accept:', err));
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
connectionManager.sendToUser(userId, { type: 'error', message: 'No active call in this DM channel' });
|
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 {
|
function handleDmCallReject(event: Record<string, unknown>, userId: string): void {
|
||||||
const dmChannelId = event.dmChannelId as string;
|
const dmChannelId = (event.dmChannelId as string) || null;
|
||||||
if (!dmChannelId || typeof dmChannelId !== 'string') {
|
const federatedCallId = (event.federatedCallId as string) || null;
|
||||||
connectionManager.sendToUser(userId, { type: 'error', message: 'dmChannelId is required' });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isDmMember(dmChannelId, userId)) {
|
if (!dmChannelId && !federatedCallId) return;
|
||||||
connectionManager.sendToUser(userId, { type: 'error', message: 'You are not a member of this DM channel' });
|
|
||||||
return;
|
// Path 1: Local room (we're the host)
|
||||||
}
|
if (dmChannelId) {
|
||||||
|
if (!isDmMember(dmChannelId, userId)) return;
|
||||||
|
|
||||||
const room = connectionManager.getRoom(dmChannelId);
|
const room = connectionManager.getRoom(dmChannelId);
|
||||||
if (!room) {
|
if (room) {
|
||||||
// Check federated call registry
|
const meta = room.metadata as DmRoomMeta;
|
||||||
const fedCall = connectionManager.getFederatedCall(dmChannelId);
|
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) {
|
if (fedCall) {
|
||||||
connectionManager.clearFederatedCall(dmChannelId);
|
connectionManager.sendToFederatedCallUsers(fedCall.federatedId, {
|
||||||
connectionManager.sendToDmMembers(dmChannelId, {
|
|
||||||
type: 'dm_call_rejected',
|
type: 'dm_call_rejected',
|
||||||
dmChannelId,
|
dmChannelId: fedCall.dmChannelId,
|
||||||
});
|
federatedCallId: fedCall.federatedId,
|
||||||
|
} as ServerEvent);
|
||||||
|
connectionManager.clearFederatedCall(fedCall.federatedId);
|
||||||
|
|
||||||
const db = getDb();
|
const db = getDb();
|
||||||
const user = db.select({ homeUserId: schema.users.homeUserId })
|
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() },
|
rejector: { homeUserId, homeInstance: getOurOrigin() },
|
||||||
},
|
},
|
||||||
}]).catch(err => console.error('[federation] Failed to send dm_call_reject:', err));
|
}]).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 {
|
function handleDmCallEnd(event: Record<string, unknown>, userId: string): void {
|
||||||
const dmChannelId = event.dmChannelId as string;
|
const dmChannelId = (event.dmChannelId as string) || null;
|
||||||
if (!dmChannelId || typeof dmChannelId !== 'string') {
|
const federatedCallId = (event.federatedCallId as string) || null;
|
||||||
connectionManager.sendToUser(userId, { type: 'error', message: 'dmChannelId is required' });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isDmMember(dmChannelId, userId)) {
|
if (!dmChannelId && !federatedCallId) return;
|
||||||
connectionManager.sendToUser(userId, { type: 'error', message: 'You are not a member of this DM channel' });
|
|
||||||
return;
|
// Path 1: Local room (we're the host)
|
||||||
}
|
if (dmChannelId) {
|
||||||
|
if (!isDmMember(dmChannelId, userId)) return;
|
||||||
|
|
||||||
const room = connectionManager.getRoom(dmChannelId);
|
const room = connectionManager.getRoom(dmChannelId);
|
||||||
if (!room) {
|
if (room) {
|
||||||
const fedCall = connectionManager.getFederatedCall(dmChannelId);
|
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) {
|
if (fedCall) {
|
||||||
connectionManager.clearFederatedCall(dmChannelId);
|
connectionManager.sendToFederatedCallUsers(fedCall.federatedId, {
|
||||||
connectionManager.sendToDmMembers(dmChannelId, {
|
|
||||||
type: 'dm_call_ended',
|
type: 'dm_call_ended',
|
||||||
dmChannelId,
|
dmChannelId: fedCall.dmChannelId,
|
||||||
});
|
federatedCallId: fedCall.federatedId,
|
||||||
|
} as ServerEvent);
|
||||||
|
connectionManager.clearFederatedCall(fedCall.federatedId);
|
||||||
|
|
||||||
const db = getDb();
|
const db = getDb();
|
||||||
const user = db.select({ homeUserId: schema.users.homeUserId })
|
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() },
|
endedBy: { homeUserId, homeInstance: getOurOrigin() },
|
||||||
},
|
},
|
||||||
}]).catch(err => console.error('[federation] Failed to send dm_call_end:', err));
|
}]).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));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user