import { useCallback } from 'react'; import { Room } from 'matrix-js-sdk'; import { useMatrixClient } from '../../hooks/useMatrixClient'; import { LiveKitFocus, LiveKitJWTResponse, OpenIdToken, WellKnownWithRTC } from './types'; import { getMxIdServer } from '../../utils/matrix'; import { guessDmRoomUserId } from '../../utils/matrix'; import { getActiveCallMembers } from './useRoomCallMembers'; /** * Get room server from room ID (format: !roomid:server.com) */ function getRoomServer(roomId: string): string | undefined { const server = getMxIdServer(roomId); console.log(`[getRoomServer] Room ID: ${roomId} -> Server: ${server}`); return server; } /** * Determine which homeservers to try for LiveKit JWT, in priority order * Prioritizes servers where active call members are located when joining existing calls * @param room - Room to start/join call in * @param userHomeserver - User's homeserver * @param myUserId - Current user's Matrix ID * @returns Array of homeserver base URLs to try in order */ export function getLiveKitHomeserverPriority( room: Room, userHomeserver: string, myUserId: string ): string[] { const servers: string[] = []; const memberCount = room.getMembers().length; const isDM = memberCount <= 2; console.log(`[getLiveKitHomeserverPriority] Room: ${room.roomId}`); console.log(`[getLiveKitHomeserverPriority] Member count: ${memberCount}, isDM: ${isDM}`); console.log(`[getLiveKitHomeserverPriority] User homeserver: ${userHomeserver}`); // Check for active call members const activeCallMembers = getActiveCallMembers(room); const hasActiveCall = activeCallMembers.length > 0; console.log(`[getLiveKitHomeserverPriority] Active call members: ${activeCallMembers.length}, hasActiveCall: ${hasActiveCall}`); if (hasActiveCall) { // Joining existing call - prioritize room's homeserver (where call is hosted) const roomServer = getRoomServer(room.roomId); console.log(`[getLiveKitHomeserverPriority] Room server (active call): ${roomServer}`); if (roomServer) { servers.push(`https://${roomServer}`); } // Add user's homeserver as fallback const userDomain = userHomeserver.replace('https://', '').replace('http://', ''); if (!roomServer || roomServer !== userDomain) { servers.push(userHomeserver); } // For DMs, also try other member's homeserver if (isDM) { const otherUserId = guessDmRoomUserId(room, myUserId); if (otherUserId && otherUserId !== myUserId) { const otherServer = getMxIdServer(otherUserId); if (otherServer && otherServer !== userDomain && otherServer !== roomServer) { servers.push(`https://${otherServer}`); } } } } else { // Starting new call - use original priority logic if (isDM) { console.log(`[getLiveKitHomeserverPriority] DM path - user homeserver first`); // For DMs: user's homeserver first, then other member's homeserver servers.push(userHomeserver); const otherUserId = guessDmRoomUserId(room, myUserId); console.log(`[getLiveKitHomeserverPriority] Other user ID: ${otherUserId}`); if (otherUserId && otherUserId !== myUserId) { const otherServer = getMxIdServer(otherUserId); console.log(`[getLiveKitHomeserverPriority] Other user server: ${otherServer}`); const userDomain = userHomeserver.replace('https://', '').replace('http://', ''); if (otherServer && otherServer !== userDomain) { servers.push(`https://${otherServer}`); } } } else { console.log(`[getLiveKitHomeserverPriority] Channel path - room homeserver first`); // For channels: room's homeserver first, then user's homeserver const roomServer = getRoomServer(room.roomId); console.log(`[getLiveKitHomeserverPriority] Room server from ID: ${roomServer}`); if (roomServer) { servers.push(`https://${roomServer}`); } // Fallback to user's homeserver if different const userDomain = userHomeserver.replace('https://', '').replace('http://', ''); console.log(`[getLiveKitHomeserverPriority] User domain: ${userDomain}, room server: ${roomServer}`); if (!roomServer || roomServer !== userDomain) { servers.push(userHomeserver); } } } console.log(`[getLiveKitHomeserverPriority] Final server list:`, servers); return servers; } /** * Fetches the LiveKit focus configuration from well-known data * @param wellKnown - The well-known response that may contain RTC foci * @returns The LiveKit focus configuration if available */ export function getLiveKitFocus(wellKnown: WellKnownWithRTC): LiveKitFocus | undefined { const rtcFoci = wellKnown['org.matrix.msc4143.rtc_foci']; console.log(`[getLiveKitFocus] rtcFoci from well-known:`, rtcFoci); if (!rtcFoci || !Array.isArray(rtcFoci)) { console.log(`[getLiveKitFocus] No rtc_foci found or not an array`); return undefined; } const livekitFocus = rtcFoci.find((focus): focus is LiveKitFocus => focus.type === 'livekit'); console.log(`[getLiveKitFocus] Found LiveKit focus:`, livekitFocus); return livekitFocus; } /** * Fetches the well-known client configuration from a homeserver * @param baseUrl - The homeserver base URL * @returns The well-known response with RTC configuration */ export async function fetchWellKnownWithRTC(baseUrl: string): Promise { // In development mode (localhost), use CORS proxy to avoid CORS issues const isDev = window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1'; let wellKnownUrl: string; if (isDev) { // Extract hostname from baseUrl for proxy const hostname = baseUrl.replace('https://', '').replace('http://', '').replace(/\/$/, ''); wellKnownUrl = `/_matrix-cors-proxy/${hostname}/.well-known/matrix/client`; } else { wellKnownUrl = `${baseUrl}/.well-known/matrix/client`; } console.log(`[fetchWellKnownWithRTC] Fetching from: ${wellKnownUrl}`); const response = await fetch(wellKnownUrl); if (!response.ok) { throw new Error(`Failed to fetch well-known: ${response.status}`); } const data = await response.json(); console.log(`[fetchWellKnownWithRTC] Response data:`, data); return data; } /** * Requests an OpenID token from the Matrix homeserver * This token can be used to authenticate with third-party services * @param homeserverBaseUrl - The Matrix homeserver base URL * @param userId - The user's Matrix ID * @param accessToken - The Matrix access token * @returns The OpenID token response */ export async function requestOpenIdToken( homeserverBaseUrl: string, userId: string, accessToken: string ): Promise { // In development mode (localhost), use CORS proxy to avoid CORS issues const isDev = window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1'; let url: string; if (isDev) { const hostname = homeserverBaseUrl.replace('https://', '').replace('http://', '').replace(/\/$/, ''); url = `/_matrix-cors-proxy/${hostname}/_matrix/client/v3/user/${encodeURIComponent(userId)}/openid/request_token`; } else { url = `${homeserverBaseUrl}/_matrix/client/v3/user/${encodeURIComponent(userId)}/openid/request_token`; } const response = await fetch(url, { method: 'POST', headers: { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json', }, body: JSON.stringify({}), }); if (!response.ok) { const errorText = await response.text(); throw new Error(`Failed to request OpenID token: ${response.status} - ${errorText}`); } return response.json(); } /** * Fetches a LiveKit JWT for joining a call using OpenID token authentication * @param livekitServiceUrl - The LiveKit service URL from Matrix homeserver well-known * @param roomId - The Matrix room ID to join * @param userId - The Matrix user ID * @param deviceId - The Matrix device ID * @param openIdToken - The OpenID token from the Matrix homeserver * @returns The LiveKit JWT and server URL */ export async function fetchLiveKitJWT( livekitServiceUrl: string, roomId: string, userId: string, deviceId: string, openIdToken: OpenIdToken ): Promise { // Generate a unique member ID for this participant (used for identity, not room assignment) const memberId = `${deviceId}_${Date.now()}`; // slot_id must be constant so all participants in the same Matrix room // get assigned to the same LiveKit room. The JWT service hashes room_id + slot_id // to create the LiveKit room name. const slotId = 'm.call'; // In development mode (localhost), use CORS proxy to avoid CORS issues const isDev = window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1'; let url: string; if (isDev) { // Extract hostname and path from livekitServiceUrl const urlObj = new URL(livekitServiceUrl); const hostname = urlObj.hostname; const pathname = urlObj.pathname; url = `/_matrix-cors-proxy/${hostname}${pathname}`; } else { url = livekitServiceUrl; } const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ openid_token: openIdToken, room_id: roomId, slot_id: slotId, member: { id: memberId, claimed_user_id: userId, claimed_device_id: deviceId, }, }), }); if (!response.ok) { const errorText = await response.text(); throw new Error(`Failed to fetch LiveKit JWT: ${response.status} - ${errorText}`); } return response.json(); } /** * Try fetching LiveKit JWT from multiple homeservers in priority order * @param homeservers - Array of homeserver base URLs to check for LiveKit config * @param userHomeserver - The user's own homeserver URL (for OpenID token) * @param roomId - The Matrix room ID * @param userId - The Matrix user ID * @param deviceId - The Matrix device ID * @param accessToken - The Matrix access token * @returns LiveKit JWT response and the homeserver that provided it, or null */ export async function fetchLiveKitJWTFromServers( homeservers: string[], userHomeserver: string, roomId: string, userId: string, deviceId: string, accessToken: string ): Promise<{ jwt: LiveKitJWTResponse; homeserver: string } | null> { for (const homeserver of homeservers) { try { console.log(`Trying LiveKit JWT from homeserver: ${homeserver}`); // Fetch well-known from this homeserver const wellKnown = await fetchWellKnownWithRTC(homeserver); const livekitFocus = getLiveKitFocus(wellKnown); if (!livekitFocus) { console.log(`No LiveKit config on ${homeserver}, trying next...`); continue; } // Always get OpenID token from user's own homeserver // (Can't use access token on remote homeserver) const openIdToken = await requestOpenIdToken(userHomeserver, userId, accessToken); // Exchange for LiveKit JWT const jwt = await fetchLiveKitJWT( livekitFocus.livekit_service_url, roomId, userId, deviceId, openIdToken ); console.log(`Successfully got LiveKit JWT from ${homeserver}`); return { jwt, homeserver }; } catch (error) { console.warn(`Failed to get LiveKit JWT from ${homeserver}:`, error); // Continue to next homeserver } } console.error('Failed to get LiveKit JWT from any homeserver'); return null; } /** * Hook to get RTC configuration functions * Provides utilities for fetching well-known data and LiveKit JWTs */ export function useRtcConfig() { const mx = useMatrixClient(); const getWellKnown = useCallback(async (): Promise => { const baseUrl = mx.getHomeserverUrl(); return fetchWellKnownWithRTC(baseUrl); }, [mx]); const getLiveKitJWT = useCallback( async (roomId: string): Promise => { const baseUrl = mx.getHomeserverUrl(); const accessToken = mx.getAccessToken(); const deviceId = mx.getDeviceId(); const userId = mx.getSafeUserId(); if (!accessToken || !deviceId) { throw new Error('Missing access token or device ID'); } const wellKnown = await fetchWellKnownWithRTC(baseUrl); const livekitFocus = getLiveKitFocus(wellKnown); if (!livekitFocus) { return null; } // Get OpenID token from homeserver const openIdToken = await requestOpenIdToken(baseUrl, userId, accessToken); // Exchange for LiveKit JWT return fetchLiveKitJWT(livekitFocus.livekit_service_url, roomId, userId, deviceId, openIdToken); }, [mx] ); const isCallSupported = useCallback(async (): Promise => { try { const baseUrl = mx.getHomeserverUrl(); const wellKnown = await fetchWellKnownWithRTC(baseUrl); return getLiveKitFocus(wellKnown) !== undefined; } catch { return false; } }, [mx]); return { getWellKnown, getLiveKitJWT, isCallSupported, }; }