diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..0300dec --- /dev/null +++ b/.dockerignore @@ -0,0 +1 @@ +**/.venv diff --git a/docker-compose-test.yaml b/docker-compose-test.yaml index 27352e5..67bc5e6 100644 --- a/docker-compose-test.yaml +++ b/docker-compose-test.yaml @@ -5,18 +5,14 @@ services: restart: on-failure healthcheck: test: > - curl --fail -H "authorization: Bearer development" http://fishjam:5002/room || exit 1 + curl --fail-with-body -H "Authorization: Bearer 12345" http://fishjam:5002/admin/health interval: 3s retries: 2 timeout: 2s start_period: 30s environment: FJ_HOST: "fishjam:5002" - FJ_INTEGRATED_TURN_IP: "${INTEGRATED_TURN_IP:-127.0.0.1}" - FJ_INTEGRATED_TURN_LISTEN_IP: "0.0.0.0" - FJ_INTEGRATED_TURN_PORT_RANGE: "50000-50050" - FJ_INTEGRATED_TCP_TURN_PORT: "49999" - FJ_SERVER_API_TOKEN: "development" + FJ_ADMIN_TOKEN: "12345" FJ_PORT: 5002 FJ_SECRET_KEY_BASE: "super-secret-key" FJ_SIP_IP: "127.0.0.1" @@ -28,29 +24,24 @@ services: volumes: - ./tests/fixtures:/app/fishjam_resources/file_component_sources - test: - container_name: test - image: "cimg/python:${PYTHON_VERSION:-3.10}" - command: sh -c "cd /app && \ poetry config virtualenvs.in-project false && \ poetry install --no-ansi && \ poetry run pytest -s" - environment: - DOCKER_TEST: "TRUE" - ports: - - "5000:5000" - volumes: - - .:/app + fishjam-prep: + image: curlimages/curl:8.12.1 + command: > + curl --fail-with-body -H "Authorization: Bearer 12345" -XPOST http://fishjam:5002/admin/user --json '{"id": "testuser", "token": "development"}' depends_on: fishjam: condition: service_healthy - examples: - container_name: examples - image: "cimg/python:${PYTHON_VERSION:-3.10}" - command: sh -c "cd /app && \ poetry config virtualenvs.in-project false && \ poetry cache clear pypi --all && \ poetry install --no-ansi && \ poetry run examples" + test: + container_name: test + build: + context: . + dockerfile: tests/Dockerfile + args: + PYTHON_VERSION: ${PYTHON_VERSION:-3.10} + command: poetry run pytest environment: DOCKER_TEST: "TRUE" - CI_LIMIT: "10" - volumes: - - .:/app depends_on: - fishjam: - condition: service_healthy + fishjam-prep: + condition: service_completed_successfully diff --git a/fishjam/_openapi_client/api/broadcaster/__init__.py b/fishjam/_openapi_client/api/broadcaster/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/fishjam/_openapi_client/api/broadcaster/verify_token.py b/fishjam/_openapi_client/api/broadcaster/verify_token.py new file mode 100644 index 0000000..df27011 --- /dev/null +++ b/fishjam/_openapi_client/api/broadcaster/verify_token.py @@ -0,0 +1,155 @@ +from http import HTTPStatus +from typing import Any, Dict, Optional, Union + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.broadcaster_verify_token_response import BroadcasterVerifyTokenResponse +from ...models.error import Error +from ...types import Response + + +def _get_kwargs( + token: str, +) -> Dict[str, Any]: + return { + "method": "get", + "url": "/broadcaster/verify/{token}".format( + token=token, + ), + } + + +def _parse_response( + *, client: Union[AuthenticatedClient, Client], response: httpx.Response +) -> Optional[Union[BroadcasterVerifyTokenResponse, Error]]: + if response.status_code == HTTPStatus.CREATED: + response_201 = BroadcasterVerifyTokenResponse.from_dict(response.json()) + + return response_201 + if response.status_code == HTTPStatus.UNAUTHORIZED: + response_401 = Error.from_dict(response.json()) + + return response_401 + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: Union[AuthenticatedClient, Client], response: httpx.Response +) -> Response[Union[BroadcasterVerifyTokenResponse, Error]]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + token: str, + *, + client: Union[AuthenticatedClient, Client], +) -> Response[Union[BroadcasterVerifyTokenResponse, Error]]: + """Verify token provided by broadcaster + + Args: + token (str): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Union[BroadcasterVerifyTokenResponse, Error]] + """ + + kwargs = _get_kwargs( + token=token, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + token: str, + *, + client: Union[AuthenticatedClient, Client], +) -> Optional[Union[BroadcasterVerifyTokenResponse, Error]]: + """Verify token provided by broadcaster + + Args: + token (str): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Union[BroadcasterVerifyTokenResponse, Error] + """ + + return sync_detailed( + token=token, + client=client, + ).parsed + + +async def asyncio_detailed( + token: str, + *, + client: Union[AuthenticatedClient, Client], +) -> Response[Union[BroadcasterVerifyTokenResponse, Error]]: + """Verify token provided by broadcaster + + Args: + token (str): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Union[BroadcasterVerifyTokenResponse, Error]] + """ + + kwargs = _get_kwargs( + token=token, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + token: str, + *, + client: Union[AuthenticatedClient, Client], +) -> Optional[Union[BroadcasterVerifyTokenResponse, Error]]: + """Verify token provided by broadcaster + + Args: + token (str): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Union[BroadcasterVerifyTokenResponse, Error] + """ + + return ( + await asyncio_detailed( + token=token, + client=client, + ) + ).parsed diff --git a/fishjam/_openapi_client/api/viewer/__init__.py b/fishjam/_openapi_client/api/viewer/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/fishjam/_openapi_client/api/viewer/generate_token.py b/fishjam/_openapi_client/api/viewer/generate_token.py new file mode 100644 index 0000000..0942b15 --- /dev/null +++ b/fishjam/_openapi_client/api/viewer/generate_token.py @@ -0,0 +1,165 @@ +from http import HTTPStatus +from typing import Any, Dict, Optional, Union, cast + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.error import Error +from ...types import Response + + +def _get_kwargs( + room_id: str, +) -> Dict[str, Any]: + return { + "method": "post", + "url": "/room/{room_id}/viewer".format( + room_id=room_id, + ), + } + + +def _parse_response( + *, client: Union[AuthenticatedClient, Client], response: httpx.Response +) -> Optional[Union[Error, str]]: + if response.status_code == HTTPStatus.CREATED: + response_201 = cast(str, response.json()) + return response_201 + if response.status_code == HTTPStatus.BAD_REQUEST: + response_400 = Error.from_dict(response.json()) + + return response_400 + if response.status_code == HTTPStatus.UNAUTHORIZED: + response_401 = Error.from_dict(response.json()) + + return response_401 + if response.status_code == HTTPStatus.NOT_FOUND: + response_404 = Error.from_dict(response.json()) + + return response_404 + if response.status_code == HTTPStatus.SERVICE_UNAVAILABLE: + response_503 = Error.from_dict(response.json()) + + return response_503 + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: Union[AuthenticatedClient, Client], response: httpx.Response +) -> Response[Union[Error, str]]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + room_id: str, + *, + client: Union[AuthenticatedClient, Client], +) -> Response[Union[Error, str]]: + """Generate token for single viewer + + Args: + room_id (str): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Union[Error, str]] + """ + + kwargs = _get_kwargs( + room_id=room_id, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + room_id: str, + *, + client: Union[AuthenticatedClient, Client], +) -> Optional[Union[Error, str]]: + """Generate token for single viewer + + Args: + room_id (str): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Union[Error, str] + """ + + return sync_detailed( + room_id=room_id, + client=client, + ).parsed + + +async def asyncio_detailed( + room_id: str, + *, + client: Union[AuthenticatedClient, Client], +) -> Response[Union[Error, str]]: + """Generate token for single viewer + + Args: + room_id (str): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Union[Error, str]] + """ + + kwargs = _get_kwargs( + room_id=room_id, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + room_id: str, + *, + client: Union[AuthenticatedClient, Client], +) -> Optional[Union[Error, str]]: + """Generate token for single viewer + + Args: + room_id (str): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Union[Error, str] + """ + + return ( + await asyncio_detailed( + room_id=room_id, + client=client, + ) + ).parsed diff --git a/fishjam/_openapi_client/models/__init__.py b/fishjam/_openapi_client/models/__init__.py index 7ca1289..c5c2d92 100644 --- a/fishjam/_openapi_client/models/__init__.py +++ b/fishjam/_openapi_client/models/__init__.py @@ -2,6 +2,8 @@ from .add_component_json_body import AddComponentJsonBody from .add_peer_json_body import AddPeerJsonBody +from .broadcaster_verify_token_response import BroadcasterVerifyTokenResponse +from .broadcaster_verify_token_response_data import BroadcasterVerifyTokenResponseData from .component_details_response import ComponentDetailsResponse from .component_file import ComponentFile from .component_hls import ComponentHLS @@ -68,6 +70,8 @@ __all__ = ( "AddComponentJsonBody", "AddPeerJsonBody", + "BroadcasterVerifyTokenResponse", + "BroadcasterVerifyTokenResponseData", "ComponentDetailsResponse", "ComponentFile", "ComponentHLS", diff --git a/fishjam/_openapi_client/models/broadcaster_verify_token_response.py b/fishjam/_openapi_client/models/broadcaster_verify_token_response.py new file mode 100644 index 0000000..314055f --- /dev/null +++ b/fishjam/_openapi_client/models/broadcaster_verify_token_response.py @@ -0,0 +1,70 @@ +from typing import TYPE_CHECKING, Any, Dict, List, Type, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +if TYPE_CHECKING: + from ..models.broadcaster_verify_token_response_data import ( + BroadcasterVerifyTokenResponseData, + ) + + +T = TypeVar("T", bound="BroadcasterVerifyTokenResponse") + + +@_attrs_define +class BroadcasterVerifyTokenResponse: + """Response containing verification information""" + + data: "BroadcasterVerifyTokenResponseData" + """""" + additional_properties: Dict[str, Any] = _attrs_field(init=False, factory=dict) + """@private""" + + def to_dict(self) -> Dict[str, Any]: + """@private""" + data = self.data.to_dict() + + field_dict: Dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "data": data, + } + ) + + return field_dict + + @classmethod + def from_dict(cls: Type[T], src_dict: Dict[str, Any]) -> T: + """@private""" + from ..models.broadcaster_verify_token_response_data import ( + BroadcasterVerifyTokenResponseData, + ) + + d = src_dict.copy() + data = BroadcasterVerifyTokenResponseData.from_dict(d.pop("data")) + + broadcaster_verify_token_response = cls( + data=data, + ) + + broadcaster_verify_token_response.additional_properties = d + return broadcaster_verify_token_response + + @property + def additional_keys(self) -> List[str]: + """@private""" + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fishjam/_openapi_client/models/broadcaster_verify_token_response_data.py b/fishjam/_openapi_client/models/broadcaster_verify_token_response_data.py new file mode 100644 index 0000000..05a3c01 --- /dev/null +++ b/fishjam/_openapi_client/models/broadcaster_verify_token_response_data.py @@ -0,0 +1,70 @@ +from typing import Any, Dict, List, Type, TypeVar, Union + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +T = TypeVar("T", bound="BroadcasterVerifyTokenResponseData") + + +@_attrs_define +class BroadcasterVerifyTokenResponseData: + """ """ + + authenticated: bool + """None""" + stream_id: Union[Unset, str] = UNSET + """None""" + additional_properties: Dict[str, Any] = _attrs_field(init=False, factory=dict) + """@private""" + + def to_dict(self) -> Dict[str, Any]: + """@private""" + authenticated = self.authenticated + stream_id = self.stream_id + + field_dict: Dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "authenticated": authenticated, + } + ) + if stream_id is not UNSET: + field_dict["streamId"] = stream_id + + return field_dict + + @classmethod + def from_dict(cls: Type[T], src_dict: Dict[str, Any]) -> T: + """@private""" + d = src_dict.copy() + authenticated = d.pop("authenticated") + + stream_id = d.pop("streamId", UNSET) + + broadcaster_verify_token_response_data = cls( + authenticated=authenticated, + stream_id=stream_id, + ) + + broadcaster_verify_token_response_data.additional_properties = d + return broadcaster_verify_token_response_data + + @property + def additional_keys(self) -> List[str]: + """@private""" + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/fishjam/_openapi_client/models/room_config.py b/fishjam/_openapi_client/models/room_config.py index 2b98b41..bd34e78 100644 --- a/fishjam/_openapi_client/models/room_config.py +++ b/fishjam/_openapi_client/models/room_config.py @@ -20,8 +20,8 @@ class RoomConfig: """Duration (in seconds) after which the peer will be removed if it is disconnected. If not provided, this feature is disabled.""" peerless_purge_timeout: Union[Unset, None, int] = UNSET """Duration (in seconds) after which the room will be removed if no peers are connected. If not provided, this feature is disabled.""" - room_type: Union[Unset, None, RoomConfigRoomType] = UNSET - """None""" + room_type: Union[Unset, RoomConfigRoomType] = RoomConfigRoomType.FULL_FEATURE + """The use-case of the room. If not provided, this defaults to full_feature.""" video_codec: Union[Unset, None, RoomConfigVideoCodec] = UNSET """Enforces video codec for each peer in the room""" webhook_url: Union[Unset, None, str] = UNSET @@ -34,9 +34,9 @@ def to_dict(self) -> Dict[str, Any]: max_peers = self.max_peers peer_disconnected_timeout = self.peer_disconnected_timeout peerless_purge_timeout = self.peerless_purge_timeout - room_type: Union[Unset, None, str] = UNSET + room_type: Union[Unset, str] = UNSET if not isinstance(self.room_type, Unset): - room_type = self.room_type.value if self.room_type else None + room_type = self.room_type.value video_codec: Union[Unset, None, str] = UNSET if not isinstance(self.video_codec, Unset): @@ -73,10 +73,8 @@ def from_dict(cls: Type[T], src_dict: Dict[str, Any]) -> T: peerless_purge_timeout = d.pop("peerlessPurgeTimeout", UNSET) _room_type = d.pop("roomType", UNSET) - room_type: Union[Unset, None, RoomConfigRoomType] - if _room_type is None: - room_type = None - elif isinstance(_room_type, Unset): + room_type: Union[Unset, RoomConfigRoomType] + if isinstance(_room_type, Unset): room_type = UNSET else: room_type = RoomConfigRoomType(_room_type) diff --git a/fishjam/_openapi_client/models/room_config_room_type.py b/fishjam/_openapi_client/models/room_config_room_type.py index 6e23a4d..7f8821b 100644 --- a/fishjam/_openapi_client/models/room_config_room_type.py +++ b/fishjam/_openapi_client/models/room_config_room_type.py @@ -2,9 +2,10 @@ class RoomConfigRoomType(str, Enum): - """None""" + """The use-case of the room. If not provided, this defaults to full_feature.""" AUDIO_ONLY = "audio_only" + BROADCASTER = "broadcaster" FULL_FEATURE = "full_feature" def __str__(self) -> str: diff --git a/fishjam/api/_fishjam_client.py b/fishjam/api/_fishjam_client.py index e002fa1..3952738 100644 --- a/fishjam/api/_fishjam_client.py +++ b/fishjam/api/_fishjam_client.py @@ -19,6 +19,7 @@ PeerOptionsWebRTC, PeerRefreshTokenResponse, RoomConfig, + RoomConfigRoomType, RoomCreateDetailsResponse, RoomDetailsResponse, RoomsListingResponse, @@ -62,6 +63,8 @@ class RoomOptions: """Enforces video codec for each peer in the room""" webhook_url: str | None = None """URL where Fishjam notifications will be sent""" + room_type: Literal["full_feature", "audio_only", "broadcaster"] = "full_feature" + """The use-case of the room. If not provided, this defaults to full_feature.""" @dataclass @@ -127,6 +130,7 @@ def create_room(self, options: RoomOptions | None = None) -> Room: peerless_purge_timeout=options.peerless_purge_timeout, video_codec=codec, webhook_url=options.webhook_url, + room_type=RoomConfigRoomType(options.room_type), ) room = cast( diff --git a/tests/Dockerfile b/tests/Dockerfile new file mode 100644 index 0000000..7beecff --- /dev/null +++ b/tests/Dockerfile @@ -0,0 +1,20 @@ +ARG PYTHON_VERSION=3.10 + +FROM python:$PYTHON_VERSION-slim as builder + +ARG POETRY_VERSION=2.1 + +ENV POETRY_HOME=/opt/poetry +ENV POETRY_VIRTUALENVS_IN_PROJECT=1 +ENV POETRY_VIRTUALENVS_CREATE=1 +ENV PYTHONDONTWRITEBYTECODE=1 +ENV PYTHONUNBUFFERED=1 +ENV POETRY_CACHE_DIR=/opt/.cache + +RUN pip install "poetry==${POETRY_VERSION}" + +WORKDIR /app + +COPY pyproject.toml poetry.lock /app/ +RUN poetry install --with=dev --no-root && rm -rf $POETRY_CACHE_DIR +COPY . /app/ diff --git a/tests/test_room_api.py b/tests/test_room_api.py index 0791a3d..14f0429 100644 --- a/tests/test_room_api.py +++ b/tests/test_room_api.py @@ -13,6 +13,7 @@ from fishjam._openapi_client.models import ( PeerStatus, RoomConfig, + RoomConfigRoomType, RoomConfigVideoCodec, ) from fishjam.api._fishjam_client import Peer, Room @@ -29,6 +30,7 @@ MAX_PEERS = 10 CODEC_H264 = "h264" +AUDIO_ONLY = "audio_only" class TestAuthentication: @@ -76,7 +78,11 @@ def test_no_params(self, room_api): assert room in room_api.get_all_rooms() def test_valid_params(self, room_api): - options = RoomOptions(max_peers=MAX_PEERS, video_codec=CODEC_H264) + options = RoomOptions( + max_peers=MAX_PEERS, + video_codec=CODEC_H264, + room_type=AUDIO_ONLY, + ) room = room_api.create_room(options) config = RoomConfig( @@ -85,6 +91,7 @@ def test_valid_params(self, room_api): webhook_url=None, peerless_purge_timeout=None, peer_disconnected_timeout=None, + room_type=RoomConfigRoomType(AUDIO_ONLY), ) config.__setitem__("roomId", room.config.__getitem__("roomId"))