"""Проверка права входа в конференцию (статус/пароль) и генерация ответа join. Заменяет `services/room_access.py`: комнаты больше не конкурируют за время («правило часа» удалено вместе с бронированием, ADR-001, п.5), но пароль закрытой конференции по-прежнему проверяется здесь же — для обычного пользователя и гостя одинаково. """ import json from core.config import get_settings from core.security import verify_password from models.conference import Conference from schemas.conferences import JoinOut from services.livekit_tokens import create_room_access_token class ConferenceEndedError(Exception): """Конференция завершена (терминальный статус) — повторный вход невозможен.""" class PasswordRequiredError(Exception): """Конференция закрыта паролем; пароль не передан.""" class InvalidPasswordError(Exception): """Указанный пароль не совпадает с паролем закрытой конференции.""" def ensure_joinable(conference: Conference, *, password: str | None) -> None: """Проверить, что в конференцию можно войти прямо сейчас. Бросает `ConferenceEndedError` для терминального статуса `ended` (история и саммари остаются, но повторный вход невозможен — ADR-001, п.2), либо `PasswordRequiredError`/`InvalidPasswordError` для закрытой паролем конференции. Ничего не бросает для открытой конференции в статусе `scheduled`/`active`. """ if conference.status == "ended": raise ConferenceEndedError if not conference.is_closed: return if conference.password_hash is None or password is None: raise PasswordRequiredError if not verify_password(password, conference.password_hash): raise InvalidPasswordError def build_join( conference: Conference, *, identity: str, name: str, chat_enabled: bool, avatar_url: str | None = None, ) -> JoinOut: """Построить ответ join: LiveKit access-токен для входа в комнату конференции. Имя LiveKit-комнаты всегда равно `conference.slug` (ADR-001, п.4). `chat_enabled` — снятый вызывающей стороной тоггл `instance_settings`: читается здесь параметром, а не заново из БД, чтобы не плодить отдельный запрос настроек на каждый join. `avatar_url` прокидывается в метаданные токена как JSON `{"avatar_url": ...}`; `None` (гость либо пользователь без аватара) — метаданные не выставляются вовсе. """ settings = get_settings() metadata = json.dumps({"avatar_url": avatar_url}) if avatar_url else None token = create_room_access_token( room_name=conference.slug, identity=identity, name=name, metadata=metadata ) return JoinOut( livekit_url=settings.livekit_public_url, token=token, room_name=conference.slug, conference_id=conference.id, chat_enabled=chat_enabled, )