diff --git a/pybotx/__init__.py b/pybotx/__init__.py index 456feb01..6dc75987 100644 --- a/pybotx/__init__.py +++ b/pybotx/__init__.py @@ -144,6 +144,7 @@ from pybotx.models.system_events.cts_login import CTSLoginEvent from pybotx.models.system_events.cts_logout import CTSLogoutEvent from pybotx.models.system_events.deleted_from_chat import DeletedFromChatEvent +from pybotx.models.system_events.event_delete import EventDeleted from pybotx.models.system_events.event_edit import EventEdit from pybotx.models.system_events.internal_bot_notification import ( InternalBotNotificationEvent, @@ -206,6 +207,7 @@ "DeletedFromChatEvent", "Document", "EditMessage", + "EventDeleted", "EventEdit", "EventNotFoundError", "File", diff --git a/pybotx/bot/bot.py b/pybotx/bot/bot.py index 887a0ee7..bedb7490 100644 --- a/pybotx/bot/bot.py +++ b/pybotx/bot/bot.py @@ -3,15 +3,7 @@ from contextlib import asynccontextmanager from datetime import datetime from types import SimpleNamespace -from typing import ( - Any, - Dict, - List, - Optional, - Set, - Tuple, - Union, -) +from typing import Any, Dict, List, Optional, Set, Tuple, Union from uuid import UUID import aiofiles diff --git a/pybotx/bot/handler.py b/pybotx/bot/handler.py index a4933e31..a26efab2 100644 --- a/pybotx/bot/handler.py +++ b/pybotx/bot/handler.py @@ -1,14 +1,6 @@ from dataclasses import dataclass from functools import partial -from typing import ( - TYPE_CHECKING, - Awaitable, - Callable, - List, - Literal, - TypeVar, - Union, -) +from typing import TYPE_CHECKING, Awaitable, Callable, List, Literal, TypeVar, Union from pybotx.models.commands import BotCommand from pybotx.models.message.incoming_message import IncomingMessage @@ -23,6 +15,7 @@ from pybotx.models.system_events.cts_login import CTSLoginEvent from pybotx.models.system_events.cts_logout import CTSLogoutEvent from pybotx.models.system_events.deleted_from_chat import DeletedFromChatEvent +from pybotx.models.system_events.event_delete import EventDeleted from pybotx.models.system_events.event_edit import EventEdit from pybotx.models.system_events.internal_bot_notification import ( InternalBotNotificationEvent, @@ -53,6 +46,7 @@ HandlerFunc[CTSLogoutEvent], HandlerFunc[InternalBotNotificationEvent], HandlerFunc[SmartAppEvent], + HandlerFunc[EventDeleted], HandlerFunc[EventEdit], HandlerFunc[JoinToChatEvent], HandlerFunc[ConferenceChangedEvent], diff --git a/pybotx/bot/handler_collector.py b/pybotx/bot/handler_collector.py index 3d07d146..8d7abd99 100644 --- a/pybotx/bot/handler_collector.py +++ b/pybotx/bot/handler_collector.py @@ -48,6 +48,7 @@ from pybotx.models.system_events.cts_login import CTSLoginEvent from pybotx.models.system_events.cts_logout import CTSLogoutEvent from pybotx.models.system_events.deleted_from_chat import DeletedFromChatEvent +from pybotx.models.system_events.event_delete import EventDeleted from pybotx.models.system_events.event_edit import EventEdit from pybotx.models.system_events.internal_bot_notification import ( InternalBotNotificationEvent, @@ -316,6 +317,14 @@ def event_edit( self._system_event(EventEdit, handler_func) return handler_func + def event_deleted( + self, + handler_func: HandlerFunc[EventDeleted], + ) -> HandlerFunc[EventDeleted]: + """Decorate `event deleted` event handler.""" + self._system_event(EventDeleted, handler_func) + return handler_func + def conference_changed( self, handler_func: HandlerFunc[ConferenceChangedEvent], diff --git a/pybotx/models/commands.py b/pybotx/models/commands.py index 5f4e21f3..f19f9038 100644 --- a/pybotx/models/commands.py +++ b/pybotx/models/commands.py @@ -31,6 +31,7 @@ BotAPIDeletedFromChat, DeletedFromChatEvent, ) +from pybotx.models.system_events.event_delete import BotAPIEventDeleted, EventDeleted from pybotx.models.system_events.event_edit import BotAPIEventEdit, EventEdit from pybotx.models.system_events.internal_bot_notification import ( BotAPIInternalBotNotification, @@ -60,6 +61,7 @@ BotAPILeftFromChat, BotAPICTSLogin, BotAPICTSLogout, + BotAPIEventDeleted, BotAPIEventEdit, BotAPIJoinToChat, BotAPIConferenceChanged, @@ -79,6 +81,7 @@ LeftFromChatEvent, CTSLoginEvent, CTSLogoutEvent, + EventDeleted, EventEdit, JoinToChatEvent, ConferenceChangedEvent, diff --git a/pybotx/models/enums.py b/pybotx/models/enums.py index d56f61d7..fac1f0ea 100644 --- a/pybotx/models/enums.py +++ b/pybotx/models/enums.py @@ -111,6 +111,7 @@ class BotAPISystemEventTypes(StrEnum): INTERNAL_BOT_NOTIFICATION = "system:internal_bot_notification" LEFT_FROM_CHAT = "system:left_from_chat" SMARTAPP_EVENT = "system:smartapp_event" + EVENT_DELETED = "system:event_deleted" EVENT_EDIT = "system:event_edit" JOIN_TO_CHAT = "system:user_joined_to_chat" CONFERENCE_CHANGED = "system:conference_changed" diff --git a/pybotx/models/system_events/event_delete.py b/pybotx/models/system_events/event_delete.py new file mode 100644 index 00000000..cd0311d3 --- /dev/null +++ b/pybotx/models/system_events/event_delete.py @@ -0,0 +1,63 @@ +from dataclasses import dataclass +from datetime import datetime +from typing import Any, Dict, List, Literal, Optional +from uuid import UUID + +from pydantic import Field + +from pybotx.models.api_base import VerifiedPayloadBaseModel +from pybotx.models.base_command import ( + BaseBotAPIContext, + BotAPIBaseCommand, + BotAPIBaseSystemEventPayload, + BotCommandBase, +) +from pybotx.models.bot_account import BotAccount +from pybotx.models.enums import BotAPISystemEventTypes + + +@dataclass +class EventDeleted(BotCommandBase): + """Event `system:event_deleted`. + + Attributes: + deleted_at: Delete message date and time. + group_chat_id: Delete message group chat id. + meta: Delete message meta. + sync_ids: Delete message sync ids. + """ + + deleted_at: datetime + group_chat_id: UUID + sync_ids: List[UUID] + meta: Optional[Dict[str, Any]] + + +class BotAPIEventDeletedData(VerifiedPayloadBaseModel): + deleted_at: datetime + group_chat_id: UUID + sync_ids: List[UUID] + meta: Optional[Dict[str, Any]] + + +class BotAPIEventDeletedPayload(BotAPIBaseSystemEventPayload): + body: Literal[BotAPISystemEventTypes.EVENT_DELETED] + data: BotAPIEventDeletedData + + +class BotAPIEventDeleted(BotAPIBaseCommand): + payload: BotAPIEventDeletedPayload = Field(..., alias="command") + bot: BaseBotAPIContext = Field(..., alias="from") + + def to_domain(self, raw_command: Dict[str, Any]) -> EventDeleted: + return EventDeleted( + bot=BotAccount( + id=self.bot_id, + host=self.bot.host, + ), + raw_command=raw_command, + deleted_at=self.payload.data.deleted_at, + group_chat_id=self.payload.data.group_chat_id, + meta=self.payload.data.meta, + sync_ids=self.payload.data.sync_ids, + ) diff --git a/pyproject.toml b/pyproject.toml index bfdfe41e..a3e0b04a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "pybotx" -version = "0.75.2" +version = "0.75.3" description = "A python library for interacting with eXpress BotX API" authors = [ "Sidnev Nikolay ", diff --git a/tests/system_events/factories.py b/tests/system_events/factories.py index 264874da..c508b407 100644 --- a/tests/system_events/factories.py +++ b/tests/system_events/factories.py @@ -77,3 +77,13 @@ class ConferenceChangedDataFactory(DictFactory): operation = "change_conference_info" sip_number = 12345678 start_at = "2025-04-15T11:00:39.634000Z" + + +class DeleteEventFactory(DictFactory): + deleted_at = "2025-09-23T09:04:49.787078Z" + group_chat_id = "30dc1980-643a-00ad-37fc-7cc10d74e935" + meta = { + "deleted_by": "fbc84c63-e432-4ff1-99bd-c3275f053866", + "deleted_by_admin": True, + } + sync_ids = ["36c4f12a-7082-599a-9842-cd146730e179"] diff --git a/tests/system_events/test_event_delete.py b/tests/system_events/test_event_delete.py new file mode 100644 index 00000000..6405f5da --- /dev/null +++ b/tests/system_events/test_event_delete.py @@ -0,0 +1,72 @@ +from datetime import datetime +from typing import Any, Callable, Dict, Optional +from uuid import UUID + +import pytest +from deepdiff import DeepDiff + +from pybotx import ( + Bot, + BotAccount, + BotAccountWithSecret, + EventDeleted, + HandlerCollector, + lifespan_wrapper, +) +from tests.system_events.factories import DeleteEventFactory + +pytestmark = [ + pytest.mark.asyncio, + pytest.mark.mock_authorization, + pytest.mark.usefixtures("respx_mock"), +] + + +async def test__event_delete__succeed( + bot_account: BotAccountWithSecret, + bot_id: UUID, + host: str, + datetime_formatter: Callable[[str], datetime], + api_incoming_message_factory: Callable[..., Dict[str, Any]], +) -> None: + # - Arrange - + event_deleted_data = DeleteEventFactory.create() + + payload = api_incoming_message_factory( + body="system:event_deleted", + command_type="system", + data=event_deleted_data, + bot_id=bot_id, + host=host, + ) + + collector = HandlerCollector() + event_deleted: Optional[EventDeleted] = None + + @collector.event_deleted + async def event_deleted_handler(event: EventDeleted, _: Bot) -> None: + nonlocal event_deleted + event_deleted = event + # Drop `raw_command` from asserting + event_deleted.raw_command = None + + built_bot = Bot(collectors=[collector], bot_accounts=[bot_account]) + + # - Act - + async with lifespan_wrapper(built_bot) as bot: + bot.async_execute_raw_bot_command(payload, verify_request=False) + + # - Assert - + diff = DeepDiff( + event_deleted, + EventDeleted( + bot=BotAccount(id=bot_id, host=host), + raw_command=None, + deleted_at=datetime_formatter(event_deleted_data["deleted_at"]), + meta=event_deleted_data["meta"], + group_chat_id=UUID(event_deleted_data["group_chat_id"]), + sync_ids=[UUID(uuid_str) for uuid_str in event_deleted_data["sync_ids"]], + ), + ) + + assert diff == {}, diff