backend: implement participant role management and update signals
This commit is contained in:
parent
9d9cb3d5b4
commit
b6acebfa18
@ -132,3 +132,18 @@ export const deleteParticipant = async (req: Request, res: Response) => {
|
||||
handleError(res, error, `deleting participant '${participantName}' from room '${roomId}'`);
|
||||
}
|
||||
};
|
||||
|
||||
export const changeParticipantRole = async (req: Request, res: Response) => {
|
||||
const logger = container.get(LoggerService);
|
||||
const participantService = container.get(ParticipantService);
|
||||
const { roomId, participantName } = req.params;
|
||||
const { role } = req.body;
|
||||
|
||||
try {
|
||||
logger.verbose(`Changing role of participant '${participantName}' in room '${roomId}' to '${role}'`);
|
||||
await participantService.changeParticipantRole(roomId, participantName, role);
|
||||
res.status(200).json({ message: `Participant '${participantName}' role updated to ${role}` });
|
||||
} catch (error) {
|
||||
handleError(res, error, `changing role for participant '${participantName}' in room '${roomId}'`);
|
||||
}
|
||||
};
|
||||
|
||||
@ -12,6 +12,27 @@ const enum OpenViduComponentsDataTopic {
|
||||
ROOM_STATUS = 'roomStatus'
|
||||
}
|
||||
|
||||
interface RecordingSignalPayload {
|
||||
id: string;
|
||||
roomName: string;
|
||||
roomId: string;
|
||||
status: string;
|
||||
filename?: string;
|
||||
startedAt?: number;
|
||||
endedAt?: number;
|
||||
duration?: number;
|
||||
size?: number;
|
||||
location?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
interface RoomStatusSignalPayload {
|
||||
isRecordingStarted: boolean;
|
||||
recordingList: RecordingSignalPayload[];
|
||||
}
|
||||
|
||||
export type OpenViduComponentsSignalPayload = RecordingSignalPayload | RoomStatusSignalPayload;
|
||||
|
||||
export class OpenViduComponentsAdapterHelper {
|
||||
private constructor() {
|
||||
// Prevent instantiation of this utility class
|
||||
@ -29,7 +50,7 @@ export class OpenViduComponentsAdapterHelper {
|
||||
|
||||
static generateRoomStatusSignal(recordingInfo: MeetRecordingInfo[], participantSid?: string) {
|
||||
const isRecordingActive = recordingInfo.some((rec) => rec.status === MeetRecordingStatus.ACTIVE);
|
||||
const payload = {
|
||||
const payload: RoomStatusSignalPayload = {
|
||||
isRecordingStarted: isRecordingActive,
|
||||
recordingList: recordingInfo.map((rec) =>
|
||||
OpenViduComponentsAdapterHelper.parseRecordingInfoToOpenViduComponents(rec)
|
||||
@ -46,7 +67,7 @@ export class OpenViduComponentsAdapterHelper {
|
||||
};
|
||||
}
|
||||
|
||||
private static parseRecordingInfoToOpenViduComponents(info: MeetRecordingInfo) {
|
||||
private static parseRecordingInfoToOpenViduComponents(info: MeetRecordingInfo): RecordingSignalPayload {
|
||||
return {
|
||||
id: info.recordingId,
|
||||
roomName: info.roomId,
|
||||
|
||||
@ -1,7 +1,12 @@
|
||||
import { AuthMode, ParticipantOptions, ParticipantRole, UserRole } from '@typings-ce';
|
||||
import { NextFunction, Request, Response } from 'express';
|
||||
import { container } from '../config/index.js';
|
||||
import { errorInsufficientPermissions, handleError, rejectRequestFromMeetError } from '../models/error.model.js';
|
||||
import {
|
||||
errorInsufficientPermissions,
|
||||
errorInvalidParticipantRole,
|
||||
handleError,
|
||||
rejectRequestFromMeetError
|
||||
} from '../models/error.model.js';
|
||||
import { MeetStorageService, RoomService } from '../services/index.js';
|
||||
import { allowAnonymous, tokenAndRoleValidator, withAuth } from './auth.middleware.js';
|
||||
|
||||
@ -91,3 +96,22 @@ export const checkParticipantFromSameRoom = async (req: Request, res: Response,
|
||||
|
||||
return next();
|
||||
};
|
||||
|
||||
export const withValidParticipantRole = async (req: Request, res: Response, next: NextFunction) => {
|
||||
const { role } = req.body;
|
||||
|
||||
if (!role) {
|
||||
const error = errorInvalidParticipantRole();
|
||||
return rejectRequestFromMeetError(res, error);
|
||||
}
|
||||
|
||||
// Validate the role against the ParticipantRole enum
|
||||
const isRoleValid = role === ParticipantRole.MODERATOR || role === ParticipantRole.PUBLISHER;
|
||||
|
||||
if (!isRoleValid) {
|
||||
const error = errorInvalidParticipantRole();
|
||||
return rejectRequestFromMeetError(res, error);
|
||||
}
|
||||
|
||||
return next();
|
||||
};
|
||||
|
||||
@ -2,7 +2,7 @@ import bodyParser from 'body-parser';
|
||||
import { Router } from 'express';
|
||||
import * as meetingCtrl from '../controllers/meeting.controller.js';
|
||||
import * as participantCtrl from '../controllers/participant.controller.js';
|
||||
import { participantTokenValidator, withAuth, withModeratorPermissions } from '../middlewares/index.js';
|
||||
import { participantTokenValidator, withAuth, withModeratorPermissions, withValidParticipantRole, withValidRoomId } from '../middlewares/index.js';
|
||||
|
||||
export const internalMeetingRouter = Router();
|
||||
internalMeetingRouter.use(bodyParser.urlencoded({ extended: true }));
|
||||
@ -13,11 +13,22 @@ internalMeetingRouter.delete(
|
||||
'/:roomId',
|
||||
withAuth(participantTokenValidator),
|
||||
withModeratorPermissions,
|
||||
withValidRoomId,
|
||||
meetingCtrl.endMeeting
|
||||
);
|
||||
internalMeetingRouter.delete(
|
||||
'/:roomId/participants/:participantName',
|
||||
withAuth(participantTokenValidator),
|
||||
withModeratorPermissions,
|
||||
withValidRoomId,
|
||||
participantCtrl.deleteParticipant
|
||||
);
|
||||
|
||||
internalMeetingRouter.patch(
|
||||
'/:roomId/participants/:participantName',
|
||||
withAuth(participantTokenValidator),
|
||||
withModeratorPermissions,
|
||||
withValidRoomId,
|
||||
withValidParticipantRole,
|
||||
participantCtrl.changeParticipantRole
|
||||
);
|
||||
|
||||
@ -1,9 +1,14 @@
|
||||
import { MeetRoom, MeetRecordingInfo } from '@typings-ce';
|
||||
import { MeetRoom, MeetRecordingInfo, ParticipantRole } from '@typings-ce';
|
||||
import { inject, injectable } from 'inversify';
|
||||
import { SendDataOptions } from 'livekit-server-sdk';
|
||||
import { OpenViduComponentsAdapterHelper } from '../helpers/index.js';
|
||||
import { OpenViduComponentsAdapterHelper, OpenViduComponentsSignalPayload } from '../helpers/index.js';
|
||||
import { LiveKitService, LoggerService } from './index.js';
|
||||
import { MeetSignalType } from '../typings/ce/event.model.js';
|
||||
import {
|
||||
MeetParticipantRoleUpdatedPayload,
|
||||
MeetRoomPreferencesUpdatedPayload,
|
||||
MeetSignalPayload,
|
||||
MeetSignalType
|
||||
} from '../typings/ce/event.model.js';
|
||||
|
||||
/**
|
||||
* Service responsible for all communication with the frontend
|
||||
@ -68,9 +73,10 @@ export class FrontendEventService {
|
||||
this.logger.debug(`Sending room preferences updated signal for room ${roomId}`);
|
||||
|
||||
try {
|
||||
const payload = {
|
||||
const payload: MeetRoomPreferencesUpdatedPayload = {
|
||||
roomId,
|
||||
preferences: updatedRoom.preferences
|
||||
preferences: updatedRoom.preferences!,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
|
||||
const options: SendDataOptions = {
|
||||
@ -83,13 +89,37 @@ export class FrontendEventService {
|
||||
}
|
||||
}
|
||||
|
||||
async sendParticipantRoleUpdatedSignal(
|
||||
roomId: string,
|
||||
participantName: string,
|
||||
newRole: ParticipantRole,
|
||||
secret: string
|
||||
): Promise<void> {
|
||||
this.logger.debug(
|
||||
`Sending participant role updated signal for participant ${participantName} in room ${roomId}`
|
||||
);
|
||||
const payload: MeetParticipantRoleUpdatedPayload = {
|
||||
participantName,
|
||||
roomId,
|
||||
newRole,
|
||||
secret,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
|
||||
const options: SendDataOptions = {
|
||||
topic: MeetSignalType.MEET_PARTICIPANT_ROLE_UPDATED
|
||||
};
|
||||
|
||||
await this.sendSignal(roomId, payload, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic method to send signals to the frontend
|
||||
*/
|
||||
|
||||
protected async sendSignal(
|
||||
roomId: string,
|
||||
rawData: Record<string, unknown>,
|
||||
rawData: MeetSignalPayload | OpenViduComponentsSignalPayload,
|
||||
options: SendDataOptions
|
||||
): Promise<void> {
|
||||
this.logger.verbose(`Notifying participants in room ${roomId}: "${options.topic}".`);
|
||||
|
||||
@ -184,6 +184,29 @@ export class LiveKitService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the metadata of a participant in a LiveKit room.
|
||||
*
|
||||
* @param roomName - The name of the room where the participant is located
|
||||
* @param participantName - The name of the participant whose metadata will be updated
|
||||
* @param metadata - The new metadata to set for the participant
|
||||
* @returns A Promise that resolves when the metadata has been successfully updated
|
||||
* @throws An internal error if there is an issue updating the metadata
|
||||
*/
|
||||
async updateParticipantMetadata(
|
||||
roomName: string,
|
||||
participantName: string,
|
||||
metadata: string
|
||||
): Promise<void> {
|
||||
try {
|
||||
await this.roomClient.updateParticipant(roomName, participantName, metadata);
|
||||
this.logger.verbose(`Updated metadata for participant ${participantName} in room ${roomName}`);
|
||||
} catch (error) {
|
||||
this.logger.error(`Error updating metadata for participant ${participantName} in room ${roomName}: ${error}`);
|
||||
throw internalError(`updating metadata for participant '${participantName}' in room '${roomName}'`);
|
||||
}
|
||||
}
|
||||
|
||||
async deleteParticipant(participantName: string, roomName: string): Promise<void> {
|
||||
const participantExists = await this.participantExists(roomName, participantName);
|
||||
|
||||
|
||||
@ -1,8 +1,15 @@
|
||||
import { OpenViduMeetPermissions, ParticipantOptions, ParticipantPermissions, ParticipantRole } from '@typings-ce';
|
||||
import {
|
||||
MeetTokenMetadata,
|
||||
OpenViduMeetPermissions,
|
||||
ParticipantOptions,
|
||||
ParticipantPermissions,
|
||||
ParticipantRole
|
||||
} from '@typings-ce';
|
||||
import { inject, injectable } from 'inversify';
|
||||
import { ParticipantInfo } from 'livekit-server-sdk';
|
||||
import { errorParticipantAlreadyExists, errorParticipantNotFound } from '../models/error.model.js';
|
||||
import { LiveKitService, LoggerService, RoomService, TokenService } from './index.js';
|
||||
import { FrontendEventService, LiveKitService, LoggerService, RoomService, TokenService } from './index.js';
|
||||
import { MeetRoomHelper } from '../helpers/room.helper.js';
|
||||
|
||||
@injectable()
|
||||
export class ParticipantService {
|
||||
@ -10,6 +17,7 @@ export class ParticipantService {
|
||||
@inject(LoggerService) protected logger: LoggerService,
|
||||
@inject(RoomService) protected roomService: RoomService,
|
||||
@inject(LiveKitService) protected livekitService: LiveKitService,
|
||||
@inject(FrontendEventService) protected frontendEventService: FrontendEventService,
|
||||
@inject(TokenService) protected tokenService: TokenService
|
||||
) {}
|
||||
|
||||
@ -53,7 +61,7 @@ export class ParticipantService {
|
||||
currentRoles.push({ role, permissions: permissions.openvidu });
|
||||
}
|
||||
|
||||
return this.tokenService.generateParticipantToken(participantOptions, permissions.livekit, currentRoles);
|
||||
return this.tokenService.generateParticipantToken(participantOptions, permissions.livekit, currentRoles, role);
|
||||
}
|
||||
|
||||
async getParticipant(roomId: string, participantName: string): Promise<ParticipantInfo | null> {
|
||||
@ -89,7 +97,43 @@ export class ParticipantService {
|
||||
}
|
||||
}
|
||||
|
||||
protected generateModeratorPermissions(roomId: string, addJoinPermission = true): ParticipantPermissions {
|
||||
async changeParticipantRole(roomId: string, participantName: string, newRole: ParticipantRole): Promise<void> {
|
||||
try {
|
||||
const meetRoom = await this.roomService.getMeetRoom(roomId);
|
||||
|
||||
const participant = await this.getParticipant(roomId, participantName);
|
||||
|
||||
const metadata: MeetTokenMetadata = this.parseMetadata(participant!.metadata);
|
||||
|
||||
if (!metadata || typeof metadata !== 'object') {
|
||||
throw new Error(`Invalid metadata for participant ${participantName}`);
|
||||
}
|
||||
|
||||
// TODO: Should we update the roles array as well?
|
||||
metadata.selectedRole = newRole;
|
||||
|
||||
await this.livekitService.updateParticipantMetadata(roomId, participantName, JSON.stringify(metadata));
|
||||
|
||||
const { publisherSecret, moderatorSecret } = MeetRoomHelper.extractSecretsFromRoom(meetRoom);
|
||||
|
||||
const secret = newRole === ParticipantRole.MODERATOR ? moderatorSecret : publisherSecret;
|
||||
await this.frontendEventService.sendParticipantRoleUpdatedSignal(roomId, participantName, newRole, secret);
|
||||
} catch (error) {
|
||||
this.logger.error('Error changing participant role:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
protected parseMetadata(metadata: string): MeetTokenMetadata {
|
||||
try {
|
||||
return JSON.parse(metadata);
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to parse participant metadata:', error);
|
||||
throw new Error('Invalid participant metadata format');
|
||||
}
|
||||
}
|
||||
|
||||
protected generateModeratorPermissions(roomId: string): ParticipantPermissions {
|
||||
return {
|
||||
livekit: {
|
||||
roomJoin: addJoinPermission,
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import {
|
||||
LiveKitPermissions,
|
||||
MeetTokenMetadata,
|
||||
OpenViduMeetPermissions,
|
||||
ParticipantOptions,
|
||||
ParticipantRole,
|
||||
@ -42,19 +43,22 @@ export class TokenService {
|
||||
async generateParticipantToken(
|
||||
participantOptions: ParticipantOptions,
|
||||
lkPermissions: LiveKitPermissions,
|
||||
roles: { role: ParticipantRole; permissions: OpenViduMeetPermissions }[]
|
||||
roles: { role: ParticipantRole; permissions: OpenViduMeetPermissions }[],
|
||||
selectedRole: ParticipantRole
|
||||
): Promise<string> {
|
||||
const { roomId, participantName } = participantOptions;
|
||||
this.logger.info(`Generating token for room '${roomId}'`);
|
||||
|
||||
const metadata: MeetTokenMetadata = {
|
||||
livekitUrl: LIVEKIT_URL,
|
||||
roles,
|
||||
selectedRole
|
||||
};
|
||||
const tokenOptions: AccessTokenOptions = {
|
||||
identity: participantName,
|
||||
name: participantName,
|
||||
ttl: INTERNAL_CONFIG.PARTICIPANT_TOKEN_EXPIRATION,
|
||||
metadata: JSON.stringify({
|
||||
livekitUrl: LIVEKIT_URL,
|
||||
roles
|
||||
})
|
||||
metadata: JSON.stringify(metadata)
|
||||
};
|
||||
return await this.generateJwtToken(tokenOptions, lkPermissions as VideoGrant);
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user