feat(federation): handleDmCallAccept/Reject/End with federated call support (FED-009)

This commit is contained in:
Jannis Braun
2026-03-31 23:30:27 +02:00
parent f2affb0f1d
commit 2f47d32136
+203 -2
View File
@@ -1432,6 +1432,40 @@ function handleDmCallAccept(event: Record<string, unknown>, userId: string, ws:
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 fedCall = connectionManager.getFederatedCall(dmChannelId);
if (fedCall) {
// Transition local state
connectionManager.activateFederatedCall(dmChannelId);
// Broadcast locally
connectionManager.sendToDmMembers(dmChannelId, {
type: 'dm_call_accepted',
dmChannelId,
});
// Send accept to host (fire-and-forget)
const db = getDb();
const user = db.select({ homeUserId: schema.users.homeUserId })
.from(schema.users)
.where(eq(schema.users.id, userId))
.get();
const homeUserId = user?.homeUserId || userId;
sendCallRelay(fedCall.federatedCallHost, [{
eventType: 'dm_call_accept',
messageId: generateSnowflake(),
encryptionVersion: 0,
timestamp: Date.now(),
federatedId: fedCall.federatedId,
call: {
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' }); connectionManager.sendToUser(userId, { type: 'error', message: 'No active call in this DM channel' });
return; return;
} }
@@ -1487,6 +1521,10 @@ function handleDmCallAccept(event: Record<string, unknown>, userId: string, ws:
userId, userId,
action: 'join', 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 {
@@ -1502,7 +1540,38 @@ function handleDmCallReject(event: Record<string, unknown>, userId: string): voi
} }
const room = connectionManager.getRoom(dmChannelId); const room = connectionManager.getRoom(dmChannelId);
if (!room) return; // No active call, silently ignore if (!room) {
// Check federated call registry
const fedCall = connectionManager.getFederatedCall(dmChannelId);
if (fedCall) {
connectionManager.clearFederatedCall(dmChannelId);
connectionManager.sendToDmMembers(dmChannelId, {
type: 'dm_call_rejected',
dmChannelId,
});
const db = getDb();
const user = db.select({ homeUserId: schema.users.homeUserId })
.from(schema.users)
.where(eq(schema.users.id, userId))
.get();
const homeUserId = user?.homeUserId || userId;
sendCallRelay(fedCall.federatedCallHost, [{
eventType: 'dm_call_reject',
messageId: generateSnowflake(),
encryptionVersion: 0,
timestamp: Date.now(),
federatedId: fedCall.federatedId,
call: {
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 // Extract caller ID before destroying the room
const meta = room.metadata as DmRoomMeta; const meta = room.metadata as DmRoomMeta;
@@ -1518,6 +1587,10 @@ function handleDmCallReject(event: Record<string, unknown>, userId: string): voi
type: 'dm_call_rejected', type: 'dm_call_rejected',
dmChannelId, 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 {
@@ -1533,7 +1606,37 @@ function handleDmCallEnd(event: Record<string, unknown>, userId: string): void {
} }
const room = connectionManager.getRoom(dmChannelId); const room = connectionManager.getRoom(dmChannelId);
if (!room) return; // No active call, silently ignore if (!room) {
const fedCall = connectionManager.getFederatedCall(dmChannelId);
if (fedCall) {
connectionManager.clearFederatedCall(dmChannelId);
connectionManager.sendToDmMembers(dmChannelId, {
type: 'dm_call_ended',
dmChannelId,
});
const db = getDb();
const user = db.select({ homeUserId: schema.users.homeUserId })
.from(schema.users)
.where(eq(schema.users.id, userId))
.get();
const homeUserId = user?.homeUserId || userId;
sendCallRelay(fedCall.federatedCallHost, [{
eventType: 'dm_call_end',
messageId: generateSnowflake(),
encryptionVersion: 0,
timestamp: Date.now(),
federatedId: fedCall.federatedId,
call: {
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) // Clear voice ws binding for the caller (may not be in participants if still ringing)
const meta = room.metadata as DmRoomMeta; const meta = room.metadata as DmRoomMeta;
@@ -1553,6 +1656,10 @@ function handleDmCallEnd(event: Record<string, unknown>, userId: string): void {
type: 'dm_call_ended', type: 'dm_call_ended',
dmChannelId, dmChannelId,
}); });
// Federation: notify remote instances the call ended
sendFederatedCallEnd(dmChannelId, userId)
.catch(err => console.error('[federation] sendFederatedCallEnd error:', err));
} }
/** /**
@@ -1671,6 +1778,100 @@ async function sendFederatedCallStart(
} }
} }
async function sendFederatedCallAccept(dmChannelId: string, acceptorUserId: string): Promise<void> {
const db = getDb();
const channel = db.select({ federatedId: schema.dmChannels.federatedId })
.from(schema.dmChannels)
.where(eq(schema.dmChannels.id, dmChannelId))
.get();
if (!channel?.federatedId) return;
const members = db.select({ homeInstance: schema.users.homeInstance })
.from(schema.dmMembers)
.innerJoin(schema.users, eq(schema.dmMembers.userId, schema.users.id))
.where(eq(schema.dmMembers.dmChannelId, dmChannelId))
.all();
const ourOrigin = getOurOrigin();
const targets = new Set<string>();
for (const m of members) {
if (m.homeInstance) {
const normalized = m.homeInstance.startsWith('http') ? m.homeInstance : `https://${m.homeInstance}`;
if (normalized !== ourOrigin) targets.add(normalized);
}
}
if (targets.size === 0) return;
const user = db.select({ homeUserId: schema.users.homeUserId })
.from(schema.users)
.where(eq(schema.users.id, acceptorUserId))
.get();
const homeUserId = user?.homeUserId || acceptorUserId;
const event = {
eventType: 'dm_call_accept' as const,
messageId: generateSnowflake(),
encryptionVersion: 0 as const,
timestamp: Date.now(),
federatedId: channel.federatedId,
call: {
acceptor: { homeUserId, homeInstance: ourOrigin },
},
};
await Promise.all(
Array.from(targets).map(origin =>
sendCallRelay(origin, [event]).catch(err =>
console.error(`[federation] Failed to send dm_call_accept to ${origin}:`, err)
)
)
);
}
async function sendFederatedCallEnd(dmChannelId: string, endedByUserId: string): Promise<void> {
const db = getDb();
const channel = db.select({ federatedId: schema.dmChannels.federatedId })
.from(schema.dmChannels)
.where(eq(schema.dmChannels.id, dmChannelId))
.get();
if (!channel?.federatedId) return;
const members = db.select({ homeInstance: schema.users.homeInstance })
.from(schema.dmMembers)
.innerJoin(schema.users, eq(schema.dmMembers.userId, schema.users.id))
.where(eq(schema.dmMembers.dmChannelId, dmChannelId))
.all();
const ourOrigin = getOurOrigin();
const targets = new Set<string>();
for (const m of members) {
if (m.homeInstance) {
const normalized = m.homeInstance.startsWith('http') ? m.homeInstance : `https://${m.homeInstance}`;
if (normalized !== ourOrigin) targets.add(normalized);
}
}
if (targets.size === 0) return;
const event = {
eventType: 'dm_call_end' as const,
messageId: generateSnowflake(),
encryptionVersion: 0 as const,
timestamp: Date.now(),
federatedId: channel.federatedId,
call: {
endedBy: { homeUserId: endedByUserId, homeInstance: ourOrigin },
},
};
await Promise.all(
Array.from(targets).map(origin =>
sendCallRelay(origin, [event]).catch(err =>
console.error(`[federation] Failed to send dm_call_end to ${origin}:`, err)
)
)
);
}
// ─── Voice Moderation Handlers ────────────────────────────────────────────── // ─── Voice Moderation Handlers ──────────────────────────────────────────────
function handleVoiceSpaceMute(event: Record<string, unknown>, userId: string): void { function handleVoiceSpaceMute(event: Record<string, unknown>, userId: string): void {