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

@ -3,6 +3,7 @@ const socket = (window as any).io();
let meet: {
endMeeting: () => void;
leaveRoom: () => void;
kickParticipant: (participantIdentity: string) => void;
on: (event: string, callback: (event: CustomEvent<any>) => void) => void;
};
let roomId: string | undefined;
@ -31,7 +32,10 @@ const escapeHtml = (unsafe: string): string => {
const getWebhookEventsFromStorage = (roomId: string): any[] => {
const data = localStorage.getItem('webhookEventsByRoom');
if (!data) return [];
if (!data) {
return [];
}
const map = JSON.parse(data);
return map[roomId] || [];
};
@ -39,7 +43,10 @@ const getWebhookEventsFromStorage = (roomId: string): any[] => {
const saveWebhookEventToStorage = (roomId: string, event: any): void => {
const data = localStorage.getItem('webhookEventsByRoom');
const map = data ? JSON.parse(data) : {};
if (!map[roomId]) map[roomId] = [];
if (!map[roomId]) {
map[roomId] = [];
}
map[roomId].push(event);
localStorage.setItem('webhookEventsByRoom', JSON.stringify(map));
};
@ -47,6 +54,7 @@ const saveWebhookEventToStorage = (roomId: string, event: any): void => {
const clearWebhookEventsByRoom = (roomId: string): void => {
const data = localStorage.getItem('webhookEventsByRoom');
if (!data) return;
const map = JSON.parse(data);
if (map[roomId]) {
map[roomId] = [];
@ -57,6 +65,7 @@ const clearWebhookEventsByRoom = (roomId: string): void => {
const shouldShowWebhook = (event: any): boolean => {
return showAllWebhooksCheckbox?.checked || event.data.roomId === roomId;
};
const listenWebhookServerEvents = () => {
socket.on('webhookEvent', (event: any) => {
console.log('Webhook received:', event);
@ -86,6 +95,7 @@ const renderStoredWebhookEvents = (roomId: string) => {
webhookLogList.removeChild(webhookLogList.firstChild);
}
}
const events = getWebhookEventsFromStorage(roomId);
events.forEach((event) => addWebhookEventElement(event));
};
@ -125,9 +135,9 @@ const addWebhookEventElement = (event: any) => {
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);
const formattedDate = date.toLocaleString('es-ES', {
// year: 'numeric',
// month: '2-digit',
@ -135,7 +145,7 @@ const addWebhookEventElement = (event: any) => {
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false,
hour12: false
});
button.innerHTML = `[${formattedDate}] <strong>${event.event}</strong>`;
@ -152,9 +162,7 @@ const addWebhookEventElement = (event: any) => {
// 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>`;
bodyDiv.innerHTML = `<pre class="mb-0"><code>${escapeHtml(formattedJson)}</code></pre>`;
// Assemble the components
header.appendChild(button);
@ -178,7 +186,6 @@ const addWebhookEventElement = (event: any) => {
};
// Listen to events from openvidu-meet
const listenWebComponentEvents = () => {
const meet = document.querySelector('openvidu-meet') as any;
if (!meet) {
@ -195,7 +202,6 @@ const listenWebComponentEvents = () => {
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));
@ -211,26 +217,25 @@ const setUpWebComponentCommands = () => {
}
// End meeting button click handler
document
.getElementById('end-meeting-btn')
?.addEventListener('click', () => meet.endMeeting());
document.getElementById('end-meeting-btn')?.addEventListener('click', () => meet.endMeeting());
// Leave room button click handler
document
.getElementById('leave-room-btn')
?.addEventListener('click', () => meet.leaveRoom());
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;
showAllWebhooksCheckbox = document.getElementById('show-all-webhooks') as HTMLInputElement;
meet = document.querySelector('openvidu-meet') as any;
if (!roomId) {
@ -238,6 +243,7 @@ document.addEventListener('DOMContentLoaded', () => {
alert('Room ID not found in the DOM');
return;
}
renderStoredWebhookEvents(roomId);
listenWebhookServerEvents();
listenWebComponentEvents();

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,20 +1,12 @@
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) => {
export const getHome = async (_req: Request, res: Response) => {
try {
const { rooms } = await getAllRooms();
//sort rooms by newest first
// Sort rooms by newest first
rooms.sort((a, b) => {
const dateA = new Date(a.creationDate);
const dateB = new Date(b.creationDate);
@ -64,6 +56,7 @@ export const deleteAllRoomsCtrl = async (_req: Request, res: Response) => {
res.render('index', { rooms: [] });
return;
}
const roomIds = allRooms.rooms.map((room) => room.roomId);
console.log(`Deleting ${roomIds.length} rooms`, roomIds);
await deleteAllRooms(roomIds);
@ -77,15 +70,13 @@ export const deleteAllRoomsCtrl = async (_req: Request, res: Response) => {
export const deleteAllRecordingsCtrl = async (_req: Request, res: Response) => {
try {
const [{ recordings }, { rooms }] = await Promise.all([
getAllRecordings(),
getAllRooms(),
]);
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`);
@ -103,21 +94,18 @@ export const deleteAllRecordingsCtrl = async (_req: Request, res: Response) => {
const processFormPreferences = (body: any): any => {
const preferences = {
chatPreferences: {
enabled: body['preferences.chatPreferences.enabled'] === 'on',
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',
}),
allowAccessTo: body['preferences.recordingPreferences.allowAccessTo'] || 'admin-moderator-publisher'
})
},
virtualBackgroundPreferences: {
enabled:
body['preferences.virtualBackgroundPreferences.enabled'] === 'on',
},
enabled: body['preferences.virtualBackgroundPreferences.enabled'] === 'on'
}
};
return preferences;

View File

@ -9,6 +9,7 @@ interface JoinRoomRequest {
roomUrl: string;
roomId: string;
participantName?: string;
showOnlyRecordings?: boolean;
}
export const joinRoom = (req: Request, res: Response) => {
@ -18,16 +19,18 @@ export const joinRoom = (req: Request, res: Response) => {
roomUrl,
roomId,
participantName = 'User',
showOnlyRecordings
} = req.body as JoinRoomRequest;
if (!roomUrl) {
throw new Error('Room URL is required.');
}
res.render('room', {
roomUrl,
participantRole,
isModerator: participantRole === 'moderator',
participantName,
roomId,
isModerator: participantRole === 'moderator',
showOnlyRecordings: showOnlyRecordings || false
});
} catch (error) {
console.error('Error joining room:', error);
@ -36,11 +39,7 @@ export const joinRoom = (req: Request, res: Response) => {
}
};
export const handleWebhook = async (
req: Request,
res: Response,
io: IOServer
): Promise<void> => {
export const handleWebhook = async (req: Request, res: Response, io: IOServer): Promise<void> => {
try {
const webhookEvent = req.body as MeetWebhookEvent;
@ -54,7 +53,7 @@ export const handleWebhook = async (
console.error('Error handling webhook:', error);
res.status(400).json({
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 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,
deleteAllRoomsCtrl,
deleteRoomCtrl,
getHome,
postCreateRoom
} from './controllers/homeController';
import { handleWebhook, joinRoom } from './controllers/roomController';
import { configService } from './services/configService';
@ -31,7 +31,6 @@ 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);
@ -45,13 +44,11 @@ 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(`Visit http://localhost:${PORT}/ to access the app`);
console.log('-----------------------------------------');
console.log('');
console.log(`Visit http://localhost:${PORT}/ to access the app`);
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

@ -1,4 +1,5 @@
import { del, get } from '../utils/http';
// @ts-ignore
import { MeetRecordingInfo } from '../../../typings/src/recording.model';
import { configService } from './configService';
@ -12,7 +13,7 @@ export const getAllRecordings = async (): Promise<{
pagination: any;
recordings: MeetRecordingInfo[];
}>(url, {
headers: { 'x-api-key': configService.meetApiKey },
headers: { 'x-api-key': configService.meetApiKey }
});
while (pagination.isTruncated) {
@ -21,21 +22,18 @@ export const getAllRecordings = async (): Promise<{
pagination: any;
recordings: MeetRecordingInfo[];
}>(nextPageUrl, {
headers: { 'x-api-key': configService.meetApiKey },
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(',')}`;
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 },
headers: { 'x-api-key': configService.meetApiKey }
});
};

View File

@ -1,4 +1,4 @@
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';
@ -9,33 +9,37 @@ export async function getAllRooms(): Promise<{
}> {
const url = `${configService.meetApiUrl}/rooms`;
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> {
const url = `${configService.meetApiUrl}/rooms`;
// 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();
} 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,
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 },
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 },
headers: { 'x-api-key': configService.meetApiKey }
});
}

View File

@ -4,28 +4,20 @@ export interface RequestOptions {
body?: any;
}
async function buildUrl(
url: string,
queryParams?: Record<string, string>
): Promise<string> {
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();
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> {
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,
body: options.body ? JSON.stringify(options.body) : undefined
};
const response = await fetch(fullUrl, fetchOptions);
if (!response.ok) {
const text = await response.text();
@ -41,30 +33,18 @@ async function request<T>(
return JSON.parse(text) as T;
}
export function get<T>(
url: string,
options?: Omit<RequestOptions, 'body'>
): Promise<T> {
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> {
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> {
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> {
export function del<T>(url: string, options?: Omit<RequestOptions, 'body'>): Promise<T> {
return request<T>('DELETE', url, options || {});
}