feat(room): роль организатора в метаданных LiveKit-токена
Предварительная работа для очереди рук и принудительного мьюта (B1/B2): build_join кладёт is_organizer:true в метаданные токена организатора (создатель мгновенной конференции и владелец при обычном входе). Метаданные токена — только подсказка для UI (см. предупреждение в докстринге build_join), любое серверное действие организатора обязано перепроверяться по conference.owner_id в БД — так и сделано в mute_participant (B2). На фронте — общий парсер метаданных участника (lib/participantMetadata.ts, переиспользован в RoomParticipantTile вместо локальной копии) и хук useIsOrganizer (читает подсказку для локального участника через useLocalParticipant — вызывается только внутри LiveKitRoom).
This commit is contained in:
@@ -53,19 +53,31 @@ def build_join(
|
||||
name: str,
|
||||
chat_enabled: bool,
|
||||
avatar_url: str | None = None,
|
||||
is_organizer: bool = False,
|
||||
) -> JoinOut:
|
||||
"""Построить ответ join: LiveKit access-токен для входа в комнату конференции.
|
||||
|
||||
Имя LiveKit-комнаты всегда равно `conference.slug` (ADR-001, п.4).
|
||||
`chat_enabled` — снятый вызывающей стороной тоггл `instance_settings`:
|
||||
читается здесь параметром, а не заново из БД, чтобы не плодить
|
||||
отдельный запрос настроек на каждый join. `avatar_url` прокидывается
|
||||
в метаданные токена как JSON
|
||||
`{"avatar_url": ...}`; `None` (гость либо пользователь без аватара) —
|
||||
метаданные не выставляются вовсе.
|
||||
отдельный запрос настроек на каждый join. `avatar_url`/`is_organizer`
|
||||
прокидываются в метаданные токена как JSON `{"avatar_url": ..., "is_organizer": true}`
|
||||
— поля добавляются, только если заданы (гость без аватара и не-организатор
|
||||
получают токен вовсе без метаданных, как и раньше).
|
||||
|
||||
⚠️ `is_organizer` в метаданных — только подсказка для UI клиента (показать/
|
||||
скрыть кнопки организатора). Метаданным токена доверять для АВТОРИЗАЦИИ
|
||||
нельзя — участник технически может их подделать на своей стороне. Любое
|
||||
серверное действие организатора (например, принудительный мьют) обязано
|
||||
заново проверяться по `conference.owner_id` в БД, а не по этому полю.
|
||||
"""
|
||||
settings = get_settings()
|
||||
metadata = json.dumps({"avatar_url": avatar_url}) if avatar_url else None
|
||||
metadata_payload: dict[str, object] = {}
|
||||
if avatar_url:
|
||||
metadata_payload["avatar_url"] = avatar_url
|
||||
if is_organizer:
|
||||
metadata_payload["is_organizer"] = True
|
||||
metadata = json.dumps(metadata_payload) if metadata_payload else None
|
||||
token = create_room_access_token(
|
||||
room_name=conference.slug, identity=identity, name=name, metadata=metadata
|
||||
)
|
||||
|
||||
@@ -154,6 +154,7 @@ class ConferenceService:
|
||||
name=owner_name,
|
||||
chat_enabled=chat_enabled,
|
||||
avatar_url=resolve_avatar_url(self._media_root, owner_avatar_path),
|
||||
is_organizer=True,
|
||||
)
|
||||
else:
|
||||
# Плановая (разовая) либо закреплённая с повторением/датой — есть
|
||||
@@ -247,6 +248,7 @@ class ConferenceService:
|
||||
name=user.name_user,
|
||||
chat_enabled=chat_enabled,
|
||||
avatar_url=resolve_avatar_url(self._media_root, user.avatar_path),
|
||||
is_organizer=conference.owner_id is not None and conference.owner_id == user.id,
|
||||
)
|
||||
|
||||
async def join_as_guest(self, conference_id: uuid.UUID, *, data: GuestJoinIn) -> JoinOut:
|
||||
|
||||
Reference in New Issue
Block a user