From 46e033e6da07c63d1ec1ffe95d0adc6562ef62ad Mon Sep 17 00:00:00 2001 From: Oleg A Date: Sat, 6 Jul 2024 20:34:07 +0300 Subject: [PATCH] Add chat member tools (#1527) * feat: add ChatMemberAdapter * chore: apply formatter * docs: added changelog * docs: rm redundant import * feat: add pre-defined groups --- CHANGES/1525.feature.rst | 1 + aiogram/utils/chat_member.py | 37 +++++++++++ docs/migration_2_to_3.rst | 73 +++++++++++++++++++-- tests/test_utils/test_chat_member.py | 95 ++++++++++++++++++++++++++++ 4 files changed, 200 insertions(+), 6 deletions(-) create mode 100644 CHANGES/1525.feature.rst create mode 100644 aiogram/utils/chat_member.py create mode 100644 tests/test_utils/test_chat_member.py diff --git a/CHANGES/1525.feature.rst b/CHANGES/1525.feature.rst new file mode 100644 index 00000000..a8c03aca --- /dev/null +++ b/CHANGES/1525.feature.rst @@ -0,0 +1 @@ +Added ChatMember resolution tool and updated 2.x migration guide. diff --git a/aiogram/utils/chat_member.py b/aiogram/utils/chat_member.py new file mode 100644 index 00000000..9d86823d --- /dev/null +++ b/aiogram/utils/chat_member.py @@ -0,0 +1,37 @@ +from typing import Tuple, Type, Union + +from pydantic import Field, TypeAdapter +from typing_extensions import Annotated + +from aiogram.types import ( + ChatMember, + ChatMemberAdministrator, + ChatMemberBanned, + ChatMemberLeft, + ChatMemberMember, + ChatMemberOwner, + ChatMemberRestricted, +) + +ChatMemberUnion = Union[ + ChatMemberOwner, + ChatMemberAdministrator, + ChatMemberMember, + ChatMemberRestricted, + ChatMemberLeft, + ChatMemberBanned, +] + +ChatMemberCollection = Tuple[Type[ChatMember], ...] + +ChatMemberAdapter = TypeAdapter( + Annotated[ + ChatMemberUnion, + Field(discriminator="status"), + ] +) + +ADMINS: ChatMemberCollection = (ChatMemberOwner, ChatMemberAdministrator) +USERS: ChatMemberCollection = (ChatMemberMember, ChatMemberRestricted) +MEMBERS: ChatMemberCollection = ADMINS + USERS +NOT_MEMBERS: ChatMemberCollection = (ChatMemberLeft, ChatMemberBanned) diff --git a/docs/migration_2_to_3.rst b/docs/migration_2_to_3.rst index 8b0e6229..0ac24c99 100644 --- a/docs/migration_2_to_3.rst +++ b/docs/migration_2_to_3.rst @@ -26,7 +26,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: @@ -178,7 +178,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, ...} @@ -188,6 +188,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) @@ -198,17 +200,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, ...} @@ -217,7 +222,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 @@ -227,9 +232,65 @@ 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 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 use pre-defined groups or create such groups yourself + and check their entry using the :func:`isinstance` function + + .. code-block:: + + # Version 2.x + + 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 ADMINS, MEMBERS + + if isinstance(chat_member, ADMINS): + print("ChatMember is chat admin") + + if isinstance(chat_member, MEMBERS): + print("ChatMember is in the chat") + + .. note:: + You also can independently create group similar to ADMINS 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.py b/tests/test_utils/test_chat_member.py new file mode 100644 index 00000000..2f0f9eef --- /dev/null +++ b/tests/test_utils/test_chat_member.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 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)