feat: implement CORS proxy for Matrix homeservers during development and refactor call handling logic
This commit is contained in:
@@ -328,6 +328,7 @@ export class CallService {
|
||||
// Step 2: Try fetching LiveKit JWT from homeservers in priority order
|
||||
const result = await fetchLiveKitJWTFromServers(
|
||||
homeserverPriority,
|
||||
this.config.homeserverBaseUrl,
|
||||
roomId,
|
||||
this.config.userId,
|
||||
this.config.deviceId,
|
||||
|
||||
@@ -152,7 +152,8 @@ export interface CallEvent {
|
||||
* Call service configuration
|
||||
*/
|
||||
export interface CallServiceConfig {
|
||||
livekitServiceUrl: string;
|
||||
/** @deprecated No longer used - JWT fetched dynamically from multiple homeservers */
|
||||
livekitServiceUrl?: string;
|
||||
homeserverBaseUrl: string;
|
||||
accessToken: string;
|
||||
userId: string;
|
||||
|
||||
@@ -82,26 +82,12 @@ export function useCallService(): CallContextValue {
|
||||
}
|
||||
|
||||
try {
|
||||
// Get LiveKit service URL from Matrix homeserver well-known
|
||||
const wellKnown = await fetchWellKnownWithRTC(baseUrl);
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Call service init - well-known:', wellKnown);
|
||||
const livekitFocus = getLiveKitFocus(wellKnown);
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Call service init - LiveKit focus:', livekitFocus);
|
||||
const livekitServiceUrl = livekitFocus?.livekit_service_url;
|
||||
|
||||
if (!livekitServiceUrl) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Call service init - No LiveKit config in well-known, calls not supported');
|
||||
setCallSupported(false);
|
||||
setCallSupportLoading(false);
|
||||
return;
|
||||
}
|
||||
// Initialize CallService without checking user's homeserver
|
||||
// Call support will be determined per-room based on available homeservers
|
||||
console.log('Call service init - Initializing (call support determined per-room)');
|
||||
|
||||
service = new CallService(
|
||||
{
|
||||
livekitServiceUrl,
|
||||
homeserverBaseUrl: baseUrl,
|
||||
accessToken,
|
||||
userId,
|
||||
@@ -112,10 +98,10 @@ export function useCallService(): CallContextValue {
|
||||
|
||||
setCallService(service);
|
||||
setGlobalCallService(service); // Make available globally for Paarrot API
|
||||
setCallSupported(true);
|
||||
setCallSupported(true); // Global flag - actual support checked per-room
|
||||
setCallSupportLoading(false);
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Call service init - Success! Calls are supported');
|
||||
console.log('Call service init - Success!');
|
||||
|
||||
unsubscribe = service.on((event: CallEvent) => {
|
||||
// Update state for any event that might change the call state
|
||||
@@ -245,8 +231,62 @@ export function useCallService(): CallContextValue {
|
||||
* Hook to check if a call is active in a specific room
|
||||
*/
|
||||
export function useRoomCall(roomId: string) {
|
||||
const { activeCall, startCall, endCall, callSupported, callSupportLoading, callService } = useCall();
|
||||
const { activeCall, startCall, endCall, callSupported: globalCallSupported, callSupportLoading: globalCallSupportLoading, callService } = useCall();
|
||||
const mx = useMatrixClient();
|
||||
const [roomCallSupported, setRoomCallSupported] = useState(false);
|
||||
const [roomCallSupportLoading, setRoomCallSupportLoading] = useState(true);
|
||||
|
||||
// Check if calls are supported for this specific room by checking all priority homeservers
|
||||
useEffect(() => {
|
||||
const checkRoomCallSupport = async () => {
|
||||
setRoomCallSupportLoading(true);
|
||||
|
||||
const room = mx.getRoom(roomId);
|
||||
if (!room) {
|
||||
setRoomCallSupported(false);
|
||||
setRoomCallSupportLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Get homeserver priority list for this room
|
||||
const homeserverPriority = getLiveKitHomeserverPriority(
|
||||
room,
|
||||
mx.getHomeserverUrl(),
|
||||
mx.getSafeUserId()
|
||||
);
|
||||
|
||||
console.log(`Checking call support for room ${roomId}, homeserver priority:`, homeserverPriority);
|
||||
|
||||
// Check each homeserver in priority order
|
||||
for (const homeserver of homeserverPriority) {
|
||||
try {
|
||||
console.log(`Fetching well-known from ${homeserver}...`);
|
||||
const wellKnown = await fetchWellKnownWithRTC(homeserver);
|
||||
console.log(`Well-known response from ${homeserver}:`, wellKnown);
|
||||
|
||||
const livekitFocus = getLiveKitFocus(wellKnown);
|
||||
console.log(`LiveKit focus from ${homeserver}:`, livekitFocus);
|
||||
|
||||
if (livekitFocus) {
|
||||
console.log(`Call support found for room ${roomId} on homeserver: ${homeserver}`);
|
||||
setRoomCallSupported(true);
|
||||
setRoomCallSupportLoading(false);
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`Failed to check call support on ${homeserver}:`, error);
|
||||
// Continue to next homeserver
|
||||
}
|
||||
}
|
||||
|
||||
// No homeserver supports calls
|
||||
console.log(`No call support found for room ${roomId}`);
|
||||
setRoomCallSupported(false);
|
||||
setRoomCallSupportLoading(false);
|
||||
};
|
||||
|
||||
checkRoomCallSupport();
|
||||
}, [roomId, mx]);
|
||||
|
||||
const isInCall = activeCall?.roomId === roomId;
|
||||
const callState = isInCall ? activeCall.state : CallState.Idle;
|
||||
@@ -287,7 +327,7 @@ export function useRoomCall(roomId: string) {
|
||||
startVoiceCall,
|
||||
startVideoCall,
|
||||
endCall,
|
||||
callSupported,
|
||||
callSupportLoading,
|
||||
callSupported: roomCallSupported,
|
||||
callSupportLoading: roomCallSupportLoading,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -10,7 +10,9 @@ import { getActiveCallMembers } from './useRoomCallMembers';
|
||||
* Get room server from room ID (format: !roomid:server.com)
|
||||
*/
|
||||
function getRoomServer(roomId: string): string | undefined {
|
||||
return getMxIdServer(roomId);
|
||||
const server = getMxIdServer(roomId);
|
||||
console.log(`[getRoomServer] Room ID: ${roomId} -> Server: ${server}`);
|
||||
return server;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -30,13 +32,20 @@ export function getLiveKitHomeserverPriority(
|
||||
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}`);
|
||||
}
|
||||
@@ -60,32 +69,39 @@ export function getLiveKitHomeserverPriority(
|
||||
} 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;
|
||||
}
|
||||
|
||||
@@ -96,11 +112,16 @@ export function getLiveKitHomeserverPriority(
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
return rtcFoci.find((focus): focus is LiveKitFocus => focus.type === 'livekit');
|
||||
const livekitFocus = rtcFoci.find((focus): focus is LiveKitFocus => focus.type === 'livekit');
|
||||
console.log(`[getLiveKitFocus] Found LiveKit focus:`, livekitFocus);
|
||||
return livekitFocus;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -109,14 +130,28 @@ export function getLiveKitFocus(wellKnown: WellKnownWithRTC): LiveKitFocus | und
|
||||
* @returns The well-known response with RTC configuration
|
||||
*/
|
||||
export async function fetchWellKnownWithRTC(baseUrl: string): Promise<WellKnownWithRTC> {
|
||||
const wellKnownUrl = `${baseUrl}/.well-known/matrix/client`;
|
||||
// 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}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
const data = await response.json();
|
||||
console.log(`[fetchWellKnownWithRTC] Response data:`, data);
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -132,7 +167,16 @@ export async function requestOpenIdToken(
|
||||
userId: string,
|
||||
accessToken: string
|
||||
): Promise<OpenIdToken> {
|
||||
const url = `${homeserverBaseUrl}/_matrix/client/v3/user/${encodeURIComponent(userId)}/openid/request_token`;
|
||||
// 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',
|
||||
@@ -175,7 +219,21 @@ export async function fetchLiveKitJWT(
|
||||
// to create the LiveKit room name.
|
||||
const slotId = 'm.call';
|
||||
|
||||
const response = await fetch(livekitServiceUrl, {
|
||||
// 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',
|
||||
@@ -202,7 +260,8 @@ export async function fetchLiveKitJWT(
|
||||
|
||||
/**
|
||||
* Try fetching LiveKit JWT from multiple homeservers in priority order
|
||||
* @param homeservers - Array of homeserver base URLs to try
|
||||
* @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
|
||||
@@ -211,6 +270,7 @@ export async function fetchLiveKitJWT(
|
||||
*/
|
||||
export async function fetchLiveKitJWTFromServers(
|
||||
homeservers: string[],
|
||||
userHomeserver: string,
|
||||
roomId: string,
|
||||
userId: string,
|
||||
deviceId: string,
|
||||
@@ -229,8 +289,9 @@ export async function fetchLiveKitJWTFromServers(
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get OpenID token from this homeserver
|
||||
const openIdToken = await requestOpenIdToken(homeserver, userId, accessToken);
|
||||
// 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(
|
||||
|
||||
@@ -56,7 +56,7 @@ import { useRoomCreators } from '../../hooks/useRoomCreators';
|
||||
import { useRoomPermissions } from '../../hooks/useRoomPermissions';
|
||||
import { InviteUserPrompt } from '../../components/invite-user-prompt';
|
||||
import { CallParticipantsIndicator } from './CallParticipantsIndicator';
|
||||
import { useCall } from '../call/useCall';
|
||||
import { useCall, useRoomCall } from '../call/useCall';
|
||||
import { CallType } from '../call/types';
|
||||
import { roomToParentsAtom } from '../../state/room/roomToParents';
|
||||
import { StateEvent } from '../../../types/matrix/room';
|
||||
@@ -75,7 +75,8 @@ const RoomNavItemMenu = forwardRef<HTMLDivElement, RoomNavItemMenuProps>(
|
||||
const unread = useRoomUnread(room.roomId, roomToUnreadAtom);
|
||||
const powerLevels = usePowerLevels(room);
|
||||
const creators = useRoomCreators(room);
|
||||
const { startCall, endCall, activeCall, callSupported } = useCall();
|
||||
const { startCall: startCallGlobal, endCall, activeCall } = useCall();
|
||||
const { callSupported } = useRoomCall(room.roomId);
|
||||
const markAsRead = useMarkAsRead(mx);
|
||||
|
||||
const permissions = useRoomPermissions(creators, powerLevels);
|
||||
@@ -119,7 +120,7 @@ const RoomNavItemMenu = forwardRef<HTMLDivElement, RoomNavItemMenuProps>(
|
||||
if (activeCall) {
|
||||
await endCall();
|
||||
}
|
||||
await startCall(room.roomId, CallType.Voice);
|
||||
await startCallGlobal(room.roomId, CallType.Voice);
|
||||
requestClose();
|
||||
} catch (error) {
|
||||
console.error('Failed to join voice call:', error);
|
||||
|
||||
@@ -450,12 +450,7 @@ function RoomCallButtons({ roomId }: RoomCallButtonsProps) {
|
||||
{isConnecting ? (
|
||||
<Spinner size="100" variant="Secondary" />
|
||||
) : (
|
||||
<Box direction="Row" gap="100" alignItems="Center">
|
||||
<Icon size="400" src={Icons.Phone} filled={isActive && activeCall?.callType === CallType.Voice} />
|
||||
{isUsingRemoteHomeserver && (
|
||||
<Icon size="50" src={Icons.HashGlobe} style={{ opacity: 0.7 }} />
|
||||
)}
|
||||
</Box>
|
||||
<Icon size="400" src={Icons.Phone} filled={isActive && activeCall?.callType === CallType.Voice} />
|
||||
)}
|
||||
</IconButton>
|
||||
)}
|
||||
@@ -481,12 +476,7 @@ function RoomCallButtons({ roomId }: RoomCallButtonsProps) {
|
||||
{isConnecting ? (
|
||||
<Spinner size="100" variant="Secondary" />
|
||||
) : (
|
||||
<Box direction="Row" gap="100" alignItems="Center">
|
||||
<Icon size="400" src={Icons.VideoCamera} filled={isActive && activeCall?.callType === CallType.Video} />
|
||||
{isUsingRemoteHomeserver && (
|
||||
<Icon size="50" src={Icons.HashGlobe} style={{ opacity: 0.7 }} />
|
||||
)}
|
||||
</Box>
|
||||
<Icon size="400" src={Icons.VideoCamera} filled={isActive && activeCall?.callType === CallType.Video} />
|
||||
)}
|
||||
</IconButton>
|
||||
)}
|
||||
|
||||
@@ -23,7 +23,7 @@ const DOMAIN_REGEX = /\b(?:[a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}\b/;
|
||||
|
||||
export const isServerName = (serverName: string): boolean => DOMAIN_REGEX.test(serverName);
|
||||
|
||||
const matchMxId = (id: string): RegExpMatchArray | null => id.match(/^([@$+#])([^\s:]+):(\S+)$/);
|
||||
const matchMxId = (id: string): RegExpMatchArray | null => id.match(/^([@!$+#])([^\s:]+):(\S+)$/);
|
||||
|
||||
const validMxId = (id: string): boolean => !!matchMxId(id);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user