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}'`);
|
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'
|
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 {
|
export class OpenViduComponentsAdapterHelper {
|
||||||
private constructor() {
|
private constructor() {
|
||||||
// Prevent instantiation of this utility class
|
// Prevent instantiation of this utility class
|
||||||
@ -29,7 +50,7 @@ export class OpenViduComponentsAdapterHelper {
|
|||||||
|
|
||||||
static generateRoomStatusSignal(recordingInfo: MeetRecordingInfo[], participantSid?: string) {
|
static generateRoomStatusSignal(recordingInfo: MeetRecordingInfo[], participantSid?: string) {
|
||||||
const isRecordingActive = recordingInfo.some((rec) => rec.status === MeetRecordingStatus.ACTIVE);
|
const isRecordingActive = recordingInfo.some((rec) => rec.status === MeetRecordingStatus.ACTIVE);
|
||||||
const payload = {
|
const payload: RoomStatusSignalPayload = {
|
||||||
isRecordingStarted: isRecordingActive,
|
isRecordingStarted: isRecordingActive,
|
||||||
recordingList: recordingInfo.map((rec) =>
|
recordingList: recordingInfo.map((rec) =>
|
||||||
OpenViduComponentsAdapterHelper.parseRecordingInfoToOpenViduComponents(rec)
|
OpenViduComponentsAdapterHelper.parseRecordingInfoToOpenViduComponents(rec)
|
||||||
@ -46,7 +67,7 @@ export class OpenViduComponentsAdapterHelper {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private static parseRecordingInfoToOpenViduComponents(info: MeetRecordingInfo) {
|
private static parseRecordingInfoToOpenViduComponents(info: MeetRecordingInfo): RecordingSignalPayload {
|
||||||
return {
|
return {
|
||||||
id: info.recordingId,
|
id: info.recordingId,
|
||||||
roomName: info.roomId,
|
roomName: info.roomId,
|
||||||
|
|||||||
@ -1,7 +1,12 @@
|
|||||||
import { AuthMode, ParticipantOptions, ParticipantRole, UserRole } from '@typings-ce';
|
import { AuthMode, ParticipantOptions, ParticipantRole, UserRole } from '@typings-ce';
|
||||||
import { NextFunction, Request, Response } from 'express';
|
import { NextFunction, Request, Response } from 'express';
|
||||||
import { container } from '../config/index.js';
|
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 { MeetStorageService, RoomService } from '../services/index.js';
|
||||||
import { allowAnonymous, tokenAndRoleValidator, withAuth } from './auth.middleware.js';
|
import { allowAnonymous, tokenAndRoleValidator, withAuth } from './auth.middleware.js';
|
||||||
|
|
||||||
@ -91,3 +96,22 @@ export const checkParticipantFromSameRoom = async (req: Request, res: Response,
|
|||||||
|
|
||||||
return next();
|
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 { Router } from 'express';
|
||||||
import * as meetingCtrl from '../controllers/meeting.controller.js';
|
import * as meetingCtrl from '../controllers/meeting.controller.js';
|
||||||
import * as participantCtrl from '../controllers/participant.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();
|
export const internalMeetingRouter = Router();
|
||||||
internalMeetingRouter.use(bodyParser.urlencoded({ extended: true }));
|
internalMeetingRouter.use(bodyParser.urlencoded({ extended: true }));
|
||||||
@ -13,11 +13,22 @@ internalMeetingRouter.delete(
|
|||||||
'/:roomId',
|
'/:roomId',
|
||||||
withAuth(participantTokenValidator),
|
withAuth(participantTokenValidator),
|
||||||
withModeratorPermissions,
|
withModeratorPermissions,
|
||||||
|
withValidRoomId,
|
||||||
meetingCtrl.endMeeting
|
meetingCtrl.endMeeting
|
||||||
);
|
);
|
||||||
internalMeetingRouter.delete(
|
internalMeetingRouter.delete(
|
||||||
'/:roomId/participants/:participantName',
|
'/:roomId/participants/:participantName',
|
||||||
withAuth(participantTokenValidator),
|
withAuth(participantTokenValidator),
|
||||||
withModeratorPermissions,
|
withModeratorPermissions,
|
||||||
|
withValidRoomId,
|
||||||
participantCtrl.deleteParticipant
|
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 { inject, injectable } from 'inversify';
|
||||||
import { SendDataOptions } from 'livekit-server-sdk';
|
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 { 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
|
* 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}`);
|
this.logger.debug(`Sending room preferences updated signal for room ${roomId}`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const payload = {
|
const payload: MeetRoomPreferencesUpdatedPayload = {
|
||||||
roomId,
|
roomId,
|
||||||
preferences: updatedRoom.preferences
|
preferences: updatedRoom.preferences!,
|
||||||
|
timestamp: Date.now()
|
||||||
};
|
};
|
||||||
|
|
||||||
const options: SendDataOptions = {
|
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
|
* Generic method to send signals to the frontend
|
||||||
*/
|
*/
|
||||||
|
|
||||||
protected async sendSignal(
|
protected async sendSignal(
|
||||||
roomId: string,
|
roomId: string,
|
||||||
rawData: Record<string, unknown>,
|
rawData: MeetSignalPayload | OpenViduComponentsSignalPayload,
|
||||||
options: SendDataOptions
|
options: SendDataOptions
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
this.logger.verbose(`Notifying participants in room ${roomId}: "${options.topic}".`);
|
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> {
|
async deleteParticipant(participantName: string, roomName: string): Promise<void> {
|
||||||
const participantExists = await this.participantExists(roomName, participantName);
|
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 { inject, injectable } from 'inversify';
|
||||||
import { ParticipantInfo } from 'livekit-server-sdk';
|
import { ParticipantInfo } from 'livekit-server-sdk';
|
||||||
import { errorParticipantAlreadyExists, errorParticipantNotFound } from '../models/error.model.js';
|
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()
|
@injectable()
|
||||||
export class ParticipantService {
|
export class ParticipantService {
|
||||||
@ -10,6 +17,7 @@ export class ParticipantService {
|
|||||||
@inject(LoggerService) protected logger: LoggerService,
|
@inject(LoggerService) protected logger: LoggerService,
|
||||||
@inject(RoomService) protected roomService: RoomService,
|
@inject(RoomService) protected roomService: RoomService,
|
||||||
@inject(LiveKitService) protected livekitService: LiveKitService,
|
@inject(LiveKitService) protected livekitService: LiveKitService,
|
||||||
|
@inject(FrontendEventService) protected frontendEventService: FrontendEventService,
|
||||||
@inject(TokenService) protected tokenService: TokenService
|
@inject(TokenService) protected tokenService: TokenService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@ -53,7 +61,7 @@ export class ParticipantService {
|
|||||||
currentRoles.push({ role, permissions: permissions.openvidu });
|
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> {
|
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 {
|
return {
|
||||||
livekit: {
|
livekit: {
|
||||||
roomJoin: addJoinPermission,
|
roomJoin: addJoinPermission,
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
LiveKitPermissions,
|
LiveKitPermissions,
|
||||||
|
MeetTokenMetadata,
|
||||||
OpenViduMeetPermissions,
|
OpenViduMeetPermissions,
|
||||||
ParticipantOptions,
|
ParticipantOptions,
|
||||||
ParticipantRole,
|
ParticipantRole,
|
||||||
@ -42,19 +43,22 @@ export class TokenService {
|
|||||||
async generateParticipantToken(
|
async generateParticipantToken(
|
||||||
participantOptions: ParticipantOptions,
|
participantOptions: ParticipantOptions,
|
||||||
lkPermissions: LiveKitPermissions,
|
lkPermissions: LiveKitPermissions,
|
||||||
roles: { role: ParticipantRole; permissions: OpenViduMeetPermissions }[]
|
roles: { role: ParticipantRole; permissions: OpenViduMeetPermissions }[],
|
||||||
|
selectedRole: ParticipantRole
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
const { roomId, participantName } = participantOptions;
|
const { roomId, participantName } = participantOptions;
|
||||||
this.logger.info(`Generating token for room '${roomId}'`);
|
this.logger.info(`Generating token for room '${roomId}'`);
|
||||||
|
|
||||||
|
const metadata: MeetTokenMetadata = {
|
||||||
|
livekitUrl: LIVEKIT_URL,
|
||||||
|
roles,
|
||||||
|
selectedRole
|
||||||
|
};
|
||||||
const tokenOptions: AccessTokenOptions = {
|
const tokenOptions: AccessTokenOptions = {
|
||||||
identity: participantName,
|
identity: participantName,
|
||||||
name: participantName,
|
name: participantName,
|
||||||
ttl: INTERNAL_CONFIG.PARTICIPANT_TOKEN_EXPIRATION,
|
ttl: INTERNAL_CONFIG.PARTICIPANT_TOKEN_EXPIRATION,
|
||||||
metadata: JSON.stringify({
|
metadata: JSON.stringify(metadata)
|
||||||
livekitUrl: LIVEKIT_URL,
|
|
||||||
roles
|
|
||||||
})
|
|
||||||
};
|
};
|
||||||
return await this.generateJwtToken(tokenOptions, lkPermissions as VideoGrant);
|
return await this.generateJwtToken(tokenOptions, lkPermissions as VideoGrant);
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user