From b3a8e9739da1aa423646b682d80265ffffd6dd2a Mon Sep 17 00:00:00 2001 From: Igor Monadical Date: Thu, 11 Sep 2025 17:52:34 -0400 Subject: [PATCH] chore: whereby & s3 settings env error reporting (#637) Co-authored-by: Igor Loskutov --- server/reflector/settings.py | 4 +- server/reflector/utils/string.py | 7 +++- server/reflector/whereby.py | 65 +++++++++++++++++++++++++++----- 3 files changed, 63 insertions(+), 13 deletions(-) diff --git a/server/reflector/settings.py b/server/reflector/settings.py index 686f67c1..9659f648 100644 --- a/server/reflector/settings.py +++ b/server/reflector/settings.py @@ -1,6 +1,8 @@ from pydantic.types import PositiveInt from pydantic_settings import BaseSettings, SettingsConfigDict +from reflector.utils.string import NonEmptyString + class Settings(BaseSettings): model_config = SettingsConfigDict( @@ -120,7 +122,7 @@ class Settings(BaseSettings): # Whereby integration WHEREBY_API_URL: str = "https://api.whereby.dev/v1" - WHEREBY_API_KEY: str | None = None + WHEREBY_API_KEY: NonEmptyString | None = None WHEREBY_WEBHOOK_SECRET: str | None = None AWS_WHEREBY_ACCESS_KEY_ID: str | None = None AWS_WHEREBY_ACCESS_KEY_SECRET: str | None = None diff --git a/server/reflector/utils/string.py b/server/reflector/utils/string.py index 08a9de78..05f40e30 100644 --- a/server/reflector/utils/string.py +++ b/server/reflector/utils/string.py @@ -10,8 +10,11 @@ NonEmptyString = Annotated[ non_empty_string_adapter = TypeAdapter(NonEmptyString) -def parse_non_empty_string(s: str) -> NonEmptyString: - return non_empty_string_adapter.validate_python(s) +def parse_non_empty_string(s: str, error: str | None = None) -> NonEmptyString: + try: + return non_empty_string_adapter.validate_python(s) + except Exception as e: + raise ValueError(f"{e}: {error}" if error else e) from e def try_parse_non_empty_string(s: str) -> NonEmptyString | None: diff --git a/server/reflector/whereby.py b/server/reflector/whereby.py index deaa5274..8b5c18fd 100644 --- a/server/reflector/whereby.py +++ b/server/reflector/whereby.py @@ -1,18 +1,60 @@ +import logging from datetime import datetime import httpx from reflector.db.rooms import Room from reflector.settings import settings +from reflector.utils.string import parse_non_empty_string + +logger = logging.getLogger(__name__) + + +def _get_headers(): + api_key = parse_non_empty_string( + settings.WHEREBY_API_KEY, "WHEREBY_API_KEY value is required." + ) + return { + "Content-Type": "application/json; charset=utf-8", + "Authorization": f"Bearer {api_key}", + } + -HEADERS = { - "Content-Type": "application/json; charset=utf-8", - "Authorization": f"Bearer {settings.WHEREBY_API_KEY}", -} TIMEOUT = 10 # seconds +def _get_whereby_s3_auth(): + errors = [] + try: + bucket_name = parse_non_empty_string( + settings.RECORDING_STORAGE_AWS_BUCKET_NAME, + "RECORDING_STORAGE_AWS_BUCKET_NAME value is required.", + ) + except Exception as e: + errors.append(e) + try: + key_id = parse_non_empty_string( + settings.AWS_WHEREBY_ACCESS_KEY_ID, + "AWS_WHEREBY_ACCESS_KEY_ID value is required.", + ) + except Exception as e: + errors.append(e) + try: + key_secret = parse_non_empty_string( + settings.AWS_WHEREBY_ACCESS_KEY_SECRET, + "AWS_WHEREBY_ACCESS_KEY_SECRET value is required.", + ) + except Exception as e: + errors.append(e) + if len(errors) > 0: + raise Exception( + f"Failed to get Whereby auth settings: {', '.join(str(e) for e in errors)}" + ) + return bucket_name, key_id, key_secret + + async def create_meeting(room_name_prefix: str, end_date: datetime, room: Room): + s3_bucket_name, s3_key_id, s3_key_secret = _get_whereby_s3_auth() data = { "isLocked": room.is_locked, "roomNamePrefix": room_name_prefix, @@ -23,23 +65,26 @@ async def create_meeting(room_name_prefix: str, end_date: datetime, room: Room): "type": room.recording_type, "destination": { "provider": "s3", - "bucket": settings.RECORDING_STORAGE_AWS_BUCKET_NAME, - "accessKeyId": settings.AWS_WHEREBY_ACCESS_KEY_ID, - "accessKeySecret": settings.AWS_WHEREBY_ACCESS_KEY_SECRET, + "bucket": s3_bucket_name, + "accessKeyId": s3_key_id, + "accessKeySecret": s3_key_secret, "fileFormat": "mp4", }, "startTrigger": room.recording_trigger, }, "fields": ["hostRoomUrl"], } - async with httpx.AsyncClient() as client: response = await client.post( f"{settings.WHEREBY_API_URL}/meetings", - headers=HEADERS, + headers=_get_headers(), json=data, timeout=TIMEOUT, ) + if response.status_code == 403: + logger.warning( + f"Failed to create meeting: access denied on Whereby: {response.text}" + ) response.raise_for_status() return response.json() @@ -48,7 +93,7 @@ async def get_room_sessions(room_name: str): async with httpx.AsyncClient() as client: response = await client.get( f"{settings.WHEREBY_API_URL}/insights/room-sessions?roomName={room_name}", - headers=HEADERS, + headers=_get_headers(), timeout=TIMEOUT, ) response.raise_for_status()