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:
@@ -18,6 +18,8 @@ from schemas.conferences import (
|
||||
GuestJoinIn,
|
||||
JoinIn,
|
||||
JoinOut,
|
||||
MuteParticipantIn,
|
||||
MuteParticipantOut,
|
||||
OccurrenceOut,
|
||||
ResolveOut,
|
||||
)
|
||||
@@ -34,6 +36,7 @@ from services.conferences import (
|
||||
InviteeUserNotFoundError,
|
||||
NotConferenceOwnerError,
|
||||
)
|
||||
from services.room_control import ParticipantNotInRoomError
|
||||
|
||||
router = APIRouter(prefix="/api/v1/conferences", tags=["conferences"])
|
||||
|
||||
@@ -171,6 +174,37 @@ async def guest_join_conference(
|
||||
) from exc
|
||||
|
||||
|
||||
@router.post("/{conference_id}/mute-participant", response_model=MuteParticipantOut)
|
||||
async def mute_participant(
|
||||
conference_id: uuid.UUID,
|
||||
data: MuteParticipantIn,
|
||||
user: Annotated[User, Depends(get_current_user)],
|
||||
session: Annotated[AsyncSession, Depends(get_session)],
|
||||
) -> MuteParticipantOut:
|
||||
"""Принудительно выключить микрофон/камеру участника (задача B2) — владелец/администратор.
|
||||
|
||||
Права проверяются ЗАНОВО по владельцу конференции в БД
|
||||
(`ConferenceService.mute_participant`), а не по метаданным LiveKit-токена
|
||||
вызывающего — те лишь подсказка для UI и потенциально подделываемы клиентом.
|
||||
"""
|
||||
service = ConferenceService(session)
|
||||
try:
|
||||
muted = await service.mute_participant(
|
||||
conference_id, actor=user, target_identity=data.identity, source=data.source
|
||||
)
|
||||
except ConferenceNotFoundError as exc:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="conference_not_found"
|
||||
) from exc
|
||||
except NotConferenceOwnerError as exc:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="not_owner") from exc
|
||||
except ParticipantNotInRoomError as exc:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="participant_not_in_room"
|
||||
) from exc
|
||||
return MuteParticipantOut(muted=muted)
|
||||
|
||||
|
||||
@router.get("/{conference_id}", response_model=ConferenceOut)
|
||||
async def get_conference(
|
||||
conference_id: uuid.UUID,
|
||||
|
||||
Reference in New Issue
Block a user