feat(room): принудительный мьют участника организатором
Новый эндпоинт POST /conferences/{id}/mute-participant: права проверяются
ЗАНОВО по владельцу конференции в БД (ConferenceService.mute_participant),
не по метаданным LiveKit-токена вызывающего — те лишь подсказка для UI и
потенциально подделываемы клиентом. Обычный участник получает 403, чужая/
несуществующая конференция — 404, участник не в комнате LiveKit — отдельный
404 (participant_not_in_room).
Само выключение — серверный вызов api.LiveKitAPI (services/room_control.py,
тот же паттерн, что services/egress.py): backend аутентифицируется
СОБСТВЕННЫМИ api_key/api_secret, а не токеном организатора, поэтому
дополнительный LiveKit-грант в токене организатора не нужен — мьютит сервер
от своего имени. Если трек данного source не опубликован (с 0.0.15 участники
заходят с выключенными микрофоном/камерой) — не ошибка, а no-op: искомое
состояние уже достигнуто, ответ muted:false.
Уведомление участника — тот же общий канал комнаты, что и очередь рук
(hand_queue_channel): рассылается всем, получатель сам сверяет identity
(ForcedMuteWatcher, рендерится внутри LiveKitRoom). Само выключение трека
участник видит сразу через штатный useTrackToggle (LiveKit сам присылает
TrackMuted), тост только поясняет причину — иначе не отличить от глюка.
Включить себя обратно можно сразу тем же тулбаром, сервер это не блокирует.
Кнопки — на чужой плитке камеры, видны только организатору по наведению
(на тач-устройствах — всегда, как и булавка закрепления).
Тесты: владелец мьютит успешно и публикует broadcast, уже-выключенный трек —
muted:false без broadcast, администратор мьютит чужую конференцию, обычный
участник получает 403 без обращения к LiveKit, конференция не найдена и
участник не в комнате — соответствующие 404.
This commit is contained in:
@@ -145,6 +145,14 @@ export interface ConferenceGuestJoinPayload {
|
||||
password?: string
|
||||
}
|
||||
|
||||
/** Источник трека, который организатор может принудительно выключить (задача B2). */
|
||||
export type MuteSource = 'microphone' | 'camera'
|
||||
|
||||
/** Ответ на принудительный мьют — `false`, если трек и так не был опубликован (нечего было мьютить). */
|
||||
export interface MuteParticipantResult {
|
||||
muted: boolean
|
||||
}
|
||||
|
||||
/** Тело частичного обновления конференции — те же поля, что и при создании, все опциональны. */
|
||||
export type ConferenceUpdatePayload = Partial<ConferenceCreatePayload>
|
||||
|
||||
@@ -202,6 +210,23 @@ export async function guestJoinConference(
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Принудительно выключить микрофон/камеру участника (задача B2) — только
|
||||
* владелец конференции/администратор, иначе 403 (`not_owner`). 404
|
||||
* (`participant_not_in_room`) — участника с таким `identity` сейчас нет в
|
||||
* комнате LiveKit.
|
||||
*/
|
||||
export async function muteParticipant(
|
||||
conferenceId: string,
|
||||
identity: string,
|
||||
source: MuteSource,
|
||||
): Promise<MuteParticipantResult> {
|
||||
return apiRequest<MuteParticipantResult>(`/conferences/${conferenceId}/mute-participant`, {
|
||||
method: 'POST',
|
||||
body: { identity, source },
|
||||
})
|
||||
}
|
||||
|
||||
/** Список «моих» конференций — закреплённые (повторяющиеся) и предстоящие разовые владельца. */
|
||||
export async function getMyConferences(): Promise<ConferenceOut[]> {
|
||||
return apiRequest<ConferenceOut[]>('/conferences/my')
|
||||
|
||||
Reference in New Issue
Block a user