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:
40
frontend/src/components/room/ForcedMuteWatcher.tsx
Normal file
40
frontend/src/components/room/ForcedMuteWatcher.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user