Refactors participant role updates to use moderation actions

Unifies backend and frontend handling of participant role changes by shifting from direct role assignment to moderation actions and UI badges. Simplifies signal payloads, removes token/secret mechanics, and clarifies notifications for moderator promotions and removals. Improves maintainability and aligns with updated moderation model.
This commit is contained in:
juancarmore 2026-03-06 11:41:15 +01:00
parent 1223e3d53b
commit f70bd04497
6 changed files with 62 additions and 65 deletions

View File

@ -1,3 +1,4 @@
import { MeetParticipantModerationAction } from '@openvidu-meet/typings';
import { Request, Response } from 'express';
import { container } from '../config/dependency-injector.config.js';
import { handleError } from '../models/error.model.js';
@ -34,14 +35,22 @@ export const updateParticipantRole = async (req: Request, res: Response) => {
const logger = container.get(LoggerService);
const roomMemberService = container.get(RoomMemberService);
const { roomId, participantIdentity } = req.params;
const { role } = req.body;
const { action } = req.body as { action: MeetParticipantModerationAction };
try {
logger.verbose(`Changing role of participant '${participantIdentity}' in room '${roomId}' to '${role}'`);
await roomMemberService.updateParticipantRole(roomId, participantIdentity, role);
res.status(200).json({ message: `Participant '${participantIdentity}' role updated to '${role}'` });
logger.verbose(
`Applying moderation action '${action}' for participant '${participantIdentity}' in room '${roomId}'`
);
await roomMemberService.updateParticipantRole(roomId, participantIdentity, action);
res.status(200).json({
message: `Moderation action '${action}' applied to participant '${participantIdentity}'`
});
} catch (error) {
handleError(res, error, `changing role for participant '${participantIdentity}' in room '${roomId}'`);
handleError(
res,
error,
`applying moderation action for participant '${participantIdentity}' in room '${roomId}'`
);
}
};

View File

@ -1,6 +1,6 @@
import { MeetRoomMemberRole } from '@openvidu-meet/typings';
import { MeetParticipantModerationAction } from '@openvidu-meet/typings';
import { z } from 'zod';
export const UpdateParticipantRoleReqSchema = z.object({
role: z.nativeEnum(MeetRoomMemberRole)
action: z.nativeEnum(MeetParticipantModerationAction)
});

View File

@ -3,7 +3,7 @@ import {
MeetRecordingInfo,
MeetRoom,
MeetRoomConfigUpdatedPayload,
MeetRoomMemberRole,
MeetRoomMemberUIBadge,
MeetSignalPayload,
MeetSignalType
} from '@openvidu-meet/typings';
@ -99,41 +99,24 @@ export class FrontendEventService {
async sendParticipantRoleUpdatedSignal(
roomId: string,
participantIdentity: string,
newRole: MeetRoomMemberRole,
secret: string
newBadge: MeetRoomMemberUIBadge
): Promise<void> {
this.logger.debug(
`Sending participant role updated signal for participant '${participantIdentity}' in room '${roomId}'`
);
const basePayload: MeetParticipantRoleUpdatedPayload = {
const signalPayload: MeetParticipantRoleUpdatedPayload = {
roomId,
participantIdentity,
newRole,
newBadge,
timestamp: Date.now()
};
const baseOptions: SendDataOptions = {
const singalOptions: SendDataOptions = {
topic: MeetSignalType.MEET_PARTICIPANT_ROLE_UPDATED
};
// Send signal with secret to the participant whose role has been updated
await this.sendSignal(
roomId,
{ ...basePayload, secret },
{ ...baseOptions, destinationIdentities: [participantIdentity] }
);
// Broadcast the role update to all other participants without the secret
const participants = await this.livekitService.listRoomParticipants(roomId);
const otherParticipantIdentities = participants
.filter((p) => p.identity !== participantIdentity)
.map((p) => p.identity);
await this.sendSignal(roomId, basePayload, {
...baseOptions,
destinationIdentities: otherParticipantIdentities
});
// Broadcast the role update to all participants in the meeting
await this.sendSignal(roomId, signalPayload, singalOptions);
}
/**

View File

@ -2,7 +2,7 @@ import { Injectable, inject } from '@angular/core';
import {
LeftEventReason,
MeetParticipantRoleUpdatedPayload,
MeetRoomMemberRole,
MeetRoomMemberUIBadge,
MeetSignalType,
WebComponentEvent,
WebComponentOutboundEventMessage
@ -20,9 +20,7 @@ import {
} from 'openvidu-components-angular';
import { NavigationService } from '../../../shared/services/navigation.service';
import { NotificationService } from '../../../shared/services/notification.service';
import { SessionStorageService } from '../../../shared/services/session-storage.service';
import { SoundService } from '../../../shared/services/sound.service';
import { TokenStorageService } from '../../../shared/services/token-storage.service';
import { RecordingService } from '../../recordings/services/recording.service';
import { RoomMemberContextService } from '../../room-members/services/room-member-context.service';
import { RoomFeatureService } from '../../rooms/services/room-feature.service';
@ -41,8 +39,6 @@ export class MeetingEventHandlerService {
protected roomFeatureService = inject(RoomFeatureService);
protected recordingService = inject(RecordingService);
protected roomMemberContextService = inject(RoomMemberContextService);
protected sessionStorageService = inject(SessionStorageService);
protected tokenStorageService = inject(TokenStorageService);
protected wcManagerService = inject(MeetingWebComponentManagerService);
protected navigationService = inject(NavigationService);
protected notificationService = inject(NotificationService);
@ -186,48 +182,55 @@ export class MeetingEventHandlerService {
* Obtains all necessary data from MeetingContextService.
*/
private async handleParticipantRoleUpdated(event: MeetParticipantRoleUpdatedPayload): Promise<void> {
const { participantIdentity, newRole, secret } = event;
const { participantIdentity, newBadge } = event;
const roomId = this.meetingContext.roomId();
const local = this.meetingContext.localParticipant();
const participantName = this.roomMemberContextService.participantName();
const isPromotedModerator = newBadge === MeetRoomMemberUIBadge.MODERATOR;
// Check if the role update is for the local participant
if (local && participantIdentity === local.identity) {
if (!secret || !roomId) return;
// Update room secret in context (without updating session storage)
this.meetingContext.setRoomSecret(secret);
if (!roomId) return;
try {
// Refresh participant token with new role
// Refresh room member token to get updated permissions based on new role
await this.roomMemberContextService.generateToken(roomId, {
secret,
joinMeeting: true,
participantName,
participantIdentity
participantIdentity,
useParticipantMetadata: true
});
// Update local participant role
local.meetRole = newRole;
this.showParticipantRoleUpdatedNotification(newRole);
local.meetBadge = newBadge;
local.promotedModerator = isPromotedModerator;
this.roomMemberContextService.setPromotedModerator(isPromotedModerator);
this.showParticipantRoleUpdatedNotification(isPromotedModerator);
} catch (error) {
console.error('Error refreshing room member token:', error);
}
} else {
// Update remote participant role
// Update remote participant badge
const remoteParticipants = this.meetingContext.remoteParticipants();
const participant = remoteParticipants.find((p) => p.identity === participantIdentity);
if (participant) {
participant.meetRole = newRole;
participant.meetBadge = newBadge;
participant.promotedModerator = isPromotedModerator;
}
}
}
private showParticipantRoleUpdatedNotification(newRole: MeetRoomMemberRole): void {
this.notificationService.showSnackbar(`You have been assigned the role of ${newRole.toUpperCase()}`);
newRole === MeetRoomMemberRole.MODERATOR
? this.soundService.playParticipantRoleUpgradedSound()
: this.soundService.playParticipantRoleDowngradedSound();
private showParticipantRoleUpdatedNotification(isPromotedModerator: boolean): void {
const message = isPromotedModerator
? 'You have been promoted to moderator'
: 'Your moderator role has been removed';
this.notificationService.showSnackbar(message);
if (isPromotedModerator) {
this.soundService.playParticipantRoleUpgradedSound();
} else {
this.soundService.playParticipantRoleDowngradedSound();
}
}
/**

View File

@ -1,6 +1,6 @@
import { Clipboard } from '@angular/cdk/clipboard';
import { inject, Injectable } from '@angular/core';
import { MeetRoom } from '@openvidu-meet/typings';
import { MeetParticipantModerationAction, MeetRoom } from '@openvidu-meet/typings';
import { LoggerService } from 'openvidu-components-angular';
import { HttpService } from '../../../shared/services/http.service';
import { NotificationService } from '../../../shared/services/notification.service';
@ -55,12 +55,16 @@ export class MeetingService {
*
* @param roomId - The unique identifier of the meeting room
* @param participantIdentity - The identity of the participant whose role is to be changed
* @param newRole - The new role to be assigned to the participant
* @param action - Moderation action to apply
*/
async changeParticipantRole(roomId: string, participantIdentity: string, newRole: string): Promise<void> {
async changeParticipantRole(
roomId: string,
participantIdentity: string,
action: MeetParticipantModerationAction
): Promise<void> {
const path = `${this.MEETINGS_API}/${roomId}/participants/${participantIdentity}/role`;
const body = { role: newRole };
const body = { action };
await this.httpService.putRequest(path, body);
this.log.d(`Changed role of participant '${participantIdentity}' to '${newRole}' in room '${roomId}'`);
this.log.d(`Applied moderation action '${action}' to participant '${participantIdentity}' in room '${roomId}'`);
}
}

View File

@ -1,5 +1,5 @@
import { MeetRoomConfig } from './database/room-config.js';
import { MeetRoomMemberRole } from './database/room-member.entity.js';
import { MeetRoomMemberUIBadge } from './response/room-member-response.js';
/**
* Interface representing a signal emitted by OpenVidu Meet to notify clients about real-time updates in the meeting.
@ -26,17 +26,15 @@ export interface MeetRoomConfigUpdatedPayload {
/**
* Payload for MEET_PARTICIPANT_ROLE_UPDATED signal,
* containing information about the participant whose role was updated and the new role.
* containing information about the participant whose role was updated and the new badge.
*/
export interface MeetParticipantRoleUpdatedPayload {
/** ID of the room where the participant's role was updated */
roomId: string;
/** Identity of the participant whose role was updated */
participantIdentity: string;
/** New role assigned to the participant */
newRole: MeetRoomMemberRole;
/** Optional secret for regenerating the participant's token if needed */
secret?: string;
/** New badge assigned to the participant */
newBadge: MeetRoomMemberUIBadge;
/** Timestamp in milliseconds when the role update occurred */
timestamp: number;
}