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

View File

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

View File

@ -42,9 +42,17 @@
<button id="leave-room-btn" class="btn btn-warning"> <button id="leave-room-btn" class="btn btn-warning">
Leave Room Leave Room
</button> </button>
<!-- <button id="toggle-chat-btn" class="btn btn-success"> {{#isModerator}}
Toggle Chat <input
</button> --> 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> </div>
<!-- Events --> <!-- Events -->
@ -82,7 +90,9 @@
<openvidu-meet <openvidu-meet
room-url="{{ roomUrl }}" room-url="{{ roomUrl }}"
participant-name="{{ participantName }}" participant-name="{{ participantName }}"
leave-redirect-url="https://openvidu.io" {{#showOnlyRecordings}}
show-only-recordings=true
{{/showOnlyRecordings}}
></openvidu-meet> ></openvidu-meet>
</div> </div>
</div> </div>

View File

@ -1,124 +1,112 @@
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import { import { getAllRooms, createRoom, deleteRoom, deleteAllRooms } from '../services/roomService';
getAllRooms, import { deleteAllRecordings, getAllRecordings } from '../services/recordingService';
createRoom,
deleteRoom,
deleteAllRooms,
} from '../services/roomService';
import {
deleteAllRecordings,
getAllRecordings,
} from '../services/recordingService';
export const getHome = async (req: Request, res: Response) => { export const getHome = async (_req: Request, res: Response) => {
try { try {
const { rooms } = await getAllRooms(); const { rooms } = await getAllRooms();
//sort rooms by newest first // Sort rooms by newest first
rooms.sort((a, b) => { rooms.sort((a, b) => {
const dateA = new Date(a.creationDate); const dateA = new Date(a.creationDate);
const dateB = new Date(b.creationDate); const dateB = new Date(b.creationDate);
return dateB.getTime() - dateA.getTime(); return dateB.getTime() - dateA.getTime();
}); });
console.log(`Rooms fetched: ${rooms.length}`); console.log(`Rooms fetched: ${rooms.length}`);
res.render('index', { rooms }); res.render('index', { rooms });
} catch (error) { } catch (error) {
console.error('Error fetching rooms:', error); console.error('Error fetching rooms:', error);
res.status(500).send('Internal Server Error'); res.status(500).send('Internal Server Error');
return; return;
} }
}; };
export const postCreateRoom = async (req: Request, res: Response) => { export const postCreateRoom = async (req: Request, res: Response) => {
try { try {
const { roomIdPrefix, autoDeletionDate } = req.body; const { roomIdPrefix, autoDeletionDate } = req.body;
const preferences = processFormPreferences(req.body); const preferences = processFormPreferences(req.body);
await createRoom({ roomIdPrefix, autoDeletionDate, preferences }); await createRoom({ roomIdPrefix, autoDeletionDate, preferences });
res.redirect('/'); res.redirect('/');
} catch (error) { } catch (error) {
console.error('Error creating room:', error); console.error('Error creating room:', error);
res.status(500).send('Internal Server Error'); res.status(500).send('Internal Server Error');
return; return;
} }
}; };
export const deleteRoomCtrl = async (req: Request, res: Response) => { export const deleteRoomCtrl = async (req: Request, res: Response) => {
try { try {
const { roomId } = req.body; const { roomId } = req.body;
await deleteRoom(roomId); await deleteRoom(roomId);
res.redirect('/'); res.redirect('/');
} catch (error) { } catch (error) {
console.error('Error deleting room:', error); console.error('Error deleting room:', error);
res.status(500).send('Internal Server Error'); res.status(500).send('Internal Server Error');
return; return;
} }
}; };
export const deleteAllRoomsCtrl = async (_req: Request, res: Response) => { export const deleteAllRoomsCtrl = async (_req: Request, res: Response) => {
try { try {
const allRooms = await getAllRooms(); const allRooms = await getAllRooms();
if (allRooms.rooms.length === 0) { if (allRooms.rooms.length === 0) {
console.log('No rooms to delete'); console.log('No rooms to delete');
res.render('index', { rooms: [] }); res.render('index', { rooms: [] });
return; return;
} }
const roomIds = allRooms.rooms.map((room) => room.roomId);
console.log(`Deleting ${roomIds.length} rooms`, roomIds); const roomIds = allRooms.rooms.map((room) => room.roomId);
await deleteAllRooms(roomIds); console.log(`Deleting ${roomIds.length} rooms`, roomIds);
res.render('index', { rooms: [] }); await deleteAllRooms(roomIds);
} catch (error) { res.render('index', { rooms: [] });
console.error('Error deleting all rooms:', error); } catch (error) {
res.status(500).send('Internal Server Error ' + JSON.stringify(error)); console.error('Error deleting all rooms:', error);
return; res.status(500).send('Internal Server Error ' + JSON.stringify(error));
} return;
}
}; };
export const deleteAllRecordingsCtrl = async (_req: Request, res: Response) => { export const deleteAllRecordingsCtrl = async (_req: Request, res: Response) => {
try { try {
const [{ recordings }, { rooms }] = await Promise.all([ const [{ recordings }, { rooms }] = await Promise.all([getAllRecordings(), getAllRooms()]);
getAllRecordings(), if (recordings.length === 0) {
getAllRooms(), console.log('No recordings to delete');
]); res.render('index', { rooms });
if (recordings.length === 0) { return;
console.log('No recordings to delete'); }
res.render('index', { rooms });
return; const recordingIds = recordings.map((recording) => recording.recordingId);
} await deleteAllRecordings(recordingIds);
const recordingIds = recordings.map((recording) => recording.recordingId); console.log(`Deleted ${recordingIds.length} recordings`);
await deleteAllRecordings(recordingIds); res.render('index', { rooms });
console.log(`Deleted ${recordingIds.length} recordings`); } catch (error) {
res.render('index', { rooms }); console.error('Error deleting all recordings:', error);
} catch (error) { res.status(500).send('Internal Server Error ' + JSON.stringify(error));
console.error('Error deleting all recordings:', error); return;
res.status(500).send('Internal Server Error ' + JSON.stringify(error)); }
return;
}
}; };
/** /**
* Converts flat form data to nested MeetRoomPreferences object * Converts flat form data to nested MeetRoomPreferences object
*/ */
const processFormPreferences = (body: any): any => { const processFormPreferences = (body: any): any => {
const preferences = { const preferences = {
chatPreferences: { chatPreferences: {
enabled: body['preferences.chatPreferences.enabled'] === 'on', enabled: body['preferences.chatPreferences.enabled'] === 'on'
}, },
recordingPreferences: { recordingPreferences: {
enabled: body['preferences.recordingPreferences.enabled'] === 'on', enabled: body['preferences.recordingPreferences.enabled'] === 'on',
// Only include allowAccessTo if recording is enabled // Only include allowAccessTo if recording is enabled
...(body['preferences.recordingPreferences.enabled'] === 'on' && { ...(body['preferences.recordingPreferences.enabled'] === 'on' && {
allowAccessTo: allowAccessTo: body['preferences.recordingPreferences.allowAccessTo'] || 'admin-moderator-publisher'
body['preferences.recordingPreferences.allowAccessTo'] || })
'admin-moderator-publisher', },
}), virtualBackgroundPreferences: {
}, enabled: body['preferences.virtualBackgroundPreferences.enabled'] === 'on'
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'; import { MeetWebhookEvent } from '../../../typings/src/webhook.model';
interface JoinRoomRequest { interface JoinRoomRequest {
participantRole: ParticipantRole; participantRole: ParticipantRole;
roomUrl: string; roomUrl: string;
roomId: string; roomId: string;
participantName?: string; participantName?: string;
showOnlyRecordings?: boolean;
} }
export const joinRoom = (req: Request, res: Response) => { export const joinRoom = (req: Request, res: Response) => {
try { try {
const { const {
participantRole, participantRole,
roomUrl, roomUrl,
roomId, roomId,
participantName = 'User', participantName = 'User',
} = req.body as JoinRoomRequest; showOnlyRecordings
if (!roomUrl) { } = req.body as JoinRoomRequest;
throw new Error('Room URL is required.'); if (!roomUrl) {
} throw new Error('Room URL is required.');
res.render('room', { }
roomUrl,
participantRole, res.render('room', {
participantName, roomUrl,
roomId, isModerator: participantRole === 'moderator',
isModerator: participantRole === 'moderator', participantName,
}); roomId,
} catch (error) { showOnlyRecordings: showOnlyRecordings || false
console.error('Error joining room:', error); });
res.status(500).send('Internal Server Error'); } catch (error) {
return; console.error('Error joining room:', error);
} res.status(500).send('Internal Server Error');
return;
}
}; };
export const handleWebhook = async ( export const handleWebhook = async (req: Request, res: Response, io: IOServer): Promise<void> => {
req: Request, try {
res: Response, const webhookEvent = req.body as MeetWebhookEvent;
io: IOServer
): Promise<void> => {
try {
const webhookEvent = req.body as MeetWebhookEvent;
// Log event details // Log event details
console.info(`Webhook received: ${webhookEvent.event}`); console.info(`Webhook received: ${webhookEvent.event}`);
// Broadcast to all connected clients // Broadcast to all connected clients
io.emit('webhookEvent', webhookEvent); io.emit('webhookEvent', webhookEvent);
res.status(200).send('Webhook received'); res.status(200).send('Webhook received');
} catch (error) { } catch (error) {
console.error('Error handling webhook:', error); console.error('Error handling webhook:', error);
res.status(400).json({ res.status(400).json({
message: 'Error handling webhook', message: 'Error handling webhook',
error: (error as Error).message, error: (error as Error).message
}); });
} }
}; };

View File

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

View File

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

View File

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

View File

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