diff --git a/pybotx/__init__.py b/pybotx/__init__.py index 20eee9b0..fa50a5c4 100644 --- a/pybotx/__init__.py +++ b/pybotx/__init__.py @@ -102,6 +102,7 @@ AttachmentTypes, ChatLinkTypes, ChatTypes, + ClientNetworkContours, ClientPlatforms, ConferenceLinkTypes, MentionTypes, @@ -221,6 +222,7 @@ "ChatNotFoundError", "ChatLinkTypes", "ChatTypes", + "ClientNetworkContours", "ClientPlatforms", "ConferenceChangedEvent", "ConferenceCreatedEvent", diff --git a/pybotx/client/chats_api/create_chat.py b/pybotx/client/chats_api/create_chat.py index 876b89ec..640c44d5 100644 --- a/pybotx/client/chats_api/create_chat.py +++ b/pybotx/client/chats_api/create_chat.py @@ -39,6 +39,8 @@ class BotXAPICreateChatRequestPayload(UnverifiedPayloadBaseModel): @model_validator(mode="before") def _convert_chat_type(cls, values: dict[str, Any]) -> dict[str, Any]: chat_type = values.get("chat_type") + if isinstance(chat_type, APIChatTypes) and chat_type == APIChatTypes.VOEX_CALL: + raise ValueError("Bot cannot create a chat of type 'voex_call'") if isinstance(chat_type, ChatTypes): values["chat_type"] = convert_chat_type_from_domain(chat_type) return values diff --git a/pybotx/models/enums.py b/pybotx/models/enums.py index b0f3de99..b458b56d 100644 --- a/pybotx/models/enums.py +++ b/pybotx/models/enums.py @@ -39,6 +39,11 @@ class ClientPlatforms(AutoName): AURORA = auto() +class ClientNetworkContours(AutoName): + INTERNAL = auto() + EXTERNAL = auto() + + class MentionTypes(AutoName): CONTACT = auto() CHAT = auto() @@ -103,6 +108,7 @@ class APIChatTypes(Enum): GROUP_CHAT = "group_chat" CHANNEL = "channel" THREAD = "thread" + VOEX_CALL = "voex_call" class BotAPICommandTypes(StrEnum): @@ -136,6 +142,11 @@ class BotAPIClientPlatforms(Enum): AURORA = "aurora" +class BotAPIClientNetworkContours(StrEnum): + INTERNAL = "internal" + EXTERNAL = "external" + + class BotAPIEntityTypes(StrEnum): MENTION = "mention" FORWARD = "forward" @@ -208,6 +219,23 @@ def convert_client_platform_to_domain( return converted_type +def convert_client_network_contour_to_domain( + client_network_contour: BotAPIClientNetworkContours, +) -> ClientNetworkContours: + client_network_contours_mapping = { + BotAPIClientNetworkContours.INTERNAL: ClientNetworkContours.INTERNAL, + BotAPIClientNetworkContours.EXTERNAL: ClientNetworkContours.EXTERNAL, + } + + converted_type = client_network_contours_mapping.get(client_network_contour) + if converted_type is None: + raise NotImplementedError( + f"Unsupported client network contour: {client_network_contour}", + ) + + return converted_type + + def convert_mention_type_from_domain( mention_type: MentionTypes, ) -> BotAPIMentionTypes: @@ -338,6 +366,7 @@ def convert_chat_type_to_domain( APIChatTypes.GROUP_CHAT: ChatTypes.GROUP_CHAT, APIChatTypes.CHANNEL: ChatTypes.CHANNEL, APIChatTypes.THREAD: ChatTypes.THREAD, + APIChatTypes.VOEX_CALL: ChatTypes.GROUP_CHAT, } converted_type: IncomingChatTypes | None diff --git a/pybotx/models/message/incoming_message.py b/pybotx/models/message/incoming_message.py index 3e5d10f4..ad6251a6 100644 --- a/pybotx/models/message/incoming_message.py +++ b/pybotx/models/message/incoming_message.py @@ -26,6 +26,7 @@ from pybotx.models.enums import ( BotAPIEntityTypes, BotAPIMentionTypes, + ClientNetworkContours, ClientPlatforms, convert_chat_type_to_domain, convert_client_platform_to_domain, @@ -68,6 +69,7 @@ class UserSender: is_chat_admin: bool | None is_chat_creator: bool | None device: UserDevice + client_network_contour: ClientNetworkContours | None = None @property def upn(self) -> str | None: diff --git a/pybotx/models/sync_smartapp_event.py b/pybotx/models/sync_smartapp_event.py index b4d519c6..cdc11fee 100644 --- a/pybotx/models/sync_smartapp_event.py +++ b/pybotx/models/sync_smartapp_event.py @@ -14,7 +14,9 @@ from pybotx.models.chats import Chat from pybotx.models.enums import ( BotAPIClientPlatforms, + BotAPIClientNetworkContours, ChatTypes, + convert_client_network_contour_to_domain, convert_client_platform_to_domain, ) from pybotx.models.message.incoming_message import UserDevice, UserSender @@ -25,6 +27,7 @@ class BotAPISyncSmartAppSender(VerifiedPayloadBaseModel): user_huid: UUID udid: UUID | None platform: BotAPIClientPlatforms | None + client_network_contour: BotAPIClientNetworkContours | None = None class BotAPISyncSmartAppPayload(VerifiedPayloadBaseModel): @@ -59,6 +62,14 @@ def to_domain(self, raw_smartapp_event: dict[str, Any]) -> SmartAppEvent: locale=None, ) + client_network_contour = ( + convert_client_network_contour_to_domain( + self.sender_info.client_network_contour, + ) + if self.sender_info.client_network_contour + else None + ) + sender = UserSender( huid=self.sender_info.user_huid, udid=self.sender_info.udid, @@ -68,6 +79,7 @@ def to_domain(self, raw_smartapp_event: dict[str, Any]) -> SmartAppEvent: username=None, is_chat_admin=None, is_chat_creator=None, + client_network_contour=client_network_contour, ) return SmartAppEvent( @@ -143,6 +155,5 @@ def jsonable_dict(self) -> dict[str, Any]: BotAPISyncSmartAppEventResponse = ( - BotAPISyncSmartAppEventResultResponse - | BotAPISyncSmartAppEventErrorResponse + BotAPISyncSmartAppEventResultResponse | BotAPISyncSmartAppEventErrorResponse ) diff --git a/pyproject.toml b/pyproject.toml index e51a668d..4fcae241 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "pybotx" -version = "0.76.2" +version = "0.76.3" description = "A python library for interacting with eXpress BotX API" authors = [ "Sidnev Nikolay ", diff --git a/tests/client/chats_api/test_chat_info.py b/tests/client/chats_api/test_chat_info.py index c1a54a46..83d8f0c9 100644 --- a/tests/client/chats_api/test_chat_info.py +++ b/tests/client/chats_api/test_chat_info.py @@ -149,6 +149,80 @@ async def test__chat_info__succeed( assert endpoint.called +async def test__chat_info__succeed_voex_call( + respx_mock: MockRouter, + host: str, + bot_id: UUID, + datetime_formatter: Callable[[str], dt], + bot_factory: Any, +) -> None: + # - Arrange - + endpoint = mock_botx( + respx_mock, + host, + REQUEST, + ok_payload( + { + "chat_type": "voex_call", + "creator": "6fafda2c-6505-57a5-a088-25ea5d1d0364", + "description": None, + "group_chat_id": "054af49e-5e18-4dca-ad73-4f96b6de63fa", + "inserted_at": "2019-08-29T11:22:48.358586Z", + "members": [ + { + "admin": True, + "user_huid": "6fafda2c-6505-57a5-a088-25ea5d1d0364", + "user_kind": "user", + }, + { + "admin": False, + "user_huid": "705df263-6bfd-536a-9d51-13524afaab5c", + "user_kind": "botx", + }, + ], + "name": "Voex Chat Example", + "shared_history": False, + }, + ), + HTTPStatus.OK, + ) + + # - Act - + async with bot_factory() as bot: + chat_info = await bot.chat_info( + bot_id=bot_id, + chat_id=UUID("054af49e-5e18-4dca-ad73-4f96b6de63fa"), + ) + + # - Assert - + assert_deep_equal( + chat_info, + ChatInfo( + chat_type=ChatTypes.GROUP_CHAT, + creator_id=UUID("6fafda2c-6505-57a5-a088-25ea5d1d0364"), + description=None, + chat_id=UUID("054af49e-5e18-4dca-ad73-4f96b6de63fa"), + created_at=datetime_formatter("2019-08-29T11:22:48.358586Z"), + members=[ + ChatInfoMember( + is_admin=True, + huid=UUID("6fafda2c-6505-57a5-a088-25ea5d1d0364"), + kind=UserKinds.RTS_USER, + ), + ChatInfoMember( + is_admin=False, + huid=UUID("705df263-6bfd-536a-9d51-13524afaab5c"), + kind=UserKinds.BOT, + ), + ], + name="Voex Chat Example", + shared_history=False, + ), + ) + + assert endpoint.called + + async def test__chat_info__notes_chat_type_mapped_to_personal_chat( respx_mock: MockRouter, host: str, diff --git a/tests/client/chats_api/test_create_chat.py b/tests/client/chats_api/test_create_chat.py index 7c73fa01..16f00df1 100644 --- a/tests/client/chats_api/test_create_chat.py +++ b/tests/client/chats_api/test_create_chat.py @@ -220,7 +220,12 @@ def test__create_chat_payload__convert_chat_type_validator() -> None: result = BotXAPICreateChatRequestPayload._convert_chat_type(values) # type: ignore[operator] assert result["chat_type"] == APIChatTypes.GROUP_CHAT - # Test with APIChatTypes value (should remain unchanged) + # Test with APIChatTypes.VOEX_CALL + values = {"chat_type": APIChatTypes.VOEX_CALL} # type: ignore[dict-item] + with pytest.raises(ValueError, match="Bot cannot create a chat of type 'voex_call'"): + BotXAPICreateChatRequestPayload._convert_chat_type(values) # type: ignore[operator] + + # Test with another APIChatTypes value (should remain unchanged) values = {"chat_type": APIChatTypes.CHAT} # type: ignore[dict-item] result = BotXAPICreateChatRequestPayload._convert_chat_type(values) # type: ignore[operator] assert result["chat_type"] == APIChatTypes.CHAT diff --git a/tests/models/test_enums.py b/tests/models/test_enums.py index 3dc6d855..7cadc43b 100644 --- a/tests/models/test_enums.py +++ b/tests/models/test_enums.py @@ -3,9 +3,12 @@ from pybotx.models.enums import ( APIChatTypes, + BotAPIClientNetworkContours, + ClientNetworkContours, ChatTypes, convert_chat_type_from_domain, convert_chat_type_to_domain, + convert_client_network_contour_to_domain, ) @@ -31,4 +34,25 @@ def test__convert_chat_type_from_domain__unsupported_chat_type_raises_error() -> def test__convert_chat_type_to_domain__notes_maps_to_personal_chat() -> None: assert convert_chat_type_to_domain(APIChatTypes.NOTES) == ChatTypes.PERSONAL_CHAT + assert convert_chat_type_to_domain(APIChatTypes.VOEX_CALL) == ChatTypes.GROUP_CHAT assert convert_chat_type_to_domain("notes") == ChatTypes.PERSONAL_CHAT + + +def test__convert_client_network_contour_to_domain__successful_conversion() -> None: + assert ( + convert_client_network_contour_to_domain(BotAPIClientNetworkContours.INTERNAL) + == ClientNetworkContours.INTERNAL + ) + assert ( + convert_client_network_contour_to_domain(BotAPIClientNetworkContours.EXTERNAL) + == ClientNetworkContours.EXTERNAL + ) + + +def test__convert_client_network_contour_to_domain__unsupported_contour_raises_error() -> ( + None +): + unsupported_client_network_contour = Mock(spec=BotAPIClientNetworkContours) + + with pytest.raises(NotImplementedError, match="Unsupported client network contour"): + convert_client_network_contour_to_domain(unsupported_client_network_contour) diff --git a/tests/models/test_sync_smartapp_event.py b/tests/models/test_sync_smartapp_event.py new file mode 100644 index 00000000..4b179fee --- /dev/null +++ b/tests/models/test_sync_smartapp_event.py @@ -0,0 +1,49 @@ +from typing import Any + +import pytest + +from pybotx.models.enums import ClientNetworkContours +from pybotx.models.sync_smartapp_event import BotAPISyncSmartAppEvent + + +@pytest.mark.parametrize( + ("api_value", "domain_value"), + [ + ("internal", ClientNetworkContours.INTERNAL), + ("external", ClientNetworkContours.EXTERNAL), + ], +) +def test__sync_smartapp_event__client_network_contour_mapped_to_sender( + api_value: str, + domain_value: ClientNetworkContours, +) -> None: + payload = _sync_smartapp_event_payload(client_network_contour=api_value) + + event = BotAPISyncSmartAppEvent.model_validate(payload).to_domain(payload) + + assert event.sender.client_network_contour == domain_value + + +def _sync_smartapp_event_payload( + *, + client_network_contour: str, +) -> dict[str, Any]: + return { + "bot_id": "2a98219d-1f57-5dcb-920c-9a992bde01ec", + "group_chat_id": "1ee7fdcf-e258-03d6-2263-2764da127088", + "method": "menu", + "payload": { + "data": { + "camelCaseValue": "value2", + "under_score_value": "value1", + }, + "files": [], + "opts": {}, + }, + "sender_info": { + "client_network_contour": client_network_contour, + "platform": "web", + "udid": "9eb0ed48-2501-59b8-9ba1-9136ff6efc59", + "user_huid": "347fdc52-fd0f-5e1d-b06f-bdfdf1cc7164", + }, + }