diff --git a/aiogram/utils/chat_member_adapter.py b/aiogram/utils/chat_member_adapter.py new file mode 100644 index 00000000..e97fa975 --- /dev/null +++ b/aiogram/utils/chat_member_adapter.py @@ -0,0 +1,22 @@ +from typing import Union + +from pydantic import Field, TypeAdapter +from typing_extensions import Annotated + +from aiogram import types + +ChatMemberUnion = Union[ + types.ChatMemberOwner, + types.ChatMemberAdministrator, + types.ChatMemberMember, + types.ChatMemberRestricted, + types.ChatMemberLeft, + types.ChatMemberBanned, +] + +ChatMemberAdapter = TypeAdapter( + Annotated[ + ChatMemberUnion, + Field(discriminator="status"), + ] +) diff --git a/docs/migration_2_to_3.rst b/docs/migration_2_to_3.rst index 2650cc38..284d368f 100644 --- a/docs/migration_2_to_3.rst +++ b/docs/migration_2_to_3.rst @@ -22,7 +22,7 @@ On this page, you can read about the changes made in relation to the last stable Feel free to contribute to this page, if you find something that is not mentioned here. Dependencies -========== +============ - The dependencies required for :code:`i18n` are no longer part of the default package. If your application uses translation functionality, be sure to add an optional dependency: @@ -173,7 +173,7 @@ Here are some usage examples: - Creating an object from a dictionary representation of an object -.. code-block:: + .. code-block:: # Version 2.x message_dict = {"id": 42, ...} @@ -183,6 +183,8 @@ Here are some usage examples: print(type(message_obj)) # + .. code-block:: + # Version 3.x message_dict = {"id": 42, ...} message_obj = Message.model_validate(message_dict) @@ -193,17 +195,20 @@ Here are some usage examples: - Creating a json representation of an object -.. code-block:: + .. code-block:: + # Version 2.x async def handler(message: Message) -> None: - # Version 2.x message_json = message.as_json() print(message_json) # {"id": 42, ...} print(type(message_json)) # - # Version 3.x + .. code-block:: + + # Version 3.x + async def handler(message: Message) -> None: message_json = json.dumps(deserialize_telegram_object_to_python(message)) print(message_json) # {"id": 42, ...} @@ -212,7 +217,7 @@ Here are some usage examples: - Creating a dictionary representation of an object -.. code-block:: + .. code-block:: async def handler(message: Message) -> None: # Version 2.x @@ -222,9 +227,73 @@ Here are some usage examples: print(type(message_dict)) # + .. code-block:: + + async def handler(message: Message) -> None: # Version 3.x message_dict = deserialize_telegram_object_to_python(message) print(message_dict) # {"id": 42, ...} print(type(message_dict)) # + + +ChatMember tools +================ + +- Now :class:`aiogram.types.chat_member.ChatMember` no longer contains tools to resolve an object with the appropriate status. + + .. code-block:: + + # Version 2.x + from aiogram.types import ChatMember + + chat_member = ChatMember.resolve(**dict_data) + + .. code-block:: + + # Version 3.x + from aiogram.utils.chat_member_adapter import ChatMemberAdapter + + chat_member = ChatMemberAdapter.validate_python(dict_data) + + +- Now :class:`aiogram.types.chat_member.ChatMember` and all its child classes no longer + contain methods for checking for membership in certain logical groups. + As a substitute, you can create such groups yourself and check their entry using + the :func:`isinstance` function + + .. code-block:: + + # Version 2.x + from aiogram.types import ChatMember + + if chat_member.is_chat_admin(): + print("ChatMember is chat admin") + + if chat_member.is_chat_member(): + print("ChatMember is in the chat") + + .. code-block:: + + # Version 3.x + from aiogram.utils.chat_member import resolve_chat_member + + ADMINS = (ChatMemberOwner, ChatMemberAdministrator) + MEMBERS = ( + ChatMemberOwner, + ChatMemberAdministrator, + ChatMemberMember, + ChatMemberRestricted, + ) + + if isinstance(chat_member, ADMINS): + print("ChatMember is chat admin") + + if isinstance(chat_member, MEMBERS): + print("ChatMember is in the chat") + + .. note:: + This way you can independently create any group that fits the logic of your application. + + E.g., you can create a PUNISHED group and include banned and restricted members there diff --git a/tests/test_utils/test_chat_member_adapter.py b/tests/test_utils/test_chat_member_adapter.py new file mode 100644 index 00000000..554084a4 --- /dev/null +++ b/tests/test_utils/test_chat_member_adapter.py @@ -0,0 +1,95 @@ +from datetime import datetime +from typing import Type + +import pytest + +from aiogram.types import ( + ChatMember, + ChatMemberAdministrator, + ChatMemberBanned, + ChatMemberLeft, + ChatMemberMember, + ChatMemberOwner, + ChatMemberRestricted, + User, +) +from aiogram.utils.chat_member_adapter import ChatMemberAdapter + +USER = User( + id=42, + first_name="John Doe", + is_bot=False, +).model_dump() + +CHAT_MEMBER_ADMINISTRATOR = ChatMemberAdministrator( + user=USER, + can_be_edited=False, + can_manage_chat=True, + can_change_info=True, + can_delete_messages=True, + can_invite_users=True, + can_restrict_members=True, + can_pin_messages=True, + can_manage_topics=False, + can_promote_members=False, + can_manage_video_chats=True, + can_post_stories=True, + can_edit_stories=True, + can_delete_stories=True, + is_anonymous=False, + can_manage_voice_chats=False, +).model_dump() + +CHAT_MEMBER_BANNED = ChatMemberBanned( + user=USER, + until_date=datetime.now(), +).model_dump() + +CHAT_MEMBER_LEFT = ChatMemberLeft( + user=USER, +).model_dump() + +CHAT_MEMBER_MEMBER = ChatMemberMember( + user=USER, +).model_dump() + +CHAT_MEMBER_OWNER = ChatMemberOwner( + user=USER, + is_anonymous=True, +).model_dump() + +CHAT_MEMBER_RESTRICTED = ChatMemberRestricted( + user=USER, + is_member=True, + can_send_messages=False, + can_send_audios=False, + can_send_documents=False, + can_send_photos=False, + can_send_videos=False, + can_send_video_notes=False, + can_send_voice_notes=False, + can_send_polls=False, + can_send_other_messages=False, + can_add_web_page_previews=False, + can_change_info=False, + can_invite_users=False, + can_pin_messages=False, + can_manage_topics=False, + until_date=datetime.now(), +).model_dump() + + +@pytest.mark.parametrize( + ("data", "resolved_type"), + [ + (CHAT_MEMBER_ADMINISTRATOR, ChatMemberAdministrator), + (CHAT_MEMBER_BANNED, ChatMemberBanned), + (CHAT_MEMBER_LEFT, ChatMemberLeft), + (CHAT_MEMBER_MEMBER, ChatMemberMember), + (CHAT_MEMBER_OWNER, ChatMemberOwner), + (CHAT_MEMBER_RESTRICTED, ChatMemberRestricted), + ], +) +def test_chat_member_resolution(data: dict, resolved_type: Type[ChatMember]) -> None: + chat_member = ChatMemberAdapter.validate_python(data) + assert isinstance(chat_member, resolved_type)