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:
@@ -33,12 +33,14 @@ from schemas.conferences import (
|
||||
JoinOut,
|
||||
OccurrenceOut,
|
||||
)
|
||||
from services import hand_queue
|
||||
from services.avatars import avatar_url as resolve_avatar_url
|
||||
from services.conference_access import build_join, ensure_joinable
|
||||
from services.conference_ids import generate_number, generate_slug
|
||||
from services.instance_settings import InstanceSettingsService
|
||||
from services.invitations_producer import enqueue_invitations
|
||||
from services.recurrence import RecurrenceRule, expand_occurrences
|
||||
from services.room_control import MuteSource, mute_participant_track
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -271,6 +273,36 @@ class ConferenceService:
|
||||
chat_enabled=chat_enabled,
|
||||
)
|
||||
|
||||
async def mute_participant(
|
||||
self,
|
||||
conference_id: uuid.UUID,
|
||||
*,
|
||||
actor: User,
|
||||
target_identity: str,
|
||||
source: MuteSource,
|
||||
) -> bool:
|
||||
"""Принудительно замьютить трек участника (задача B2); владелец/администратор.
|
||||
|
||||
Права — ТОЛЬКО отсюда (`_ensure_owner_or_admin` по `conference.owner_id`
|
||||
в БД), не по метаданным LiveKit-токена вызывающего: те лишь подсказка
|
||||
для UI (см. `services/conference_access.py::build_join`) и потенциально
|
||||
подделываемы клиентом. `target_identity` НИКАК не валидируется против
|
||||
состава участников заранее — если его сейчас нет в комнате LiveKit,
|
||||
`mute_participant_track` бросит `ParticipantNotInRoomError` (ловит
|
||||
API-роутер).
|
||||
"""
|
||||
conference = await self._get_or_raise(conference_id)
|
||||
self._ensure_owner_or_admin(conference, actor)
|
||||
|
||||
muted = await mute_participant_track(
|
||||
conference.slug, identity=target_identity, source=source
|
||||
)
|
||||
if muted:
|
||||
await hand_queue.publish_forced_mute(
|
||||
conference.id, identity=target_identity, source=source
|
||||
)
|
||||
return muted
|
||||
|
||||
async def update(
|
||||
self, conference_id: uuid.UUID, *, actor: User, data: ConferenceUpdateIn
|
||||
) -> Conference:
|
||||
|
||||
Reference in New Issue
Block a user