testapp: add kick participant functionality and fix view recordings link

- Enhanced the web component to include a kick participant feature.
- Updated the UI to allow moderators to input participant identity for kicking.
- Modified the room controller to handle the new kick participant request.
- Refactored code for better readability and maintainability across various services and controllers.
- Updated the index.mustache and room.mustache templates to reflect changes in functionality.
This commit is contained in:
juancarmore 2025-07-08 23:57:04 +02:00
parent 0a028226f3
commit 2b7466c6e3
10 changed files with 457 additions and 471 deletions

View File

@ -1,9 +1,10 @@
const socket = (window as any).io();
let meet: {
endMeeting: () => void;
leaveRoom: () => void;
on: (event: string, callback: (event: CustomEvent<any>) => void) => void;
endMeeting: () => void;
leaveRoom: () => void;
kickParticipant: (participantIdentity: string) => void;
on: (event: string, callback: (event: CustomEvent<any>) => void) => void;
};
let roomId: string | undefined;
let showAllWebhooksCheckbox: HTMLInputElement | null;
@ -12,238 +13,243 @@ let showAllWebhooksCheckbox: HTMLInputElement | null;
* Add a component event to the events log
*/
const addEventToLog = (eventType: string, eventMessage: string): void => {
const eventsList = document.getElementById('events-list');
if (eventsList) {
const li = document.createElement('li');
li.className = `event-${eventType}`;
li.textContent = `[ ${eventType} ] : ${eventMessage}`;
eventsList.insertBefore(li, eventsList.firstChild);
}
const eventsList = document.getElementById('events-list');
if (eventsList) {
const li = document.createElement('li');
li.className = `event-${eventType}`;
li.textContent = `[ ${eventType} ] : ${eventMessage}`;
eventsList.insertBefore(li, eventsList.firstChild);
}
};
const escapeHtml = (unsafe: string): string => {
return unsafe
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
return unsafe
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
};
const getWebhookEventsFromStorage = (roomId: string): any[] => {
const data = localStorage.getItem('webhookEventsByRoom');
if (!data) return [];
const map = JSON.parse(data);
return map[roomId] || [];
const data = localStorage.getItem('webhookEventsByRoom');
if (!data) {
return [];
}
const map = JSON.parse(data);
return map[roomId] || [];
};
const saveWebhookEventToStorage = (roomId: string, event: any): void => {
const data = localStorage.getItem('webhookEventsByRoom');
const map = data ? JSON.parse(data) : {};
if (!map[roomId]) map[roomId] = [];
map[roomId].push(event);
localStorage.setItem('webhookEventsByRoom', JSON.stringify(map));
const data = localStorage.getItem('webhookEventsByRoom');
const map = data ? JSON.parse(data) : {};
if (!map[roomId]) {
map[roomId] = [];
}
map[roomId].push(event);
localStorage.setItem('webhookEventsByRoom', JSON.stringify(map));
};
const clearWebhookEventsByRoom = (roomId: string): void => {
const data = localStorage.getItem('webhookEventsByRoom');
if (!data) return;
const map = JSON.parse(data);
if (map[roomId]) {
map[roomId] = [];
localStorage.setItem('webhookEventsByRoom', JSON.stringify(map));
}
const data = localStorage.getItem('webhookEventsByRoom');
if (!data) return;
const map = JSON.parse(data);
if (map[roomId]) {
map[roomId] = [];
localStorage.setItem('webhookEventsByRoom', JSON.stringify(map));
}
};
const shouldShowWebhook = (event: any): boolean => {
return showAllWebhooksCheckbox?.checked || event.data.roomId === roomId;
return showAllWebhooksCheckbox?.checked || event.data.roomId === roomId;
};
const listenWebhookServerEvents = () => {
socket.on('webhookEvent', (event: any) => {
console.log('Webhook received:', event);
const webhookRoomId = event.data.roomId;
socket.on('webhookEvent', (event: any) => {
console.log('Webhook received:', event);
const webhookRoomId = event.data.roomId;
if (webhookRoomId) {
saveWebhookEventToStorage(webhookRoomId, event);
}
if (webhookRoomId) {
saveWebhookEventToStorage(webhookRoomId, event);
}
if (!shouldShowWebhook(event)) {
console.log('Ignoring webhook event:', event);
return;
}
if (!shouldShowWebhook(event)) {
console.log('Ignoring webhook event:', event);
return;
}
addWebhookEventElement(event);
addWebhookEventElement(event);
// Clean up the previous events
const isMeetingEnded = event.event === 'meetingEnded';
if (isMeetingEnded) clearWebhookEventsByRoom(webhookRoomId);
});
// Clean up the previous events
const isMeetingEnded = event.event === 'meetingEnded';
if (isMeetingEnded) clearWebhookEventsByRoom(webhookRoomId);
});
};
const renderStoredWebhookEvents = (roomId: string) => {
const webhookLogList = document.getElementById('webhook-log-list');
if (webhookLogList) {
while (webhookLogList.firstChild) {
webhookLogList.removeChild(webhookLogList.firstChild);
}
}
const events = getWebhookEventsFromStorage(roomId);
events.forEach((event) => addWebhookEventElement(event));
const webhookLogList = document.getElementById('webhook-log-list');
if (webhookLogList) {
while (webhookLogList.firstChild) {
webhookLogList.removeChild(webhookLogList.firstChild);
}
}
const events = getWebhookEventsFromStorage(roomId);
events.forEach((event) => addWebhookEventElement(event));
};
const addWebhookEventElement = (event: any) => {
const webhookLogList = document.getElementById('webhook-log-list');
if (webhookLogList) {
// Create unique IDs for this accordion item
const itemId = event.creationDate;
const headerClassName = `webhook-${event.event}`;
const collapseId = `collapse-${itemId}`;
const webhookLogList = document.getElementById('webhook-log-list');
if (webhookLogList) {
// Create unique IDs for this accordion item
const itemId = event.creationDate;
const headerClassName = `webhook-${event.event}`;
const collapseId = `collapse-${itemId}`;
// Create accordion item container
const accordionItem = document.createElement('div');
accordionItem.className = 'accordion-item';
// Create accordion item container
const accordionItem = document.createElement('div');
accordionItem.className = 'accordion-item';
// Create header
const header = document.createElement('h2');
header.classList.add(headerClassName, 'accordion-header');
// Create header
const header = document.createElement('h2');
header.classList.add(headerClassName, 'accordion-header');
// Create header button
const button = document.createElement('button');
button.className = 'accordion-button';
button.type = 'button';
button.setAttribute('data-bs-toggle', 'collapse');
button.setAttribute('data-bs-target', `#${collapseId}`);
button.setAttribute('aria-expanded', 'true');
button.setAttribute('aria-controls', collapseId);
button.style.padding = '10px';
// Create header button
const button = document.createElement('button');
button.className = 'accordion-button';
button.type = 'button';
button.setAttribute('data-bs-toggle', 'collapse');
button.setAttribute('data-bs-target', `#${collapseId}`);
button.setAttribute('aria-expanded', 'true');
button.setAttribute('aria-controls', collapseId);
button.style.padding = '10px';
if (event.event === 'meetingStarted') {
button.classList.add('bg-success');
}
if (event.event === 'meetingEnded') {
button.classList.add('bg-danger');
}
if (event.event.includes('recording')) {
button.classList.add('bg-warning');
}
// Format the header text with event name and timestamp
const date = new Date(event.creationDate);
if (event.event === 'meetingStarted') {
button.classList.add('bg-success');
}
if (event.event === 'meetingEnded') {
button.classList.add('bg-danger');
}
if (event.event.includes('recording')) {
button.classList.add('bg-warning');
}
const formattedDate = date.toLocaleString('es-ES', {
// year: 'numeric',
// month: '2-digit',
// day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false,
});
button.innerHTML = `[${formattedDate}] <strong>${event.event}</strong>`;
// Format the header text with event name and timestamp
const date = new Date(event.creationDate);
const formattedDate = date.toLocaleString('es-ES', {
// year: 'numeric',
// month: '2-digit',
// day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false
});
button.innerHTML = `[${formattedDate}] <strong>${event.event}</strong>`;
// Create collapsible content container
const collapseDiv = document.createElement('div');
collapseDiv.id = collapseId;
collapseDiv.className = 'accordion-collapse collapse';
collapseDiv.setAttribute('aria-labelledby', headerClassName);
collapseDiv.setAttribute('data-bs-parent', '#webhook-log-list');
// Create collapsible content container
const collapseDiv = document.createElement('div');
collapseDiv.id = collapseId;
collapseDiv.className = 'accordion-collapse collapse';
collapseDiv.setAttribute('aria-labelledby', headerClassName);
collapseDiv.setAttribute('data-bs-parent', '#webhook-log-list');
// Create body content
const bodyDiv = document.createElement('div');
bodyDiv.className = 'accordion-body';
// Create body content
const bodyDiv = document.createElement('div');
bodyDiv.className = 'accordion-body';
// Format JSON with syntax highlighting if possible
const formattedJson = JSON.stringify(event, null, 2);
bodyDiv.innerHTML = `<pre class="mb-0"><code>${escapeHtml(
formattedJson
)}</code></pre>`;
// Format JSON with syntax highlighting if possible
const formattedJson = JSON.stringify(event, null, 2);
bodyDiv.innerHTML = `<pre class="mb-0"><code>${escapeHtml(formattedJson)}</code></pre>`;
// Assemble the components
header.appendChild(button);
collapseDiv.appendChild(bodyDiv);
accordionItem.appendChild(header);
accordionItem.appendChild(collapseDiv);
// Assemble the components
header.appendChild(button);
collapseDiv.appendChild(bodyDiv);
accordionItem.appendChild(header);
accordionItem.appendChild(collapseDiv);
// Insert at the top of the list (latest events first)
if (webhookLogList.firstChild) {
webhookLogList.insertBefore(accordionItem, webhookLogList.firstChild);
} else {
webhookLogList.appendChild(accordionItem);
}
// Insert at the top of the list (latest events first)
if (webhookLogList.firstChild) {
webhookLogList.insertBefore(accordionItem, webhookLogList.firstChild);
} else {
webhookLogList.appendChild(accordionItem);
}
// Limit the number of items to prevent performance issues
const maxItems = 50;
while (webhookLogList.children.length > maxItems) {
webhookLogList.removeChild(webhookLogList.lastChild!);
}
}
// Limit the number of items to prevent performance issues
const maxItems = 50;
while (webhookLogList.children.length > maxItems) {
webhookLogList.removeChild(webhookLogList.lastChild!);
}
}
};
// Listen to events from openvidu-meet
const listenWebComponentEvents = () => {
const meet = document.querySelector('openvidu-meet') as any;
if (!meet) {
console.error('openvidu-meet component not found');
alert('openvidu-meet component not found in the DOM');
return;
}
const meet = document.querySelector('openvidu-meet') as any;
if (!meet) {
console.error('openvidu-meet component not found');
alert('openvidu-meet component not found in the DOM');
return;
}
meet.on('JOIN', (event: CustomEvent<any>) => {
console.log('JOIN event received:', event);
addEventToLog('JOIN', JSON.stringify(event));
});
meet.on('LEFT', (event: CustomEvent<any>) => {
console.log('LEFT event received:', event);
addEventToLog('LEFT', JSON.stringify(event));
});
meet.on('MEETING_ENDED', (event: CustomEvent<any>) => {
console.log('MEETING_ENDED event received:', event);
addEventToLog('MEETING_ENDED', JSON.stringify(event));
});
meet.on('JOIN', (event: CustomEvent<any>) => {
console.log('JOIN event received:', event);
addEventToLog('JOIN', JSON.stringify(event));
});
meet.on('LEFT', (event: CustomEvent<any>) => {
console.log('LEFT event received:', event);
addEventToLog('LEFT', JSON.stringify(event));
});
meet.on('MEETING_ENDED', (event: CustomEvent<any>) => {
console.log('MEETING_ENDED event received:', event);
addEventToLog('MEETING_ENDED', JSON.stringify(event));
});
};
// Set up commands for the web component
const setUpWebComponentCommands = () => {
if (!meet) {
console.error('openvidu-meet component not found');
alert('openvidu-meet component not found in the DOM');
return;
}
if (!meet) {
console.error('openvidu-meet component not found');
alert('openvidu-meet component not found in the DOM');
return;
}
// End meeting button click handler
document
.getElementById('end-meeting-btn')
?.addEventListener('click', () => meet.endMeeting());
// End meeting button click handler
document.getElementById('end-meeting-btn')?.addEventListener('click', () => meet.endMeeting());
// Leave room button click handler
document
.getElementById('leave-room-btn')
?.addEventListener('click', () => meet.leaveRoom());
// Leave room button click handler
document.getElementById('leave-room-btn')?.addEventListener('click', () => meet.leaveRoom());
// Toggle chat button click handler
// document
// .getElementById('toggle-chat-btn')
// ?.addEventListener('click', () => meet.toggleChat());
// Kick participant button click handler
document.getElementById('kick-participant-btn')?.addEventListener('click', () => {
const participantIdentity = (
document.getElementById('participant-identity-input') as HTMLInputElement
).value.trim();
if (participantIdentity) {
meet.kickParticipant(participantIdentity);
}
});
};
document.addEventListener('DOMContentLoaded', () => {
roomId = document.getElementById('room-id')?.textContent?.trim();
showAllWebhooksCheckbox = document.getElementById(
'show-all-webhooks'
) as HTMLInputElement;
meet = document.querySelector('openvidu-meet') as any;
roomId = document.getElementById('room-id')?.textContent?.trim();
showAllWebhooksCheckbox = document.getElementById('show-all-webhooks') as HTMLInputElement;
meet = document.querySelector('openvidu-meet') as any;
if (!roomId) {
console.error('Room ID not found in the DOM');
alert('Room ID not found in the DOM');
return;
}
renderStoredWebhookEvents(roomId);
listenWebhookServerEvents();
listenWebComponentEvents();
setUpWebComponentCommands();
if (!roomId) {
console.error('Room ID not found in the DOM');
alert('Room ID not found in the DOM');
return;
}
showAllWebhooksCheckbox?.addEventListener('change', () => {
if (roomId) renderStoredWebhookEvents(roomId);
});
renderStoredWebhookEvents(roomId);
listenWebhookServerEvents();
listenWebComponentEvents();
setUpWebComponentCommands();
showAllWebhooksCheckbox?.addEventListener('change', () => {
if (roomId) renderStoredWebhookEvents(roomId);
});
});

View File

@ -109,24 +109,29 @@
</button>
</form>
</li>
{{! ONLY RECORDINGS LINK }}
{{! SHOW ONLY RECORDINGS LINK }}
<li>
<form action="/join-room" method="post">
<input
type="hidden"
name="roomUrl"
value="{{ publisherRoomUrl + '?viewRecordings=true' }}"
value="{{ moderatorRoomUrl }}"
/>
<input
type="hidden"
name="participantRole"
value="publisher"
value="moderator"
/>
<input type="hidden" name="roomId" value="{{ roomId }}" />
<input
type="hidden"
name="showOnlyRecordings"
value="true"
/>
<button
type="submit"
id="join-as-publisher"
id="view-recordings"
class="dropdown-item"
>
View Recordings
@ -273,7 +278,6 @@
<option value="admin-moderator-publisher" selected>
Admin, Moderator & Publisher
</option>
<option value="public">Public Access</option>
</select>
</div>
</div>

View File

@ -42,9 +42,17 @@
<button id="leave-room-btn" class="btn btn-warning">
Leave Room
</button>
<!-- <button id="toggle-chat-btn" class="btn btn-success">
Toggle Chat
</button> -->
{{#isModerator}}
<input
type="text"
id="participant-identity-input"
placeholder="Participant Identity"
class="form-control mb-2"
/>
<button id="kick-participant-btn" class="btn btn-info">
Kick Participant
</button>
{{/isModerator}}
</div>
<!-- Events -->
@ -82,7 +90,9 @@
<openvidu-meet
room-url="{{ roomUrl }}"
participant-name="{{ participantName }}"
leave-redirect-url="https://openvidu.io"
{{#showOnlyRecordings}}
show-only-recordings=true
{{/showOnlyRecordings}}
></openvidu-meet>
</div>
</div>

View File

@ -1,124 +1,112 @@
import { Request, Response } from 'express';
import {
getAllRooms,
createRoom,
deleteRoom,
deleteAllRooms,
} from '../services/roomService';
import {
deleteAllRecordings,
getAllRecordings,
} from '../services/recordingService';
import { getAllRooms, createRoom, deleteRoom, deleteAllRooms } from '../services/roomService';
import { deleteAllRecordings, getAllRecordings } from '../services/recordingService';
export const getHome = async (req: Request, res: Response) => {
try {
const { rooms } = await getAllRooms();
export const getHome = async (_req: Request, res: Response) => {
try {
const { rooms } = await getAllRooms();
//sort rooms by newest first
rooms.sort((a, b) => {
const dateA = new Date(a.creationDate);
const dateB = new Date(b.creationDate);
return dateB.getTime() - dateA.getTime();
});
// Sort rooms by newest first
rooms.sort((a, b) => {
const dateA = new Date(a.creationDate);
const dateB = new Date(b.creationDate);
return dateB.getTime() - dateA.getTime();
});
console.log(`Rooms fetched: ${rooms.length}`);
res.render('index', { rooms });
} catch (error) {
console.error('Error fetching rooms:', error);
res.status(500).send('Internal Server Error');
return;
}
console.log(`Rooms fetched: ${rooms.length}`);
res.render('index', { rooms });
} catch (error) {
console.error('Error fetching rooms:', error);
res.status(500).send('Internal Server Error');
return;
}
};
export const postCreateRoom = async (req: Request, res: Response) => {
try {
const { roomIdPrefix, autoDeletionDate } = req.body;
const preferences = processFormPreferences(req.body);
try {
const { roomIdPrefix, autoDeletionDate } = req.body;
const preferences = processFormPreferences(req.body);
await createRoom({ roomIdPrefix, autoDeletionDate, preferences });
res.redirect('/');
} catch (error) {
console.error('Error creating room:', error);
res.status(500).send('Internal Server Error');
return;
}
await createRoom({ roomIdPrefix, autoDeletionDate, preferences });
res.redirect('/');
} catch (error) {
console.error('Error creating room:', error);
res.status(500).send('Internal Server Error');
return;
}
};
export const deleteRoomCtrl = async (req: Request, res: Response) => {
try {
const { roomId } = req.body;
await deleteRoom(roomId);
res.redirect('/');
} catch (error) {
console.error('Error deleting room:', error);
res.status(500).send('Internal Server Error');
return;
}
try {
const { roomId } = req.body;
await deleteRoom(roomId);
res.redirect('/');
} catch (error) {
console.error('Error deleting room:', error);
res.status(500).send('Internal Server Error');
return;
}
};
export const deleteAllRoomsCtrl = async (_req: Request, res: Response) => {
try {
const allRooms = await getAllRooms();
if (allRooms.rooms.length === 0) {
console.log('No rooms to delete');
res.render('index', { rooms: [] });
return;
}
const roomIds = allRooms.rooms.map((room) => room.roomId);
console.log(`Deleting ${roomIds.length} rooms`, roomIds);
await deleteAllRooms(roomIds);
res.render('index', { rooms: [] });
} catch (error) {
console.error('Error deleting all rooms:', error);
res.status(500).send('Internal Server Error ' + JSON.stringify(error));
return;
}
try {
const allRooms = await getAllRooms();
if (allRooms.rooms.length === 0) {
console.log('No rooms to delete');
res.render('index', { rooms: [] });
return;
}
const roomIds = allRooms.rooms.map((room) => room.roomId);
console.log(`Deleting ${roomIds.length} rooms`, roomIds);
await deleteAllRooms(roomIds);
res.render('index', { rooms: [] });
} catch (error) {
console.error('Error deleting all rooms:', error);
res.status(500).send('Internal Server Error ' + JSON.stringify(error));
return;
}
};
export const deleteAllRecordingsCtrl = async (_req: Request, res: Response) => {
try {
const [{ recordings }, { rooms }] = await Promise.all([
getAllRecordings(),
getAllRooms(),
]);
if (recordings.length === 0) {
console.log('No recordings to delete');
res.render('index', { rooms });
return;
}
const recordingIds = recordings.map((recording) => recording.recordingId);
await deleteAllRecordings(recordingIds);
console.log(`Deleted ${recordingIds.length} recordings`);
res.render('index', { rooms });
} catch (error) {
console.error('Error deleting all recordings:', error);
res.status(500).send('Internal Server Error ' + JSON.stringify(error));
return;
}
try {
const [{ recordings }, { rooms }] = await Promise.all([getAllRecordings(), getAllRooms()]);
if (recordings.length === 0) {
console.log('No recordings to delete');
res.render('index', { rooms });
return;
}
const recordingIds = recordings.map((recording) => recording.recordingId);
await deleteAllRecordings(recordingIds);
console.log(`Deleted ${recordingIds.length} recordings`);
res.render('index', { rooms });
} catch (error) {
console.error('Error deleting all recordings:', error);
res.status(500).send('Internal Server Error ' + JSON.stringify(error));
return;
}
};
/**
* Converts flat form data to nested MeetRoomPreferences object
*/
const processFormPreferences = (body: any): any => {
const preferences = {
chatPreferences: {
enabled: body['preferences.chatPreferences.enabled'] === 'on',
},
recordingPreferences: {
enabled: body['preferences.recordingPreferences.enabled'] === 'on',
// Only include allowAccessTo if recording is enabled
...(body['preferences.recordingPreferences.enabled'] === 'on' && {
allowAccessTo:
body['preferences.recordingPreferences.allowAccessTo'] ||
'admin-moderator-publisher',
}),
},
virtualBackgroundPreferences: {
enabled:
body['preferences.virtualBackgroundPreferences.enabled'] === 'on',
},
};
const preferences = {
chatPreferences: {
enabled: body['preferences.chatPreferences.enabled'] === 'on'
},
recordingPreferences: {
enabled: body['preferences.recordingPreferences.enabled'] === 'on',
// Only include allowAccessTo if recording is enabled
...(body['preferences.recordingPreferences.enabled'] === 'on' && {
allowAccessTo: body['preferences.recordingPreferences.allowAccessTo'] || 'admin-moderator-publisher'
})
},
virtualBackgroundPreferences: {
enabled: body['preferences.virtualBackgroundPreferences.enabled'] === 'on'
}
};
return preferences;
return preferences;
};

View File

@ -5,56 +5,55 @@ import { ParticipantRole } from '../../../typings/src/participant';
import { MeetWebhookEvent } from '../../../typings/src/webhook.model';
interface JoinRoomRequest {
participantRole: ParticipantRole;
roomUrl: string;
roomId: string;
participantName?: string;
participantRole: ParticipantRole;
roomUrl: string;
roomId: string;
participantName?: string;
showOnlyRecordings?: boolean;
}
export const joinRoom = (req: Request, res: Response) => {
try {
const {
participantRole,
roomUrl,
roomId,
participantName = 'User',
} = req.body as JoinRoomRequest;
if (!roomUrl) {
throw new Error('Room URL is required.');
}
res.render('room', {
roomUrl,
participantRole,
participantName,
roomId,
isModerator: participantRole === 'moderator',
});
} catch (error) {
console.error('Error joining room:', error);
res.status(500).send('Internal Server Error');
return;
}
try {
const {
participantRole,
roomUrl,
roomId,
participantName = 'User',
showOnlyRecordings
} = req.body as JoinRoomRequest;
if (!roomUrl) {
throw new Error('Room URL is required.');
}
res.render('room', {
roomUrl,
isModerator: participantRole === 'moderator',
participantName,
roomId,
showOnlyRecordings: showOnlyRecordings || false
});
} catch (error) {
console.error('Error joining room:', error);
res.status(500).send('Internal Server Error');
return;
}
};
export const handleWebhook = async (
req: Request,
res: Response,
io: IOServer
): Promise<void> => {
try {
const webhookEvent = req.body as MeetWebhookEvent;
export const handleWebhook = async (req: Request, res: Response, io: IOServer): Promise<void> => {
try {
const webhookEvent = req.body as MeetWebhookEvent;
// Log event details
console.info(`Webhook received: ${webhookEvent.event}`);
// Log event details
console.info(`Webhook received: ${webhookEvent.event}`);
// Broadcast to all connected clients
io.emit('webhookEvent', webhookEvent);
res.status(200).send('Webhook received');
} catch (error) {
console.error('Error handling webhook:', error);
res.status(400).json({
message: 'Error handling webhook',
error: (error as Error).message,
});
}
// Broadcast to all connected clients
io.emit('webhookEvent', webhookEvent);
res.status(200).send('Webhook received');
} catch (error) {
console.error('Error handling webhook:', error);
res.status(400).json({
message: 'Error handling webhook',
error: (error as Error).message
});
}
};

View File

@ -1,13 +1,13 @@
import express from 'express';
import http from 'http';
import { Server as IOServer } from 'socket.io';
import path from 'path';
import { Server as IOServer } from 'socket.io';
import {
getHome,
postCreateRoom,
deleteRoomCtrl,
deleteAllRoomsCtrl,
deleteAllRecordingsCtrl,
deleteAllRecordingsCtrl,
deleteAllRoomsCtrl,
deleteRoomCtrl,
getHome,
postCreateRoom
} from './controllers/homeController';
import { handleWebhook, joinRoom } from './controllers/roomController';
import { configService } from './services/configService';
@ -31,27 +31,24 @@ app.use(express.json());
// Routes
app.get('/', getHome);
app.get('/room', joinRoom);
app.post('/room', postCreateRoom);
app.post('/room/delete', deleteRoomCtrl);
app.post('/delete-all-rooms', deleteAllRoomsCtrl);
app.post('/delete-all-recordings', deleteAllRecordingsCtrl);
app.post('/join-room', joinRoom);
app.post('/webhook', (req, res) => {
handleWebhook(req, res, io);
handleWebhook(req, res, io);
});
const PORT = configService.serverPort;
server.listen(PORT, () => {
console.log('-----------------------------------------');
console.log(`Server running on port ${PORT}`);
console.log(`Meet API URL: ${configService.meetApiUrl}`);
console.log('-----------------------------------------');
console.log('');
console.log(`Visit http://localhost:${PORT}/ to access the app`);
console.log('-----------------------------------------');
console.log(`Server running on port ${PORT}`);
console.log(`Visit http://localhost:${PORT}/ to access the app`);
console.log('-----------------------------------------');
console.log('');
console.log('Environment variables:');
console.log(`OPENVIDU_MEET_URL: ${process.env.OPENVIDU_MEET_URL}`);
console.log(`MEET_API_KEY: ${process.env.MEET_API_KEY} `);
console.log(`PORT: ${process.env.PORT}`);
console.log('OpenVidu Meet Configuration:');
console.log(`Meet API URL: ${configService.meetApiUrl}`);
console.log(`Meet API key: ${configService.meetApiKey}`);
});

View File

@ -3,15 +3,15 @@ import dotenv from 'dotenv';
dotenv.config();
export class ConfigService {
public meetApiUrl: string;
public meetApiKey: string;
public serverPort: number;
public meetApiUrl: string;
public meetApiKey: string;
public serverPort: number;
constructor() {
this.meetApiUrl = process.env.OPENVIDU_MEET_URL!;
this.meetApiKey = process.env.MEET_API_KEY!;
this.serverPort = parseInt(process.env.PORT!, 10);
}
constructor() {
this.meetApiUrl = process.env.OPENVIDU_MEET_URL!;
this.meetApiKey = process.env.MEET_API_KEY!;
this.serverPort = parseInt(process.env.PORT!, 10);
}
}
export const configService = new ConfigService();

View File

@ -1,41 +1,39 @@
import { del, get } from '../utils/http';
// @ts-ignore
import { MeetRecordingInfo } from '../../../typings/src/recording.model';
import { configService } from './configService';
export const getAllRecordings = async (): Promise<{
pagination: { isTruncated: boolean; nextPageToken?: string };
recordings: MeetRecordingInfo[];
pagination: { isTruncated: boolean; nextPageToken?: string };
recordings: MeetRecordingInfo[];
}> => {
const url = `${configService.meetApiUrl}/recordings`;
const url = `${configService.meetApiUrl}/recordings`;
let { pagination, recordings } = await get<{
pagination: any;
recordings: MeetRecordingInfo[];
}>(url, {
headers: { 'x-api-key': configService.meetApiKey },
});
let { pagination, recordings } = await get<{
pagination: any;
recordings: MeetRecordingInfo[];
}>(url, {
headers: { 'x-api-key': configService.meetApiKey }
});
while (pagination.isTruncated) {
const nextPageUrl = `${url}?nextPageToken=${pagination.nextPageToken}`;
const nextPageResult = await get<{
pagination: any;
recordings: MeetRecordingInfo[];
}>(nextPageUrl, {
headers: { 'x-api-key': configService.meetApiKey },
});
recordings.push(...nextPageResult.recordings);
pagination = nextPageResult.pagination;
}
return { pagination, recordings };
while (pagination.isTruncated) {
const nextPageUrl = `${url}?nextPageToken=${pagination.nextPageToken}`;
const nextPageResult = await get<{
pagination: any;
recordings: MeetRecordingInfo[];
}>(nextPageUrl, {
headers: { 'x-api-key': configService.meetApiKey }
});
recordings.push(...nextPageResult.recordings);
pagination = nextPageResult.pagination;
}
return { pagination, recordings };
};
export const deleteAllRecordings = async (
recordingIds: string[]
): Promise<void> => {
const url = `${
configService.meetApiUrl
}/recordings?recordingIds=${recordingIds.join(',')}`;
await del<void>(url, {
headers: { 'x-api-key': configService.meetApiKey },
});
export const deleteAllRecordings = async (recordingIds: string[]): Promise<void> => {
const url = `${configService.meetApiUrl}/recordings?recordingIds=${recordingIds.join(',')}`;
await del<void>(url, {
headers: { 'x-api-key': configService.meetApiKey }
});
};

View File

@ -1,41 +1,45 @@
import { get, post, del } from '../utils/http';
import { del, get, post } from '../utils/http';
import { configService } from './configService';
// @ts-ignore
import { MeetRoom, MeetRoomOptions } from '../../../typings/src/room';
export async function getAllRooms(): Promise<{
pagination: any;
rooms: MeetRoom[];
pagination: any;
rooms: MeetRoom[];
}> {
const url = `${configService.meetApiUrl}/rooms`;
return get<{ pagination: any; rooms: MeetRoom[] }>(url, {
headers: { 'x-api-key': configService.meetApiKey },
});
const url = `${configService.meetApiUrl}/rooms`;
return get<{ pagination: any; rooms: MeetRoom[] }>(url, {
headers: { 'x-api-key': configService.meetApiKey }
});
}
export async function createRoom(roomData: MeetRoomOptions): Promise<MeetRoom> {
const url = `${configService.meetApiUrl}/rooms`;
// Default to 1 hour if autoDeletionDate is not provided
if (!roomData.autoDeletionDate)
roomData.autoDeletionDate = new Date(Date.now() + 60 * 61 * 1000).getTime();
console.log('Creating room with options:', roomData);
return post<MeetRoom>(url, {
headers: { 'x-api-key': configService.meetApiKey },
body: roomData,
});
const url = `${configService.meetApiUrl}/rooms`;
// Default to 1 hour if autoDeletionDate is not provided
if (!roomData.autoDeletionDate) {
roomData.autoDeletionDate = new Date(Date.now() + 60 * 61 * 1000).getTime();
} else {
// Ensure autoDeletionDate is a timestamp
roomData.autoDeletionDate = new Date(roomData.autoDeletionDate).getTime();
}
console.log('Creating room with options:', roomData);
return post<MeetRoom>(url, {
headers: { 'x-api-key': configService.meetApiKey },
body: roomData
});
}
export async function deleteRoom(roomId: string): Promise<void> {
const url = `${configService.meetApiUrl}/rooms/${roomId}`;
await del<void>(url, {
headers: { 'x-api-key': configService.meetApiKey },
});
const url = `${configService.meetApiUrl}/rooms/${roomId}`;
await del<void>(url, {
headers: { 'x-api-key': configService.meetApiKey }
});
}
export async function deleteAllRooms(roomIds: string[]): Promise<void> {
const url = `${configService.meetApiUrl}/rooms?roomIds=${roomIds.join(',')}`;
await del<void>(url, {
headers: { 'x-api-key': configService.meetApiKey },
});
const url = `${configService.meetApiUrl}/rooms?roomIds=${roomIds.join(',')}`;
await del<void>(url, {
headers: { 'x-api-key': configService.meetApiKey }
});
}

View File

@ -1,70 +1,50 @@
export interface RequestOptions {
headers?: Record<string, string>;
queryParams?: Record<string, string>;
body?: any;
headers?: Record<string, string>;
queryParams?: Record<string, string>;
body?: any;
}
async function buildUrl(
url: string,
queryParams?: Record<string, string>
): Promise<string> {
if (!queryParams) return url;
const params = new URLSearchParams(
queryParams as Record<string, string>
).toString();
return `${url}?${params}`;
async function buildUrl(url: string, queryParams?: Record<string, string>): Promise<string> {
if (!queryParams) return url;
const params = new URLSearchParams(queryParams as Record<string, string>).toString();
return `${url}?${params}`;
}
async function request<T>(
method: string,
url: string,
options: RequestOptions = {}
): Promise<T> {
const fullUrl = await buildUrl(url, options.queryParams);
const fetchOptions: RequestInit = {
method,
headers: { 'Content-Type': 'application/json', ...(options.headers || {}) },
body: options.body ? JSON.stringify(options.body) : undefined,
};
const response = await fetch(fullUrl, fetchOptions);
if (!response.ok) {
const text = await response.text();
throw new Error(`HTTP ${response.status}: ${text}`);
}
async function request<T>(method: string, url: string, options: RequestOptions = {}): Promise<T> {
const fullUrl = await buildUrl(url, options.queryParams);
const fetchOptions: RequestInit = {
method,
headers: { 'Content-Type': 'application/json', ...(options.headers || {}) },
body: options.body ? JSON.stringify(options.body) : undefined
};
// Handle empty responses (e.g., for DELETE requests)
const text = await response.text();
if (!text) {
return {} as T;
}
const response = await fetch(fullUrl, fetchOptions);
if (!response.ok) {
const text = await response.text();
throw new Error(`HTTP ${response.status}: ${text}`);
}
return JSON.parse(text) as T;
// Handle empty responses (e.g., for DELETE requests)
const text = await response.text();
if (!text) {
return {} as T;
}
return JSON.parse(text) as T;
}
export function get<T>(
url: string,
options?: Omit<RequestOptions, 'body'>
): Promise<T> {
return request<T>('GET', url, options || {});
export function get<T>(url: string, options?: Omit<RequestOptions, 'body'>): Promise<T> {
return request<T>('GET', url, options || {});
}
export function post<T>(
url: string,
options?: Omit<RequestOptions, 'body'> & { body: any }
): Promise<T> {
return request<T>('POST', url, options as RequestOptions);
export function post<T>(url: string, options?: Omit<RequestOptions, 'body'> & { body: any }): Promise<T> {
return request<T>('POST', url, options as RequestOptions);
}
export function put<T>(
url: string,
options?: Omit<RequestOptions, 'body'> & { body: any }
): Promise<T> {
return request<T>('PUT', url, options as RequestOptions);
export function put<T>(url: string, options?: Omit<RequestOptions, 'body'> & { body: any }): Promise<T> {
return request<T>('PUT', url, options as RequestOptions);
}
export function del<T>(
url: string,
options?: Omit<RequestOptions, 'body'>
): Promise<T> {
return request<T>('DELETE', url, options || {});
export function del<T>(url: string, options?: Omit<RequestOptions, 'body'>): Promise<T> {
return request<T>('DELETE', url, options || {});
}