431 lines
11 KiB
TypeScript
431 lines
11 KiB
TypeScript
/**
|
|
* Paarrot API Handler
|
|
*
|
|
* This module handles API actions from the Electron API server.
|
|
* It receives actions via IPC and executes them on the Matrix client.
|
|
*
|
|
* To use this, import and call initPaarrotAPI() early in your app initialization.
|
|
*/
|
|
|
|
import { MatrixClient } from 'matrix-js-sdk';
|
|
import { getHomeRoomPath, getDirectRoomPath, getSpaceRoomPath } from './pages/pathUtils';
|
|
import { getCanonicalAliasOrRoomId, isRoomId } from './utils/matrix';
|
|
import { getCallService } from './features/call/useCall';
|
|
|
|
/**
|
|
* Global reference to the router navigate function
|
|
* This is set by the router and used to navigate programmatically
|
|
*/
|
|
let navigateFunction: ((path: string) => void) | null = null;
|
|
|
|
/**
|
|
* Set the navigate function for programmatic navigation
|
|
* Call this from your router setup
|
|
*/
|
|
export function setPaarrotNavigate(navigate: (path: string) => void) {
|
|
navigateFunction = navigate;
|
|
console.log('Paarrot API: Navigate function registered');
|
|
}
|
|
|
|
/**
|
|
* Initialize the Paarrot API handler
|
|
* This should be called once during app startup
|
|
*/
|
|
export function initPaarrotAPI(matrixClient: MatrixClient) {
|
|
// Check if we're running in Electron
|
|
if (!(window as any).electron?.api?.onAction) {
|
|
console.log('Paarrot API: Not running in Electron, skipping API initialization');
|
|
return;
|
|
}
|
|
|
|
console.log('Paarrot API: Initializing API handler');
|
|
|
|
// Listen for API actions from the Electron API server
|
|
(window as any).electron.api.onAction(async (action: {
|
|
action: string;
|
|
params: any;
|
|
responseChannel: string;
|
|
}) => {
|
|
console.log('Paarrot API: Received action:', action.action, action.params);
|
|
|
|
try {
|
|
let result;
|
|
|
|
switch (action.action) {
|
|
case 'get-status':
|
|
result = await getStatus(matrixClient);
|
|
break;
|
|
|
|
case 'toggle-mute':
|
|
result = await toggleMute(matrixClient);
|
|
break;
|
|
|
|
case 'set-mute':
|
|
result = await setMute(matrixClient, action.params.muted);
|
|
break;
|
|
|
|
case 'toggle-deafen':
|
|
result = await toggleDeafen(matrixClient);
|
|
break;
|
|
|
|
case 'set-deafen':
|
|
result = await setDeafen(matrixClient, action.params.deafened);
|
|
break;
|
|
|
|
case 'change-channel':
|
|
result = await changeChannel(matrixClient, action.params.roomId);
|
|
break;
|
|
|
|
case 'get-channels':
|
|
result = await getChannels(matrixClient);
|
|
break;
|
|
|
|
case 'send-message':
|
|
result = await sendMessage(matrixClient, action.params.roomId, action.params.message);
|
|
break;
|
|
|
|
case 'send-message-current':
|
|
result = await sendMessageCurrent(matrixClient, action.params.message);
|
|
break;
|
|
|
|
case 'get-current-room':
|
|
result = await getCurrentRoom(matrixClient);
|
|
break;
|
|
|
|
default:
|
|
throw new Error(`Unknown action: ${action.action}`);
|
|
}
|
|
|
|
// Send success response back
|
|
(window as any).electron.api.sendResponse(action.responseChannel, {
|
|
success: true,
|
|
data: result,
|
|
});
|
|
|
|
console.log('Paarrot API: Action completed successfully:', action.action);
|
|
} catch (error: any) {
|
|
console.error('Paarrot API: Action failed:', action.action, error);
|
|
|
|
// Send error response back
|
|
(window as any).electron.api.sendResponse(action.responseChannel, {
|
|
success: false,
|
|
error: error.message || 'Unknown error',
|
|
});
|
|
}
|
|
});
|
|
|
|
console.log('Paarrot API: Handler initialized');
|
|
}
|
|
|
|
/**
|
|
* Get current app status
|
|
*/
|
|
async function getStatus(matrixClient: MatrixClient) {
|
|
const currentRoom = getCurrentRoomId();
|
|
|
|
// Get actual mute/deafen state from CallService if available
|
|
const callService = getCallService();
|
|
const activeCall = callService?.getActiveCall();
|
|
|
|
return {
|
|
muted: activeCall?.isMuted ?? false,
|
|
deafened: activeCall?.isDeafened ?? false,
|
|
currentRoom,
|
|
connected: matrixClient.isInitialSyncComplete() || false,
|
|
userId: matrixClient.getUserId(),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Toggle microphone mute
|
|
*/
|
|
async function toggleMute(matrixClient: MatrixClient) {
|
|
const callService = getCallService();
|
|
|
|
if (!callService) {
|
|
return {
|
|
muted: false,
|
|
message: 'CallService not initialized - no active call',
|
|
};
|
|
}
|
|
|
|
const activeCall = callService.getActiveCall();
|
|
if (!activeCall) {
|
|
return {
|
|
muted: false,
|
|
message: 'No active call to mute/unmute',
|
|
};
|
|
}
|
|
|
|
// Toggle mute state
|
|
const newMuteState = callService.toggleMute();
|
|
|
|
return {
|
|
muted: newMuteState,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Set microphone mute state
|
|
*/
|
|
async function setMute(matrixClient: MatrixClient, muted: boolean) {
|
|
const callService = getCallService();
|
|
|
|
if (!callService) {
|
|
return {
|
|
muted,
|
|
message: 'CallService not initialized - no active call',
|
|
};
|
|
}
|
|
|
|
const activeCall = callService.getActiveCall();
|
|
if (!activeCall) {
|
|
return {
|
|
muted,
|
|
message: 'No active call to mute/unmute',
|
|
};
|
|
}
|
|
|
|
// Set mute to desired state if it's different
|
|
if (activeCall.isMuted !== muted) {
|
|
callService.toggleMute();
|
|
}
|
|
|
|
return {
|
|
muted,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Toggle deafen (mute speakers)
|
|
*/
|
|
async function toggleDeafen(matrixClient: MatrixClient) {
|
|
const callService = getCallService();
|
|
|
|
if (!callService) {
|
|
return {
|
|
deafened: false,
|
|
message: 'CallService not initialized - no active call',
|
|
};
|
|
}
|
|
|
|
const activeCall = callService.getActiveCall();
|
|
if (!activeCall) {
|
|
return {
|
|
deafened: false,
|
|
message: 'No active call to deafen/undeafen',
|
|
};
|
|
}
|
|
|
|
// Toggle deafen state
|
|
const newDeafenState = callService.toggleDeafen();
|
|
|
|
return {
|
|
deafened: newDeafenState,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Set deafen state
|
|
*/
|
|
async function setDeafen(matrixClient: MatrixClient, deafened: boolean) {
|
|
const callService = getCallService();
|
|
|
|
if (!callService) {
|
|
return {
|
|
deafened,
|
|
message: 'CallService not initialized - no active call',
|
|
};
|
|
}
|
|
|
|
const activeCall = callService.getActiveCall();
|
|
if (!activeCall) {
|
|
return {
|
|
deafened,
|
|
message: 'No active call to deafen/undeafen',
|
|
};
|
|
}
|
|
|
|
// Set deafen to desired state if it's different
|
|
if (activeCall.isDeafened !== deafened) {
|
|
callService.toggleDeafen();
|
|
}
|
|
|
|
return {
|
|
deafened,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Change to a different room/channel
|
|
*/
|
|
async function changeChannel(matrixClient: any, roomId: string) {
|
|
const room = matrixClient?.getRoom(roomId);
|
|
if (!room) {
|
|
throw new Error(`Room not found: ${roomId}`);
|
|
}
|
|
|
|
if (!navigateFunction) {
|
|
throw new Error('Navigate function not registered. Call setPaarrotNavigate from your router setup.');
|
|
}
|
|
|
|
// Determine the appropriate path based on room type
|
|
const roomIdOrAlias = getCanonicalAliasOrRoomId(matrixClient, roomId);
|
|
let path: string;
|
|
|
|
// Check if it's a direct message
|
|
const isDirect = room.guessDMUserId() !== null;
|
|
|
|
if (isDirect) {
|
|
// Navigate to direct message
|
|
path = getDirectRoomPath(roomIdOrAlias);
|
|
} else if (room.isSpaceRoom()) {
|
|
// Navigate to space
|
|
path = getSpaceRoomPath(roomIdOrAlias, roomIdOrAlias);
|
|
} else {
|
|
// Navigate to regular room in home
|
|
path = getHomeRoomPath(roomIdOrAlias);
|
|
}
|
|
|
|
// Navigate to the room
|
|
navigateFunction(path);
|
|
|
|
return {
|
|
roomId,
|
|
roomName: room.name || 'Unnamed Room',
|
|
path,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Get list of rooms/channels
|
|
*/
|
|
async function getChannels(matrixClient: any) {
|
|
const rooms = matrixClient?.getRooms() || [];
|
|
|
|
return rooms
|
|
.filter((room: any) => !room.isSpaceRoom())
|
|
.map((room: any) => {
|
|
// Extract server domain from roomId (!localpart:domain.tld)
|
|
const serverMatch = room.roomId.match(/:(.+)$/);
|
|
const server = serverMatch ? serverMatch[1] : null;
|
|
|
|
return {
|
|
roomId: room.roomId,
|
|
name: room.name || 'Unnamed Room',
|
|
server: server,
|
|
isDirect: room.getMyMembership() === 'invite' ? false : room.guessDMUserId() !== null,
|
|
avatar: room.getMxcAvatarUrl() || null,
|
|
};
|
|
})
|
|
.sort((a: any, b: any) => a.name.localeCompare(b.name));
|
|
}
|
|
|
|
/**
|
|
* Send a message to a specific room
|
|
*/
|
|
async function sendMessage(matrixClient: any, roomId: string, message: string) {
|
|
const room = matrixClient?.getRoom(roomId);
|
|
if (!room) {
|
|
throw new Error(`Room not found: ${roomId}`);
|
|
}
|
|
|
|
const content = {
|
|
msgtype: 'm.text',
|
|
body: message,
|
|
};
|
|
|
|
const result = await matrixClient.sendMessage(roomId, content);
|
|
|
|
return {
|
|
eventId: result.event_id,
|
|
roomId,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Send a message to the current room
|
|
*/
|
|
async function sendMessageCurrent(matrixClient: any, message: string) {
|
|
const currentRoomId = getCurrentRoomId();
|
|
if (!currentRoomId) {
|
|
throw new Error('No room currently active');
|
|
}
|
|
|
|
return sendMessage(matrixClient, currentRoomId, message);
|
|
}
|
|
|
|
/**
|
|
* Get information about the current room
|
|
*/
|
|
async function getCurrentRoom(matrixClient: any) {
|
|
const currentRoomId = getCurrentRoomId();
|
|
if (!currentRoomId) {
|
|
throw new Error('No room currently active');
|
|
}
|
|
|
|
const room = matrixClient?.getRoom(currentRoomId);
|
|
if (!room) {
|
|
throw new Error(`Current room not found: ${currentRoomId}`);
|
|
}
|
|
|
|
// Extract server domain from roomId (!localpart:domain.tld)
|
|
const serverMatch = room.roomId.match(/:(.+)$/);
|
|
const server = serverMatch ? serverMatch[1] : null;
|
|
|
|
return {
|
|
roomId: room.roomId,
|
|
name: room.name || 'Unnamed Room',
|
|
server: server,
|
|
avatar: room.getMxcAvatarUrl() || null,
|
|
isDirect: room.guessDMUserId() !== null,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Get the currently active room ID
|
|
* Extracts room ID from the current URL pathname
|
|
*/
|
|
function getCurrentRoomId(): string | null {
|
|
try {
|
|
// Get the pathname from URL
|
|
// Support both normal routing and hash routing
|
|
let pathname = window.location.pathname;
|
|
|
|
// If using hash router, parse from hash
|
|
if (window.location.hash) {
|
|
const hashMatch = window.location.hash.match(/#(.+)/);
|
|
if (hashMatch) {
|
|
pathname = hashMatch[1];
|
|
}
|
|
}
|
|
|
|
// URL patterns:
|
|
// /home/:roomIdOrAlias/:eventId?/
|
|
// /direct/:roomIdOrAlias/:eventId?/
|
|
// /:spaceIdOrAlias/:roomIdOrAlias/:eventId?/
|
|
|
|
// Try to match room patterns
|
|
const patterns = [
|
|
/^\/home\/([^/]+)/, // /home/:roomIdOrAlias
|
|
/^\/direct\/([^/]+)/, // /direct/:roomIdOrAlias
|
|
/^\/[^/]+\/([^/]+)/, // /:spaceIdOrAlias/:roomIdOrAlias (space rooms)
|
|
];
|
|
|
|
for (const pattern of patterns) {
|
|
const match = pathname.match(pattern);
|
|
if (match && match[1]) {
|
|
// Decode URI component in case the room ID was encoded
|
|
return decodeURIComponent(match[1]);
|
|
}
|
|
}
|
|
|
|
return null;
|
|
} catch (error) {
|
|
console.error('Paarrot API: Error getting current room ID:', error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export default { initPaarrotAPI };
|