diff --git a/aiogram/bot/bot.py b/aiogram/bot/bot.py index 66ea8af5..66af31c7 100644 --- a/aiogram/bot/bot.py +++ b/aiogram/bot/bot.py @@ -2129,7 +2129,8 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): return types.Chat(**result) async def get_chat_administrators(self, chat_id: typing.Union[base.Integer, base.String] - ) -> typing.List[types.ChatMember]: + ) -> typing.List[typing.Union[types.ChatMemberOwner, types.ChatMemberAdministrator]]: + """ Use this method to get a list of administrators in a chat. diff --git a/aiogram/types/chat.py b/aiogram/types/chat.py index 2cd19a0f..c00a3b79 100644 --- a/aiogram/types/chat.py +++ b/aiogram/types/chat.py @@ -7,7 +7,7 @@ import typing from . import base, fields from .chat_invite_link import ChatInviteLink from .chat_location import ChatLocation -from .chat_member import ChatMember +from .chat_member import ChatMember, ChatMemberAdministrator, ChatMemberOwner from .chat_permissions import ChatPermissions from .chat_photo import ChatPhoto from .input_file import InputFile @@ -470,7 +470,7 @@ class Chat(base.TelegramObject): """ return await self.bot.leave_chat(self.id) - async def get_administrators(self) -> typing.List[ChatMember]: + async def get_administrators(self) -> typing.List[typing.Union[ChatMemberOwner, ChatMemberAdministrator]]: """ Use this method to get a list of administrators in a chat. @@ -480,7 +480,7 @@ class Chat(base.TelegramObject): chat administrators except other bots. If the chat is a group or a supergroup and no administrators were appointed, only the creator will be returned. - :rtype: :obj:`typing.List[types.ChatMember]` + :rtype: :obj:`typing.List[typing.Union[types.ChatMemberOwner, types.ChatMemberAdministrator]]` """ return await self.bot.get_chat_administrators(self.id) diff --git a/aiogram/types/chat_member.py b/aiogram/types/chat_member.py index 372b3468..ecbf9d2c 100644 --- a/aiogram/types/chat_member.py +++ b/aiogram/types/chat_member.py @@ -1,6 +1,5 @@ import datetime import typing -from typing import Optional from . import base, fields from .user import User @@ -29,6 +28,8 @@ class ChatMemberStatus(helper.Helper): def is_chat_creator(cls, role: str) -> bool: return role == cls.CREATOR + is_chat_owner = is_chat_creator + @classmethod def is_chat_admin(cls, role: str) -> bool: return role in (cls.ADMINISTRATOR, cls.CREATOR) @@ -38,7 +39,7 @@ class ChatMemberStatus(helper.Helper): return role in (cls.MEMBER, cls.ADMINISTRATOR, cls.CREATOR, cls.RESTRICTED) @classmethod - def get_class_by_status(cls, status: str) -> Optional["ChatMember"]: + def get_class_by_status(cls, status: str) -> typing.Optional[typing.Type["ChatMember"]]: return { cls.OWNER: ChatMemberOwner, cls.ADMINISTRATOR: ChatMemberAdministrator, @@ -69,7 +70,9 @@ class ChatMember(base.TelegramObject): return self.user.id @classmethod - def resolve(cls, **kwargs) -> "ChatMember": + def resolve(cls, **kwargs) -> typing.Union["ChatMemberOwner", "ChatMemberAdministrator", + "ChatMemberMember", "ChatMemberRestricted", + "ChatMemberLeft", "ChatMemberBanned"]: status = kwargs.get("status") mapping = { ChatMemberStatus.OWNER: ChatMemberOwner, @@ -89,12 +92,16 @@ class ChatMember(base.TelegramObject): def to_object(cls, data: typing.Dict[str, typing.Any], conf: typing.Dict[str, typing.Any] = None - ) -> "ChatMember": - return cls.resolve(**data) + ) -> typing.Union["ChatMemberOwner", "ChatMemberAdministrator", + "ChatMemberMember", "ChatMemberRestricted", + "ChatMemberLeft", "ChatMemberBanned"]: + return cls.resolve(conf=conf, **data) def is_chat_creator(self) -> bool: return ChatMemberStatus.is_chat_creator(self.status) + is_chat_owner = is_chat_creator + def is_chat_admin(self) -> bool: return ChatMemberStatus.is_chat_admin(self.status) @@ -113,6 +120,22 @@ class ChatMemberOwner(ChatMember): custom_title: base.String = fields.Field() is_anonymous: base.Boolean = fields.Field() + # Next fields cannot be received from API but + # it useful for compatibility and cleaner code: + # >>> if member.is_admin() and member.can_promote_members: + # >>> await message.reply('You can promote me') + can_be_edited: base.Boolean = fields.ConstField(False) + can_manage_chat: base.Boolean = fields.ConstField(True) + can_post_messages: base.Boolean = fields.ConstField(True) + can_edit_messages: base.Boolean = fields.ConstField(True) + can_delete_messages: base.Boolean = fields.ConstField(True) + can_manage_voice_chats: base.Boolean = fields.ConstField(True) + can_restrict_members: base.Boolean = fields.ConstField(True) + can_promote_members: base.Boolean = fields.ConstField(True) + can_change_info: base.Boolean = fields.ConstField(True) + can_invite_users: base.Boolean = fields.ConstField(True) + can_pin_messages: base.Boolean = fields.ConstField(True) + class ChatMemberAdministrator(ChatMember): """ diff --git a/aiogram/types/fields.py b/aiogram/types/fields.py index f898fc62..11c83eab 100644 --- a/aiogram/types/fields.py +++ b/aiogram/types/fields.py @@ -2,7 +2,7 @@ import abc import datetime import weakref -__all__ = ('BaseField', 'Field', 'ListField', 'DateTimeField', 'TextField', 'ListOfLists') +__all__ = ('BaseField', 'Field', 'ListField', 'DateTimeField', 'TextField', 'ListOfLists', 'ConstField') class BaseField(metaclass=abc.ABCMeta): @@ -192,5 +192,13 @@ class TextField(Field): def deserialize(self, value, parent=None): if value is not None and not isinstance(value, str): - raise TypeError(f"Field '{self.alias}' should be str not {type(value).__name__}") + raise TypeError(f"Field {self.alias!r} should be str not {type(value).__name__!r}") return value + + +class ConstField(Field): + def __init__(self, default=None, **kwargs): + super(ConstField, self).__init__(default=default, **kwargs) + + def __set__(self, instance, value): + raise TypeError(f"Field {self.alias!r} is not mutable") diff --git a/tests/test_bot.py b/tests/test_bot.py index 61abe962..b2da7952 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -425,14 +425,19 @@ async def test_get_chat(bot: Bot): async def test_get_chat_administrators(bot: Bot): """ getChatAdministrators method test """ - from .types.dataset import CHAT, CHAT_MEMBER + from .types.dataset import CHAT, CHAT_MEMBER, CHAT_MEMBER_OWNER chat = types.Chat(**CHAT) member = types.ChatMember.resolve(**CHAT_MEMBER) + owner = types.ChatMember.resolve(**CHAT_MEMBER_OWNER) - async with FakeTelegram(message_data=[CHAT_MEMBER, CHAT_MEMBER]): + async with FakeTelegram(message_data=[CHAT_MEMBER, CHAT_MEMBER_OWNER]): result = await bot.get_chat_administrators(chat_id=chat.id) assert result[0] == member + assert result[1] == owner assert len(result) == 2 + for m in result: + assert m.is_chat_admin() + assert hasattr(m, "can_be_edited") async def test_get_chat_member_count(bot: Bot): diff --git a/tests/types/dataset.py b/tests/types/dataset.py index a14ce316..be23da9e 100644 --- a/tests/types/dataset.py +++ b/tests/types/dataset.py @@ -44,12 +44,21 @@ CHAT_MEMBER = { "user": USER, "status": "administrator", "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_promote_members": False, + "can_manage_voice_chats": True, + "is_anonymous": False, +} + +CHAT_MEMBER_OWNER = { + "user": USER, + "status": "creator", + "is_anonymous": False, } CONTACT = {