diff --git a/README.md b/README.md index 2d3c0545..fca118a0 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ [![PyPi status](https://img.shields.io/pypi/status/aiogram.svg?style=flat-square)](https://pypi.python.org/pypi/aiogram) [![Downloads](https://img.shields.io/pypi/dm/aiogram.svg?style=flat-square)](https://pypi.python.org/pypi/aiogram) [![Supported python versions](https://img.shields.io/pypi/pyversions/aiogram.svg?style=flat-square)](https://pypi.python.org/pypi/aiogram) -[![Telegram Bot API](https://img.shields.io/badge/Telegram%20Bot%20API-5.2-blue.svg?style=flat-square&logo=telegram)](https://core.telegram.org/bots/api) +[![Telegram Bot API](https://img.shields.io/badge/Telegram%20Bot%20API-5.3-blue.svg?style=flat-square&logo=telegram)](https://core.telegram.org/bots/api) [![Documentation Status](https://img.shields.io/readthedocs/aiogram?style=flat-square)](http://docs.aiogram.dev/en/latest/?badge=latest) [![Github issues](https://img.shields.io/github/issues/aiogram/aiogram.svg?style=flat-square)](https://github.com/aiogram/aiogram/issues) [![MIT License](https://img.shields.io/pypi/l/aiogram.svg?style=flat-square)](https://opensource.org/licenses/MIT) diff --git a/README.rst b/README.rst index caf6149c..6df651a2 100644 --- a/README.rst +++ b/README.rst @@ -21,7 +21,7 @@ AIOGramBot :target: https://pypi.python.org/pypi/aiogram :alt: Supported python versions -.. image:: https://img.shields.io/badge/Telegram%20Bot%20API-5.2-blue.svg?style=flat-square&logo=telegram +.. image:: https://img.shields.io/badge/Telegram%20Bot%20API-5.3-blue.svg?style=flat-square&logo=telegram :target: https://core.telegram.org/bots/api :alt: Telegram Bot API diff --git a/aiogram/__init__.py b/aiogram/__init__.py index 3471ddcd..2d852a7e 100644 --- a/aiogram/__init__.py +++ b/aiogram/__init__.py @@ -43,5 +43,5 @@ __all__ = ( 'utils', ) -__version__ = '2.13' -__api_version__ = '5.2' +__version__ = '2.14' +__api_version__ = '5.3' diff --git a/aiogram/bot/api.py b/aiogram/bot/api.py index 38cbee89..1bf00d47 100644 --- a/aiogram/bot/api.py +++ b/aiogram/bot/api.py @@ -189,7 +189,7 @@ class Methods(Helper): """ Helper for Telegram API Methods listed on https://core.telegram.org/bots/api - List is updated to Bot API 5.2 + List is updated to Bot API 5.3 """ mode = HelperMode.lowerCamelCase @@ -225,6 +225,7 @@ class Methods(Helper): GET_USER_PROFILE_PHOTOS = Item() # getUserProfilePhotos GET_FILE = Item() # getFile KICK_CHAT_MEMBER = Item() # kickChatMember + BAN_CHAT_MEMBER = Item() # banChatMember UNBAN_CHAT_MEMBER = Item() # unbanChatMember RESTRICT_CHAT_MEMBER = Item() # restrictChatMember PROMOTE_CHAT_MEMBER = Item() # promoteChatMember @@ -244,12 +245,14 @@ class Methods(Helper): LEAVE_CHAT = Item() # leaveChat GET_CHAT = Item() # getChat GET_CHAT_ADMINISTRATORS = Item() # getChatAdministrators - GET_CHAT_MEMBERS_COUNT = Item() # getChatMembersCount + GET_CHAT_MEMBER_COUNT = Item() # getChatMemberCount + GET_CHAT_MEMBERS_COUNT = Item() # getChatMembersCount (renamed to getChatMemberCount) GET_CHAT_MEMBER = Item() # getChatMember SET_CHAT_STICKER_SET = Item() # setChatStickerSet DELETE_CHAT_STICKER_SET = Item() # deleteChatStickerSet ANSWER_CALLBACK_QUERY = Item() # answerCallbackQuery SET_MY_COMMANDS = Item() # setMyCommands + DELETE_MY_COMMANDS = Item() # deleteMyCommands GET_MY_COMMANDS = Item() # getMyCommands # Updating messages diff --git a/aiogram/bot/bot.py b/aiogram/bot/bot.py index 07d4b963..435def3e 100644 --- a/aiogram/bot/bot.py +++ b/aiogram/bot/bot.py @@ -1562,41 +1562,42 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): result = await self.request(api.Methods.GET_FILE, payload) return types.File(**result) - async def kick_chat_member(self, - chat_id: typing.Union[base.Integer, base.String], - user_id: base.Integer, - until_date: typing.Union[base.Integer, datetime.datetime, - datetime.timedelta, None] = None, - revoke_messages: typing.Optional[base.Boolean] = None, - ) -> base.Boolean: + async def ban_chat_member(self, + chat_id: typing.Union[base.Integer, base.String], + user_id: base.Integer, + until_date: typing.Union[base.Integer, datetime.datetime, + datetime.timedelta, None] = None, + revoke_messages: typing.Optional[base.Boolean] = None, + ) -> base.Boolean: """ - Use this method to kick a user from a group, a supergroup or a channel. - In the case of supergroups and channels, the user will not be able to return - to the chat on their own using invite links, etc., unless unbanned first. + Use this method to ban a user in a group, a supergroup or a + channel. In the case of supergroups and channels, the user will + not be able to return to the chat on their own using invite + links, etc., unless unbanned first. The bot must be an + administrator in the chat for this to work and must have the + appropriate admin rights. Returns True on success. - The bot must be an administrator in the chat for this to work and must have - the appropriate admin rights. + Source: https://core.telegram.org/bots/api#banchatmember - Source: https://core.telegram.org/bots/api#kickchatmember - - :param chat_id: Unique identifier for the target group or username of the - target supergroup or channel (in the format @channelusername) + :param chat_id: Unique identifier for the target group or + username of the target supergroup or channel (in the format + @channelusername) :type chat_id: :obj:`typing.Union[base.Integer, base.String]` :param user_id: Unique identifier of the target user :type user_id: :obj:`base.Integer` - :param until_date: Date when the user will be unbanned. If user is banned - for more than 366 days or less than 30 seconds from the current time they - are considered to be banned forever. Applied for supergroups and channels - only. - :type until_date: :obj:`typing.Union[base.Integer, datetime.datetime, - datetime.timedelta, None]` + :param until_date: Date when the user will be unbanned, unix + time. If user is banned for more than 366 days or less than + 30 seconds from the current time they are considered to be + banned forever. Applied for supergroups and channels only. + :type until_date: :obj:`typing.Union[base.Integer, + datetime.datetime, datetime.timedelta, None]` - :param revoke_messages: Pass True to delete all messages from the chat for - the user that is being removed. If False, the user will be able to see - messages in the group that were sent before the user was removed. Always - True for supergroups and channels. + :param revoke_messages: Pass True to delete all messages from + the chat for the user that is being removed. If False, the user + will be able to see messages in the group that were sent before + the user was removed. Always True for supergroups and channels. :type revoke_messages: :obj:`typing.Optional[base.Boolean]` :return: Returns True on success @@ -1605,7 +1606,22 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): until_date = prepare_arg(until_date) payload = generate_payload(**locals()) - return await self.request(api.Methods.KICK_CHAT_MEMBER, payload) + return await self.request(api.Methods.BAN_CHAT_MEMBER, payload) + + async def kick_chat_member(self, + chat_id: typing.Union[base.Integer, base.String], + user_id: base.Integer, + until_date: typing.Union[base.Integer, datetime.datetime, + datetime.timedelta, None] = None, + revoke_messages: typing.Optional[base.Boolean] = None, + ) -> base.Boolean: + """Renamed to ban_chat_member.""" + return await self.ban_chat_member( + chat_id=chat_id, + user_id=user_id, + until_date=until_date, + revoke_messages=revoke_messages, + ) async def unban_chat_member(self, chat_id: typing.Union[base.Integer, base.String], @@ -2130,13 +2146,13 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): payload = generate_payload(**locals()) result = await self.request(api.Methods.GET_CHAT_ADMINISTRATORS, payload) - return [types.ChatMember(**chatmember) for chatmember in result] + return [types.ChatMember.resolve(**chat_member) for chat_member in result] - async def get_chat_members_count(self, chat_id: typing.Union[base.Integer, base.String]) -> base.Integer: + async def get_chat_member_count(self, chat_id: typing.Union[base.Integer, base.String]) -> base.Integer: """ Use this method to get the number of members in a chat. - Source: https://core.telegram.org/bots/api#getchatmemberscount + Source: https://core.telegram.org/bots/api#getchatmembercount :param chat_id: Unique identifier for the target chat or username of the target supergroup or channel :type chat_id: :obj:`typing.Union[base.Integer, base.String]` @@ -2145,7 +2161,11 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): """ payload = generate_payload(**locals()) - return await self.request(api.Methods.GET_CHAT_MEMBERS_COUNT, payload) + return await self.request(api.Methods.GET_CHAT_MEMBER_COUNT, payload) + + async def get_chat_members_count(self, chat_id: typing.Union[base.Integer, base.String]) -> base.Integer: + """Renamed to get_chat_member_count.""" + return await self.get_chat_member_count(chat_id) async def get_chat_member(self, chat_id: typing.Union[base.Integer, base.String], user_id: base.Integer) -> types.ChatMember: @@ -2164,7 +2184,7 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): payload = generate_payload(**locals()) result = await self.request(api.Methods.GET_CHAT_MEMBER, payload) - return types.ChatMember(**result) + return types.ChatMember.resolve(**result) async def set_chat_sticker_set(self, chat_id: typing.Union[base.Integer, base.String], sticker_set_name: base.String) -> base.Boolean: @@ -2241,31 +2261,95 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): return await self.request(api.Methods.ANSWER_CALLBACK_QUERY, payload) - async def set_my_commands(self, commands: typing.List[types.BotCommand]) -> base.Boolean: + async def set_my_commands(self, + commands: typing.List[types.BotCommand], + scope: typing.Optional[types.BotCommandScope] = None, + language_code: typing.Optional[base.String] = None, + ) -> base.Boolean: """ Use this method to change the list of the bot's commands. Source: https://core.telegram.org/bots/api#setmycommands - :param commands: A JSON-serialized list of bot commands to be set as the list of the bot's commands. - At most 100 commands can be specified. + :param commands: A JSON-serialized list of bot commands to be + set as the list of the bot's commands. At most 100 commands + can be specified. :type commands: :obj: `typing.List[types.BotCommand]` + + :param scope: A JSON-serialized object, describing scope of + users for which the commands are relevant. Defaults to + BotCommandScopeDefault. + :type scope: :obj: `typing.Optional[types.BotCommandScope]` + + :param language_code: A two-letter ISO 639-1 language code. If + empty, commands will be applied to all users from the given + scope, for whose language there are no dedicated commands + :type language_code: :obj: `typing.Optional[base.String]` + :return: Returns True on success. :rtype: :obj:`base.Boolean` """ commands = prepare_arg(commands) + scope = prepare_arg(scope) payload = generate_payload(**locals()) return await self.request(api.Methods.SET_MY_COMMANDS, payload) - async def get_my_commands(self) -> typing.List[types.BotCommand]: + async def delete_my_commands(self, + scope: typing.Optional[types.BotCommandScope] = None, + language_code: typing.Optional[base.String] = None, + ) -> base.Boolean: """ - Use this method to get the current list of the bot's commands. + Use this method to delete the list of the bot's commands for the + given scope and user language. After deletion, higher level + commands will be shown to affected users. + + Source: https://core.telegram.org/bots/api#deletemycommands + + :param scope: A JSON-serialized object, describing scope of + users for which the commands are relevant. Defaults to + BotCommandScopeDefault. + :type scope: :obj: `typing.Optional[types.BotCommandScope]` + + :param language_code: A two-letter ISO 639-1 language code. If + empty, commands will be applied to all users from the given + scope, for whose language there are no dedicated commands + :type language_code: :obj: `typing.Optional[base.String]` + + :return: Returns True on success. + :rtype: :obj:`base.Boolean` + """ + scope = prepare_arg(scope) + payload = generate_payload(**locals()) + + return await self.request(api.Methods.DELETE_MY_COMMANDS, payload) + + async def get_my_commands(self, + scope: typing.Optional[types.BotCommandScope] = None, + language_code: typing.Optional[base.String] = None, + ) -> typing.List[types.BotCommand]: + """ + Use this method to get the current list of the bot's commands + for the given scope and user language. Returns Array of + BotCommand on success. If commands aren't set, an empty list is + returned. Source: https://core.telegram.org/bots/api#getmycommands - :return: Returns Array of BotCommand on success. + + :param scope: A JSON-serialized object, describing scope of + users for which the commands are relevant. Defaults to + BotCommandScopeDefault. + :type scope: :obj: `typing.Optional[types.BotCommandScope]` + + :param language_code: A two-letter ISO 639-1 language code. If + empty, commands will be applied to all users from the given + scope, for whose language there are no dedicated commands + :type language_code: :obj: `typing.Optional[base.String]` + + :return: Returns Array of BotCommand on success or empty list. :rtype: :obj:`typing.List[types.BotCommand]` """ + scope = prepare_arg(scope) payload = generate_payload(**locals()) result = await self.request(api.Methods.GET_MY_COMMANDS, payload) diff --git a/aiogram/types/__init__.py b/aiogram/types/__init__.py index 90909e81..a9e6af8c 100644 --- a/aiogram/types/__init__.py +++ b/aiogram/types/__init__.py @@ -4,6 +4,10 @@ from .animation import Animation from .audio import Audio from .auth_widget_data import AuthWidgetData from .bot_command import BotCommand +from .bot_command_scope import BotCommandScope, BotCommandScopeAllChatAdministrators, \ + BotCommandScopeAllGroupChats, BotCommandScopeAllPrivateChats, BotCommandScopeChat, \ + BotCommandScopeChatAdministrators, BotCommandScopeChatMember, \ + BotCommandScopeDefault, BotCommandScopeType from .callback_game import CallbackGame from .callback_query import CallbackQuery from .chat import Chat, ChatActions, ChatType @@ -82,6 +86,15 @@ __all__ = ( 'Audio', 'AuthWidgetData', 'BotCommand', + 'BotCommandScope', + 'BotCommandScopeAllChatAdministrators', + 'BotCommandScopeAllGroupChats', + 'BotCommandScopeAllPrivateChats', + 'BotCommandScopeChat', + 'BotCommandScopeChatAdministrators', + 'BotCommandScopeChatMember', + 'BotCommandScopeDefault', + 'BotCommandScopeType', 'CallbackGame', 'CallbackQuery', 'Chat', diff --git a/aiogram/types/bot_command_scope.py b/aiogram/types/bot_command_scope.py new file mode 100644 index 00000000..e3091a7e --- /dev/null +++ b/aiogram/types/bot_command_scope.py @@ -0,0 +1,121 @@ +import typing + +from . import base, fields +from ..utils import helper + + +class BotCommandScopeType(helper.Helper): + mode = helper.HelperMode.lowercase + + DEFAULT = helper.Item() # default + ALL_PRIVATE_CHATS = helper.Item() # all_private_chats + ALL_GROUP_CHATS = helper.Item() # all_group_chats + ALL_CHAT_ADMINISTRATORS = helper.Item() # all_chat_administrators + CHAT = helper.Item() # chat + CHAT_ADMINISTRATORS = helper.Item() # chat_administrators + CHAT_MEMBER = helper.Item() # chat_member + + +class BotCommandScope(base.TelegramObject): + """ + This object represents the scope to which bot commands are applied. + Currently, the following 7 scopes are supported: + BotCommandScopeDefault + BotCommandScopeAllPrivateChats + BotCommandScopeAllGroupChats + BotCommandScopeAllChatAdministrators + BotCommandScopeChat + BotCommandScopeChatAdministrators + BotCommandScopeChatMember + + https://core.telegram.org/bots/api#botcommandscope + """ + type: base.String = fields.Field() + + @classmethod + def from_type(cls, type: str, **kwargs: typing.Any): + if type == BotCommandScopeType.DEFAULT: + return BotCommandScopeDefault(type=type, **kwargs) + if type == BotCommandScopeType.ALL_PRIVATE_CHATS: + return BotCommandScopeAllPrivateChats(type=type, **kwargs) + if type == BotCommandScopeType.ALL_GROUP_CHATS: + return BotCommandScopeAllGroupChats(type=type, **kwargs) + if type == BotCommandScopeType.ALL_CHAT_ADMINISTRATORS: + return BotCommandScopeAllChatAdministrators(type=type, **kwargs) + if type == BotCommandScopeType.CHAT: + return BotCommandScopeChat(type=type, **kwargs) + if type == BotCommandScopeType.CHAT_ADMINISTRATORS: + return BotCommandScopeChatAdministrators(type=type, **kwargs) + if type == BotCommandScopeType.CHAT_MEMBER: + return BotCommandScopeChatMember(type=type, **kwargs) + raise ValueError(f"Unknown BotCommandScope type {type!r}") + + +class BotCommandScopeDefault(BotCommandScope): + """ + Represents the default scope of bot commands. + Default commands are used if no commands with a narrower scope are + specified for the user. + """ + type = fields.Field(default=BotCommandScopeType.DEFAULT) + + +class BotCommandScopeAllPrivateChats(BotCommandScope): + """ + Represents the scope of bot commands, covering all private chats. + """ + type = fields.Field(default=BotCommandScopeType.ALL_PRIVATE_CHATS) + + +class BotCommandScopeAllGroupChats(BotCommandScope): + """ + Represents the scope of bot commands, covering all group and + supergroup chats. + """ + type = fields.Field(default=BotCommandScopeType.ALL_GROUP_CHATS) + + +class BotCommandScopeAllChatAdministrators(BotCommandScope): + """ + Represents the scope of bot commands, covering all group and + supergroup chat administrators. + """ + type = fields.Field(default=BotCommandScopeType.ALL_CHAT_ADMINISTRATORS) + + +class BotCommandScopeChat(BotCommandScope): + """ + Represents the scope of bot commands, covering a specific chat. + """ + type = fields.Field(default=BotCommandScopeType.CHAT) + chat_id: typing.Union[base.String, base.Integer] = fields.Field() + + def __init__(self, chat_id: typing.Union[base.String, base.Integer], **kwargs): + super().__init__(chat_id=chat_id, **kwargs) + + +class BotCommandScopeChatAdministrators(BotCommandScopeChat): + """ + Represents the scope of bot commands, covering all administrators + of a specific group or supergroup chat. + """ + type = fields.Field(default=BotCommandScopeType.CHAT_ADMINISTRATORS) + chat_id: typing.Union[base.String, base.Integer] = fields.Field() + + +class BotCommandScopeChatMember(BotCommandScopeChat): + """ + Represents the scope of bot commands, covering a specific member of + a group or supergroup chat. + """ + type = fields.Field(default=BotCommandScopeType.CHAT_MEMBER) + chat_id: typing.Union[base.String, base.Integer] = fields.Field() + user_id: base.Integer = fields.Field() + + def __init__( + self, + chat_id: typing.Union[base.String, base.Integer], + user_id: base.Integer, + **kwargs, + ): + super().__init__(chat_id=chat_id, user_id=user_id, **kwargs) diff --git a/aiogram/types/chat.py b/aiogram/types/chat.py index 5b3b315a..2cd19a0f 100644 --- a/aiogram/types/chat.py +++ b/aiogram/types/chat.py @@ -301,7 +301,7 @@ class Chat(base.TelegramObject): can_send_other_messages=can_send_other_messages, can_add_web_page_previews=can_add_web_page_previews) - async def promote(self, + async def promote(self, user_id: base.Integer, is_anonymous: typing.Optional[base.Boolean] = None, can_change_info: typing.Optional[base.Boolean] = None, @@ -321,36 +321,36 @@ class Chat(base.TelegramObject): :param user_id: Unique identifier of the target user :type user_id: :obj:`base.Integer` - + :param is_anonymous: Pass True, if the administrator's presence in the chat is hidden :type is_anonymous: :obj:`typing.Optional[base.Boolean]` - + :param can_change_info: Pass True, if the administrator can change chat title, photo and other settings :type can_change_info: :obj:`typing.Optional[base.Boolean]` - + :param can_post_messages: Pass True, if the administrator can create channel posts, channels only :type can_post_messages: :obj:`typing.Optional[base.Boolean]` - + :param can_edit_messages: Pass True, if the administrator can edit messages of other users, channels only :type can_edit_messages: :obj:`typing.Optional[base.Boolean]` - + :param can_delete_messages: Pass True, if the administrator can delete messages of other users :type can_delete_messages: :obj:`typing.Optional[base.Boolean]` - + :param can_invite_users: Pass True, if the administrator can invite new users to the chat :type can_invite_users: :obj:`typing.Optional[base.Boolean]` - + :param can_restrict_members: Pass True, if the administrator can restrict, ban or unban chat members :type can_restrict_members: :obj:`typing.Optional[base.Boolean]` - + :param can_pin_messages: Pass True, if the administrator can pin messages, supergroups only :type can_pin_messages: :obj:`typing.Optional[base.Boolean]` - + :param can_promote_members: Pass True, if the administrator can add new administrators with a subset of his own privileges or demote administrators that he has promoted, directly or indirectly (promoted by administrators that were appointed by him) :type can_promote_members: :obj:`typing.Optional[base.Boolean]` - + :return: Returns True on success. :rtype: :obj:`base.Boolean` """ @@ -484,16 +484,20 @@ class Chat(base.TelegramObject): """ return await self.bot.get_chat_administrators(self.id) - async def get_members_count(self) -> base.Integer: + async def get_member_count(self) -> base.Integer: """ Use this method to get the number of members in a chat. - Source: https://core.telegram.org/bots/api#getchatmemberscount + Source: https://core.telegram.org/bots/api#getchatmembercount :return: Returns Int on success. :rtype: :obj:`base.Integer` """ - return await self.bot.get_chat_members_count(self.id) + return await self.bot.get_chat_member_count(self.id) + + async def get_members_count(self) -> base.Integer: + """Renamed to get_member_count.""" + return await self.get_member_count(self.id) async def get_member(self, user_id: base.Integer) -> ChatMember: """ diff --git a/aiogram/types/chat_member.py b/aiogram/types/chat_member.py index c48a91d0..58e4cb62 100644 --- a/aiogram/types/chat_member.py +++ b/aiogram/types/chat_member.py @@ -1,53 +1,11 @@ import datetime +from typing import Optional -from . import base -from . import fields +from . import base, fields from .user import User from ..utils import helper -class ChatMember(base.TelegramObject): - """ - This object contains information about one member of a chat. - - https://core.telegram.org/bots/api#chatmember - """ - user: User = fields.Field(base=User) - status: base.String = fields.Field() - custom_title: base.String = fields.Field() - is_anonymous: base.Boolean = fields.Field() - can_be_edited: base.Boolean = fields.Field() - can_manage_chat: base.Boolean = fields.Field() - can_post_messages: base.Boolean = fields.Field() - can_edit_messages: base.Boolean = fields.Field() - can_delete_messages: base.Boolean = fields.Field() - can_manage_voice_chats: base.Boolean = fields.Field() - can_restrict_members: base.Boolean = fields.Field() - can_promote_members: base.Boolean = fields.Field() - can_change_info: base.Boolean = fields.Field() - can_invite_users: base.Boolean = fields.Field() - can_pin_messages: base.Boolean = fields.Field() - is_member: base.Boolean = fields.Field() - can_send_messages: base.Boolean = fields.Field() - can_send_media_messages: base.Boolean = fields.Field() - can_send_polls: base.Boolean = fields.Field() - can_send_other_messages: base.Boolean = fields.Field() - can_add_web_page_previews: base.Boolean = fields.Field() - until_date: datetime.datetime = fields.DateTimeField() - - def is_chat_creator(self) -> bool: - return ChatMemberStatus.is_chat_creator(self.status) - - def is_chat_admin(self) -> bool: - return ChatMemberStatus.is_chat_admin(self.status) - - def is_chat_member(self) -> bool: - return ChatMemberStatus.is_chat_member(self.status) - - def __int__(self) -> int: - return self.user.id - - class ChatMemberStatus(helper.Helper): """ Chat member status @@ -55,11 +13,13 @@ class ChatMemberStatus(helper.Helper): mode = helper.HelperMode.lowercase CREATOR = helper.Item() # creator + OWNER = CREATOR # creator ADMINISTRATOR = helper.Item() # administrator MEMBER = helper.Item() # member RESTRICTED = helper.Item() # restricted LEFT = helper.Item() # left KICKED = helper.Item() # kicked + BANNED = KICKED # kicked @classmethod def is_chat_creator(cls, role: str) -> bool: @@ -72,3 +32,141 @@ class ChatMemberStatus(helper.Helper): @classmethod def is_chat_member(cls, role: str) -> bool: return role in (cls.MEMBER, cls.ADMINISTRATOR, cls.CREATOR, cls.RESTRICTED) + + @classmethod + def get_class_by_status(cls, status: str) -> Optional["ChatMember"]: + return { + cls.OWNER: ChatMemberOwner, + cls.ADMINISTRATOR: ChatMemberAdministrator, + cls.MEMBER: ChatMemberMember, + cls.RESTRICTED: ChatMemberRestricted, + cls.LEFT: ChatMemberLeft, + cls.BANNED: ChatMemberBanned, + }.get(status) + + +class ChatMember(base.TelegramObject): + """ + This object contains information about one member of a chat. + Currently, the following 6 types of chat members are supported: + ChatMemberOwner + ChatMemberAdministrator + ChatMemberMember + ChatMemberRestricted + ChatMemberLeft + ChatMemberBanned + + https://core.telegram.org/bots/api#chatmember + """ + status: base.String = fields.Field() + user: User = fields.Field(base=User) + + def __int__(self) -> int: + return self.user.id + + @classmethod + def resolve(cls, **kwargs) -> "ChatMember": + status = kwargs.get("status") + mapping = { + ChatMemberStatus.OWNER: ChatMemberOwner, + ChatMemberStatus.ADMINISTRATOR: ChatMemberAdministrator, + ChatMemberStatus.MEMBER: ChatMemberMember, + ChatMemberStatus.RESTRICTED: ChatMemberRestricted, + ChatMemberStatus.LEFT: ChatMemberLeft, + ChatMemberStatus.BANNED: ChatMemberBanned, + } + class_ = mapping.get(status) + if class_ is None: + raise ValueError(f"Can't find `ChatMember` class for status `{status}`") + + return class_(**kwargs) + + +class ChatMemberOwner(ChatMember): + """ + Represents a chat member that owns the chat and has all + administrator privileges. + https://core.telegram.org/bots/api#chatmemberowner + """ + status: base.String = fields.Field(default=ChatMemberStatus.OWNER) + user: User = fields.Field(base=User) + custom_title: base.String = fields.Field() + is_anonymous: base.Boolean = fields.Field() + + +class ChatMemberAdministrator(ChatMember): + """ + Represents a chat member that has some additional privileges. + + https://core.telegram.org/bots/api#chatmemberadministrator + """ + status: base.String = fields.Field(default=ChatMemberStatus.ADMINISTRATOR) + user: User = fields.Field(base=User) + can_be_edited: base.Boolean = fields.Field() + custom_title: base.String = fields.Field() + is_anonymous: base.Boolean = fields.Field() + can_manage_chat: base.Boolean = fields.Field() + can_post_messages: base.Boolean = fields.Field() + can_edit_messages: base.Boolean = fields.Field() + can_delete_messages: base.Boolean = fields.Field() + can_manage_voice_chats: base.Boolean = fields.Field() + can_restrict_members: base.Boolean = fields.Field() + can_promote_members: base.Boolean = fields.Field() + can_change_info: base.Boolean = fields.Field() + can_invite_users: base.Boolean = fields.Field() + can_pin_messages: base.Boolean = fields.Field() + + +class ChatMemberMember(ChatMember): + """ + Represents a chat member that has no additional privileges or + restrictions. + + https://core.telegram.org/bots/api#chatmembermember + """ + status: base.String = fields.Field(default=ChatMemberStatus.MEMBER) + user: User = fields.Field(base=User) + + +class ChatMemberRestricted(ChatMember): + """ + Represents a chat member that is under certain restrictions in the + chat. Supergroups only. + + https://core.telegram.org/bots/api#chatmemberrestricted + """ + status: base.String = fields.Field(default=ChatMemberStatus.RESTRICTED) + user: User = fields.Field(base=User) + is_member: base.Boolean = fields.Field() + can_change_info: base.Boolean = fields.Field() + can_invite_users: base.Boolean = fields.Field() + can_pin_messages: base.Boolean = fields.Field() + can_send_messages: base.Boolean = fields.Field() + can_send_media_messages: base.Boolean = fields.Field() + can_send_polls: base.Boolean = fields.Field() + can_send_other_messages: base.Boolean = fields.Field() + can_add_web_page_previews: base.Boolean = fields.Field() + until_date: datetime.datetime = fields.DateTimeField() + + +class ChatMemberLeft(ChatMember): + """ + Represents a chat member that isn't currently a member of the chat, + but may join it themselves. + + https://core.telegram.org/bots/api#chatmemberleft + """ + status: base.String = fields.Field(default=ChatMemberStatus.LEFT) + user: User = fields.Field(base=User) + + +class ChatMemberBanned(ChatMember): + """ + Represents a chat member that was banned in the chat and can't + return to the chat or view chat messages. + + https://core.telegram.org/bots/api#chatmemberbanned + """ + status: base.String = fields.Field(default=ChatMemberStatus.BANNED) + user: User = fields.Field(base=User) + until_date: datetime.datetime = fields.DateTimeField() diff --git a/aiogram/types/force_reply.py b/aiogram/types/force_reply.py index 97ec16c6..d6b4f19f 100644 --- a/aiogram/types/force_reply.py +++ b/aiogram/types/force_reply.py @@ -6,31 +6,28 @@ from . import fields class ForceReply(base.TelegramObject): """ - Upon receiving a message with this object, - Telegram clients will display a reply interface to the user - (act as if the user has selected the bot‘s message and tapped ’Reply'). - This can be extremely useful if you want to create user-friendly step-by-step + Upon receiving a message with this object, Telegram clients will + display a reply interface to the user (act as if the user has + selected the bot's message and tapped 'Reply'). This can be + extremely useful if you want to create user-friendly step-by-step interfaces without having to sacrifice privacy mode. - Example: A poll bot for groups runs in privacy mode - (only receives commands, replies to its messages and mentions). - There could be two ways to create a new poll - - The last option is definitely more attractive. - And if you use ForceReply in your bot‘s questions, it will receive the user’s answers even - if it only receives replies, commands and mentions — without any extra work for the user. - https://core.telegram.org/bots/api#forcereply """ force_reply: base.Boolean = fields.Field(default=True) + input_field_placeholder: base.String = fields.Field() selective: base.Boolean = fields.Field() @classmethod - def create(cls, selective: typing.Optional[base.Boolean] = None): + def create(cls, + input_field_placeholder: typing.Optional[base.String] = None, + selective: typing.Optional[base.Boolean] = None, + ) -> 'ForceReply': """ Create new force reply :param selective: + :param input_field_placeholder: :return: """ - return cls(selective=selective) + return cls(selective=selective, input_field_placeholder=input_field_placeholder) diff --git a/aiogram/types/reply_keyboard.py b/aiogram/types/reply_keyboard.py index ffe07ae1..e648e036 100644 --- a/aiogram/types/reply_keyboard.py +++ b/aiogram/types/reply_keyboard.py @@ -18,23 +18,32 @@ class KeyboardButtonPollType(base.TelegramObject): class ReplyKeyboardMarkup(base.TelegramObject): """ - This object represents a custom keyboard with reply options (see Introduction to bots for details and examples). + This object represents a custom keyboard with reply options + (see https://core.telegram.org/bots#keyboards to bots for details + and examples). https://core.telegram.org/bots/api#replykeyboardmarkup """ keyboard: 'typing.List[typing.List[KeyboardButton]]' = fields.ListOfLists(base='KeyboardButton', default=[]) resize_keyboard: base.Boolean = fields.Field() one_time_keyboard: base.Boolean = fields.Field() + input_field_placeholder: base.String = fields.Field() selective: base.Boolean = fields.Field() def __init__(self, keyboard: 'typing.List[typing.List[KeyboardButton]]' = None, resize_keyboard: base.Boolean = None, one_time_keyboard: base.Boolean = None, + input_field_placeholder: base.String = None, selective: base.Boolean = None, row_width: base.Integer = 3): - super(ReplyKeyboardMarkup, self).__init__(keyboard=keyboard, resize_keyboard=resize_keyboard, - one_time_keyboard=one_time_keyboard, selective=selective, - conf={'row_width': row_width}) + super().__init__( + keyboard=keyboard, + resize_keyboard=resize_keyboard, + one_time_keyboard=one_time_keyboard, + input_field_placeholder=input_field_placeholder, + selective=selective, + conf={'row_width': row_width}, + ) @property def row_width(self): diff --git a/docs/source/index.rst b/docs/source/index.rst index 3631b150..cd4b99d0 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -22,7 +22,7 @@ Welcome to aiogram's documentation! :target: https://pypi.python.org/pypi/aiogram :alt: Supported python versions - .. image:: https://img.shields.io/badge/Telegram%20Bot%20API-5.2-blue.svg?style=flat-square&logo=telegram + .. image:: https://img.shields.io/badge/Telegram%20Bot%20API-5.3-blue.svg?style=flat-square&logo=telegram :target: https://core.telegram.org/bots/api :alt: Telegram Bot API diff --git a/tests/test_bot.py b/tests/test_bot.py index 224666ec..61abe962 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -427,7 +427,7 @@ async def test_get_chat_administrators(bot: Bot): """ getChatAdministrators method test """ from .types.dataset import CHAT, CHAT_MEMBER chat = types.Chat(**CHAT) - member = types.ChatMember(**CHAT_MEMBER) + member = types.ChatMember.resolve(**CHAT_MEMBER) async with FakeTelegram(message_data=[CHAT_MEMBER, CHAT_MEMBER]): result = await bot.get_chat_administrators(chat_id=chat.id) @@ -435,14 +435,14 @@ async def test_get_chat_administrators(bot: Bot): assert len(result) == 2 -async def test_get_chat_members_count(bot: Bot): +async def test_get_chat_member_count(bot: Bot): """ getChatMembersCount method test """ from .types.dataset import CHAT chat = types.Chat(**CHAT) count = 5 async with FakeTelegram(message_data=count): - result = await bot.get_chat_members_count(chat_id=chat.id) + result = await bot.get_chat_member_count(chat_id=chat.id) assert result == count @@ -450,7 +450,7 @@ async def test_get_chat_member(bot: Bot): """ getChatMember method test """ from .types.dataset import CHAT, CHAT_MEMBER chat = types.Chat(**CHAT) - member = types.ChatMember(**CHAT_MEMBER) + member = types.ChatMember.resolve(**CHAT_MEMBER) async with FakeTelegram(message_data=CHAT_MEMBER): result = await bot.get_chat_member(chat_id=chat.id, user_id=member.user.id) diff --git a/tests/types/test_chat_member.py b/tests/types/test_chat_member.py index 2cea44ce..2fe3e677 100644 --- a/tests/types/test_chat_member.py +++ b/tests/types/test_chat_member.py @@ -1,7 +1,7 @@ from aiogram import types from .dataset import CHAT_MEMBER -chat_member = types.ChatMember(**CHAT_MEMBER) +chat_member = types.ChatMember.resolve(**CHAT_MEMBER) def test_export():