Files
vidconf/frontend/src/components/room/ForcedMuteWatcher.tsx
Max Ronzhin 4c60e092e5
Some checks failed
CI / backend (push) Has been cancelled
CI / frontend (push) Has been cancelled
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.
2026-08-01 22:07:06 +03:00

41 lines
2.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useEffect } from 'react'
import { useLocalParticipant } from '@livekit/components-react'
import { useToast } from '@/components/ui/ToastProvider'
import type { ForcedMuteEvent } from '@/hooks/useChat'
/**
* Уведомляет ЛОКАЛЬНОГО участника тостом, когда организатор принудительно
* выключил его микрофон/камеру (задача B2). Рендерится безусловно внутри
* `<LiveKitRoom>` — `useLocalParticipant` недоступен снаружи (`RoomPage`
* сам вне контекста LiveKit, см. докстринг `useIsOrganizer`).
*
* Само выключение трека организатор делает СЕРВЕРНЫМ вызовом LiveKit API
* (`services/room_control.py`) — тулбарные кнопки (useTrackToggle) сами
* отразят новое состояние по родному событию LiveKit `TrackMuted`, этот
* компонент только поясняет ПОЧЕМУ: без тоста человек не отличил бы
* действие организатора от случайного глюка. Участник может включить себя
* обратно сразу тем же тулбаром — сервер это не блокирует (см. докстринг B2
* в CHANGELOG/коммите).
*/
export function ForcedMuteWatcher({ event }: { event: ForcedMuteEvent | null }) {
const { localParticipant } = useLocalParticipant()
const toast = useToast()
useEffect(() => {
if (!event || event.identity !== localParticipant.identity) return
toast.show(
event.source === 'microphone'
? 'Организатор выключил ваш микрофон'
: 'Организатор выключил вашу камеру',
'info',
)
// `event` (включая `nonce`) — единственная зависимость, которая должна
// повторно показывать тост; `localParticipant`/`toast` стабильны в
// рамках подключения и намеренно не входят в список, чтобы их
// пересоздание (если когда-нибудь случится) не дублировало уведомление.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [event])
return null
}