Files
cinny/src/app/paarrot-api.ts

443 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;
/**
* Track if the API has been initialized to prevent duplicate listeners
*/
let isInitialized = false;
/**
* 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;
}
// Prevent duplicate registrations
if (isInitialized) {
console.log('Paarrot API: Already initialized, skipping');
return;
}
isInitialized = true;
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.trim(),
};
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 };