diff --git a/README.rst b/README.rst index 6e8aa44ce..41ce1c86d 100644 --- a/README.rst +++ b/README.rst @@ -20,7 +20,7 @@ We have a vibrant community of developers helping each other in our `Telegram gr :target: https://pypi.org/project/python-telegram-bot/ :alt: Supported Python versions -.. image:: https://img.shields.io/badge/Bot%20API-5.2-blue?logo=telegram +.. image:: https://img.shields.io/badge/Bot%20API-5.3-blue?logo=telegram :target: https://core.telegram.org/bots/api-changelog :alt: Supported Bot API versions @@ -111,7 +111,7 @@ Installing both ``python-telegram-bot`` and ``python-telegram-bot-raw`` in conju Telegram API support ==================== -All types and methods of the Telegram Bot API **5.2** are supported. +All types and methods of the Telegram Bot API **5.3** are supported. ========== Installing diff --git a/README_RAW.rst b/README_RAW.rst index 3020fec94..7a8c8fd5e 100644 --- a/README_RAW.rst +++ b/README_RAW.rst @@ -20,7 +20,7 @@ We have a vibrant community of developers helping each other in our `Telegram gr :target: https://pypi.org/project/python-telegram-bot-raw/ :alt: Supported Python versions -.. image:: https://img.shields.io/badge/Bot%20API-5.2-blue?logo=telegram +.. image:: https://img.shields.io/badge/Bot%20API-5.3-blue?logo=telegram :target: https://core.telegram.org/bots/api-changelog :alt: Supported Bot API versions @@ -105,7 +105,7 @@ Installing both ``python-telegram-bot`` and ``python-telegram-bot-raw`` in conju Telegram API support ==================== -All types and methods of the Telegram Bot API **5.2** are supported. +All types and methods of the Telegram Bot API **5.3** are supported. ========== Installing diff --git a/docs/source/telegram.botcommandscope.rst b/docs/source/telegram.botcommandscope.rst new file mode 100644 index 000000000..625712520 --- /dev/null +++ b/docs/source/telegram.botcommandscope.rst @@ -0,0 +1,8 @@ +:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/botcommandscope.py + +telegram.BotCommandScope +======================== + +.. autoclass:: telegram.BotCommandScope + :members: + :show-inheritance: \ No newline at end of file diff --git a/docs/source/telegram.botcommandscopeallchatadministrators.rst b/docs/source/telegram.botcommandscopeallchatadministrators.rst new file mode 100644 index 000000000..fa30ce538 --- /dev/null +++ b/docs/source/telegram.botcommandscopeallchatadministrators.rst @@ -0,0 +1,8 @@ +:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/botcommandscope.py + +telegram.BotCommandScopeAllChatAdministrators +============================================= + +.. autoclass:: telegram.BotCommandScopeAllChatAdministrators + :members: + :show-inheritance: \ No newline at end of file diff --git a/docs/source/telegram.botcommandscopeallgroupchats.rst b/docs/source/telegram.botcommandscopeallgroupchats.rst new file mode 100644 index 000000000..2c4f672eb --- /dev/null +++ b/docs/source/telegram.botcommandscopeallgroupchats.rst @@ -0,0 +1,8 @@ +:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/botcommandscope.py + +telegram.BotCommandScopeAllGroupChats +======================================= + +.. autoclass:: telegram.BotCommandScopeAllGroupChats + :members: + :show-inheritance: \ No newline at end of file diff --git a/docs/source/telegram.botcommandscopeallprivatechats.rst b/docs/source/telegram.botcommandscopeallprivatechats.rst new file mode 100644 index 000000000..91ea4d034 --- /dev/null +++ b/docs/source/telegram.botcommandscopeallprivatechats.rst @@ -0,0 +1,8 @@ +:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/botcommandscope.py + +telegram.BotCommandScopeAllPrivateChats +======================================= + +.. autoclass:: telegram.BotCommandScopeAllPrivateChats + :members: + :show-inheritance: \ No newline at end of file diff --git a/docs/source/telegram.botcommandscopechat.rst b/docs/source/telegram.botcommandscopechat.rst new file mode 100644 index 000000000..dd7795428 --- /dev/null +++ b/docs/source/telegram.botcommandscopechat.rst @@ -0,0 +1,8 @@ +:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/botcommandscope.py + +telegram.BotCommandScopeChat +============================ + +.. autoclass:: telegram.BotCommandScopeChat + :members: + :show-inheritance: \ No newline at end of file diff --git a/docs/source/telegram.botcommandscopechatadministrators.rst b/docs/source/telegram.botcommandscopechatadministrators.rst new file mode 100644 index 000000000..68cb13282 --- /dev/null +++ b/docs/source/telegram.botcommandscopechatadministrators.rst @@ -0,0 +1,8 @@ +:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/botcommandscope.py + +telegram.BotCommandScopeChatAdministrators +========================================== + +.. autoclass:: telegram.BotCommandScopeChatAdministrators + :members: + :show-inheritance: \ No newline at end of file diff --git a/docs/source/telegram.botcommandscopechatmember.rst b/docs/source/telegram.botcommandscopechatmember.rst new file mode 100644 index 000000000..e28105179 --- /dev/null +++ b/docs/source/telegram.botcommandscopechatmember.rst @@ -0,0 +1,8 @@ +:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/botcommandscope.py + +telegram.BotCommandScopeChatMember +================================== + +.. autoclass:: telegram.BotCommandScopeChatMember + :members: + :show-inheritance: \ No newline at end of file diff --git a/docs/source/telegram.botcommandscopedefault.rst b/docs/source/telegram.botcommandscopedefault.rst new file mode 100644 index 000000000..2705a734c --- /dev/null +++ b/docs/source/telegram.botcommandscopedefault.rst @@ -0,0 +1,8 @@ +:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/botcommandscope.py + +telegram.BotCommandScopeDefault +=============================== + +.. autoclass:: telegram.BotCommandScopeDefault + :members: + :show-inheritance: \ No newline at end of file diff --git a/docs/source/telegram.chatmemberadministrator.rst b/docs/source/telegram.chatmemberadministrator.rst new file mode 100644 index 000000000..ac5108651 --- /dev/null +++ b/docs/source/telegram.chatmemberadministrator.rst @@ -0,0 +1,8 @@ +:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/chatmember.py + +telegram.ChatMemberAdministrator +================================ + +.. autoclass:: telegram.ChatMemberAdministrator + :members: + :show-inheritance: diff --git a/docs/source/telegram.chatmemberbanned.rst b/docs/source/telegram.chatmemberbanned.rst new file mode 100644 index 000000000..168c2df96 --- /dev/null +++ b/docs/source/telegram.chatmemberbanned.rst @@ -0,0 +1,8 @@ +:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/chatmember.py + +telegram.ChatMemberBanned +========================= + +.. autoclass:: telegram.ChatMemberBanned + :members: + :show-inheritance: diff --git a/docs/source/telegram.chatmemberleft.rst b/docs/source/telegram.chatmemberleft.rst new file mode 100644 index 000000000..6840063c6 --- /dev/null +++ b/docs/source/telegram.chatmemberleft.rst @@ -0,0 +1,8 @@ +:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/chatmember.py + +telegram.ChatMemberLeft +======================= + +.. autoclass:: telegram.ChatMemberLeft + :members: + :show-inheritance: diff --git a/docs/source/telegram.chatmembermember.rst b/docs/source/telegram.chatmembermember.rst new file mode 100644 index 000000000..7e2d8293b --- /dev/null +++ b/docs/source/telegram.chatmembermember.rst @@ -0,0 +1,8 @@ +:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/chatmember.py + +telegram.ChatMemberMember +========================= + +.. autoclass:: telegram.ChatMemberMember + :members: + :show-inheritance: diff --git a/docs/source/telegram.chatmemberowner.rst b/docs/source/telegram.chatmemberowner.rst new file mode 100644 index 000000000..784cb644a --- /dev/null +++ b/docs/source/telegram.chatmemberowner.rst @@ -0,0 +1,9 @@ +:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/chatmember.py + +telegram.ChatMemberOwner +======================== + +.. autoclass:: telegram.ChatMemberOwner + :members: + :show-inheritance: + diff --git a/docs/source/telegram.chatmemberrestricted.rst b/docs/source/telegram.chatmemberrestricted.rst new file mode 100644 index 000000000..cd76611fd --- /dev/null +++ b/docs/source/telegram.chatmemberrestricted.rst @@ -0,0 +1,8 @@ +:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/chatmember.py + +telegram.ChatMemberRestricted +============================= + +.. autoclass:: telegram.ChatMemberRestricted + :members: + :show-inheritance: diff --git a/docs/source/telegram.rst b/docs/source/telegram.rst index 1cb0289f0..39d8a6b13 100644 --- a/docs/source/telegram.rst +++ b/docs/source/telegram.rst @@ -7,12 +7,26 @@ telegram package telegram.audio telegram.bot telegram.botcommand + telegram.botcommandscope + telegram.botcommandscopedefault + telegram.botcommandscopeallprivatechats + telegram.botcommandscopeallgroupchats + telegram.botcommandscopeallchatadministrators + telegram.botcommandscopechat + telegram.botcommandscopechatadministrators + telegram.botcommandscopechatmember telegram.callbackquery telegram.chat telegram.chataction telegram.chatinvitelink telegram.chatlocation telegram.chatmember + telegram.chatmemberowner + telegram.chatmemberadministrator + telegram.chatmembermember + telegram.chatmemberrestricted + telegram.chatmemberleft + telegram.chatmemberbanned telegram.chatmemberupdated telegram.chatpermissions telegram.chatphoto diff --git a/examples/conversationbot.py b/examples/conversationbot.py index 8ee31cfab..4e5f62efb 100644 --- a/examples/conversationbot.py +++ b/examples/conversationbot.py @@ -44,7 +44,9 @@ def start(update: Update, context: CallbackContext) -> int: 'Hi! My name is Professor Bot. I will hold a conversation with you. ' 'Send /cancel to stop talking to me.\n\n' 'Are you a boy or a girl?', - reply_markup=ReplyKeyboardMarkup(reply_keyboard, one_time_keyboard=True), + reply_markup=ReplyKeyboardMarkup( + reply_keyboard, one_time_keyboard=True, input_field_placeholder='Boy or Girl?' + ), ) return GENDER diff --git a/telegram/__init__.py b/telegram/__init__.py index c71d3fd69..59179e8ae 100644 --- a/telegram/__init__.py +++ b/telegram/__init__.py @@ -25,7 +25,15 @@ from .files.chatphoto import ChatPhoto from .chat import Chat from .chatlocation import ChatLocation from .chatinvitelink import ChatInviteLink -from .chatmember import ChatMember +from .chatmember import ( + ChatMember, + ChatMemberOwner, + ChatMemberAdministrator, + ChatMemberMember, + ChatMemberRestricted, + ChatMemberLeft, + ChatMemberBanned, +) from .chatmemberupdated import ChatMemberUpdated from .chatpermissions import ChatPermissions from .files.photosize import PhotoSize @@ -153,6 +161,16 @@ from .passport.credentials import ( FileCredentials, TelegramDecryptionError, ) +from .botcommandscope import ( + BotCommandScope, + BotCommandScopeDefault, + BotCommandScopeAllPrivateChats, + BotCommandScopeAllGroupChats, + BotCommandScopeAllChatAdministrators, + BotCommandScopeChat, + BotCommandScopeChatAdministrators, + BotCommandScopeChatMember, +) from .bot import Bot from .version import __version__, bot_api_version # noqa: F401 @@ -163,6 +181,14 @@ __all__ = ( # Keep this alphabetically ordered 'Audio', 'Bot', 'BotCommand', + 'BotCommandScope', + 'BotCommandScopeAllChatAdministrators', + 'BotCommandScopeAllGroupChats', + 'BotCommandScopeAllPrivateChats', + 'BotCommandScopeChat', + 'BotCommandScopeChatAdministrators', + 'BotCommandScopeChatMember', + 'BotCommandScopeDefault', 'CallbackGame', 'CallbackQuery', 'Chat', @@ -170,6 +196,12 @@ __all__ = ( # Keep this alphabetically ordered 'ChatInviteLink', 'ChatLocation', 'ChatMember', + 'ChatMemberOwner', + 'ChatMemberAdministrator', + 'ChatMemberMember', + 'ChatMemberRestricted', + 'ChatMemberLeft', + 'ChatMemberBanned', 'ChatMemberUpdated', 'ChatPermissions', 'ChatPhoto', diff --git a/telegram/bot.py b/telegram/bot.py index db81a1df4..87eec560c 100644 --- a/telegram/bot.py +++ b/telegram/bot.py @@ -57,6 +57,7 @@ from telegram import ( Animation, Audio, BotCommand, + BotCommandScope, Chat, ChatMember, ChatPermissions, @@ -400,7 +401,20 @@ class Bot(TelegramObject): @property def commands(self) -> List[BotCommand]: - """List[:class:`BotCommand`]: Bot's commands.""" + """ + List[:class:`BotCommand`]: Bot's commands as available in the default scope. + + .. deprecated:: 13.7 + This property has been deprecated since there can be different commands available for + different scopes. + """ + warnings.warn( + "Bot.commands has been deprecated since there can be different command " + "lists for different scopes.", + TelegramDeprecationWarning, + stacklevel=2, + ) + if self._commands is None: self._commands = self.get_my_commands() return self._commands @@ -2312,11 +2326,43 @@ class Bot(TelegramObject): revoke_messages: bool = None, ) -> bool: """ - Use this method to kick a user from a group, supergroup or a channel. In the case of + Deprecated, use :func:`~telegram.Bot.ban_chat_member` instead. + + .. deprecated:: 13.7 + + """ + warnings.warn( + '`bot.kick_chat_member` is deprecated. Use `bot.ban_chat_member` instead.', + TelegramDeprecationWarning, + stacklevel=2, + ) + return self.ban_chat_member( + chat_id=chat_id, + user_id=user_id, + timeout=timeout, + until_date=until_date, + api_kwargs=api_kwargs, + revoke_messages=revoke_messages, + ) + + @log + def ban_chat_member( + self, + chat_id: Union[str, int], + user_id: Union[str, int], + timeout: ODVInput[float] = DEFAULT_NONE, + until_date: Union[int, datetime] = None, + api_kwargs: JSONDict = None, + revoke_messages: bool = None, + ) -> bool: + """ + Use this method to ban a user from a group, supergroup or a channel. In the case of supergroups and channels, the user will not be able to return to the group 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. + .. versionadded:: 13.7 + Args: chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target group or username of the target supergroup or channel (in the format ``@channelusername``). @@ -2358,7 +2404,7 @@ class Bot(TelegramObject): if revoke_messages is not None: data['revoke_messages'] = revoke_messages - result = self._post('kickChatMember', data, timeout=timeout, api_kwargs=api_kwargs) + result = self._post('banChatMember', data, timeout=timeout, api_kwargs=api_kwargs) return result # type: ignore[return-value] @@ -3061,9 +3107,31 @@ class Bot(TelegramObject): chat_id: Union[str, int], timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, + ) -> int: + """ + Deprecated, use :func:`~telegram.Bot.get_chat_member_count` instead. + + .. deprecated:: 13.7 + """ + warnings.warn( + '`bot.get_chat_members_count` is deprecated. ' + 'Use `bot.get_chat_member_count` instead.', + TelegramDeprecationWarning, + stacklevel=2, + ) + return self.get_chat_member_count(chat_id=chat_id, timeout=timeout, api_kwargs=api_kwargs) + + @log + def get_chat_member_count( + self, + chat_id: Union[str, int], + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, ) -> int: """Use this method to get the number of members in a chat. + .. versionadded:: 13.7 + Args: chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username of the target supergroup or channel (in the format ``@channelusername``). @@ -3082,7 +3150,7 @@ class Bot(TelegramObject): """ data: JSONDict = {'chat_id': chat_id} - result = self._post('getChatMembersCount', data, timeout=timeout, api_kwargs=api_kwargs) + result = self._post('getChatMemberCount', data, timeout=timeout, api_kwargs=api_kwargs) return result # type: ignore[return-value] @@ -4959,10 +5027,15 @@ class Bot(TelegramObject): @log def get_my_commands( - self, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None + self, + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + scope: BotCommandScope = None, + language_code: str = None, ) -> List[BotCommand]: """ - Use this method to get the current list of the bot's commands. + Use this method to get the current list of the bot's commands for the given scope and user + language. Args: timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as @@ -4970,19 +5043,39 @@ class Bot(TelegramObject): the connection pool). api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the Telegram API. + scope (:class:`telegram.BotCommandScope`, optional): A JSON-serialized object, + describing scope of users. Defaults to :class:`telegram.BotCommandScopeDefault`. + + .. versionadded:: 13.7 + + language_code (:obj:`str`, optional): A two-letter ISO 639-1 language code or an empty + string. + + .. versionadded:: 13.7 Returns: - List[:class:`telegram.BotCommand]`: On success, the commands set for the bot + List[:class:`telegram.BotCommand`]: On success, the commands set for the bot. An empty + list is returned if commands are not set. Raises: :class:`telegram.error.TelegramError` """ - result = self._post('getMyCommands', timeout=timeout, api_kwargs=api_kwargs) + data: JSONDict = {} - self._commands = BotCommand.de_list(result, self) # type: ignore[assignment,arg-type] + if scope: + data['scope'] = scope.to_dict() - return self._commands # type: ignore[return-value] + if language_code: + data['language_code'] = language_code + + result = self._post('getMyCommands', data, timeout=timeout, api_kwargs=api_kwargs) + + if (scope is None or scope.type == scope.DEFAULT) and language_code is None: + self._commands = BotCommand.de_list(result, self) # type: ignore[assignment,arg-type] + return self._commands # type: ignore[return-value] + + return BotCommand.de_list(result, self) # type: ignore[return-value,arg-type] @log def set_my_commands( @@ -4990,9 +5083,13 @@ class Bot(TelegramObject): commands: List[Union[BotCommand, Tuple[str, str]]], timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, + scope: BotCommandScope = None, + language_code: str = None, ) -> bool: """ - Use this method to change the list of the bot's commands. + Use this method to change the list of the bot's commands. See the + `Telegram docs `_ for more details about bot + commands. Args: commands (List[:class:`BotCommand` | (:obj:`str`, :obj:`str`)]): A JSON-serialized list @@ -5003,9 +5100,20 @@ class Bot(TelegramObject): the connection pool). api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the Telegram API. + scope (:class:`telegram.BotCommandScope`, optional): A JSON-serialized object, + describing scope of users for which the commands are relevant. Defaults to + :class:`telegram.BotCommandScopeDefault`. + + .. versionadded:: 13.7 + + language_code (:obj:`str`, optional): 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. + + .. versionadded:: 13.7 Returns: - :obj:`True`: On success + :obj:`bool`: On success, :obj:`True` is returned. Raises: :class:`telegram.error.TelegramError` @@ -5015,11 +5123,68 @@ class Bot(TelegramObject): data: JSONDict = {'commands': [c.to_dict() for c in cmds]} + if scope: + data['scope'] = scope.to_dict() + + if language_code: + data['language_code'] = language_code + result = self._post('setMyCommands', data, timeout=timeout, api_kwargs=api_kwargs) - # Set commands. No need to check for outcome. + # Set commands only for default scope. No need to check for outcome. # If request failed, we won't come this far - self._commands = cmds + if (scope is None or scope.type == scope.DEFAULT) and language_code is None: + self._commands = cmds + + return result # type: ignore[return-value] + + @log + def delete_my_commands( + self, + scope: BotCommandScope = None, + language_code: str = None, + api_kwargs: JSONDict = None, + timeout: ODVInput[float] = DEFAULT_NONE, + ) -> bool: + """ + 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. + + .. versionadded:: 13.7 + + Args: + scope (:class:`telegram.BotCommandScope`, optional): A JSON-serialized object, + describing scope of users for which the commands are relevant. Defaults to + :class:`telegram.BotCommandScopeDefault`. + language_code (:obj:`str`, optional): 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. + timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as + the read timeout from the server (instead of the one specified during creation of + the connection pool). + api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the + Telegram API. + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + Raises: + :class:`telegram.error.TelegramError` + """ + data: JSONDict = {} + + if scope: + data['scope'] = scope.to_dict() + + if language_code: + data['language_code'] = language_code + + result = self._post('deleteMyCommands', data, timeout=timeout, api_kwargs=api_kwargs) + + if (scope is None or scope.type == scope.DEFAULT) and language_code is None: + self._commands = [] return result # type: ignore[return-value] @@ -5210,6 +5375,8 @@ class Bot(TelegramObject): """Alias for :meth:`get_user_profile_photos`""" getFile = get_file """Alias for :meth:`get_file`""" + banChatMember = ban_chat_member + """Alias for :meth:`ban_chat_member`""" kickChatMember = kick_chat_member """Alias for :meth:`kick_chat_member`""" unbanChatMember = unban_chat_member @@ -5242,6 +5409,8 @@ class Bot(TelegramObject): """Alias for :meth:`set_chat_sticker_set`""" deleteChatStickerSet = delete_chat_sticker_set """Alias for :meth:`delete_chat_sticker_set`""" + getChatMemberCount = get_chat_member_count + """Alias for :meth:`get_chat_member_count`""" getChatMembersCount = get_chat_members_count """Alias for :meth:`get_chat_members_count`""" getWebhookInfo = get_webhook_info @@ -5312,6 +5481,8 @@ class Bot(TelegramObject): """Alias for :meth:`get_my_commands`""" setMyCommands = set_my_commands """Alias for :meth:`set_my_commands`""" + deleteMyCommands = delete_my_commands + """Alias for :meth:`delete_my_commands`""" logOut = log_out """Alias for :meth:`log_out`""" copyMessage = copy_message diff --git a/telegram/botcommandscope.py b/telegram/botcommandscope.py new file mode 100644 index 000000000..b4729290b --- /dev/null +++ b/telegram/botcommandscope.py @@ -0,0 +1,263 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2021 +# Leandro Toledo de Souza +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser Public License for more details. +# +# You should have received a copy of the GNU Lesser Public License +# along with this program. If not, see [http://www.gnu.org/licenses/]. +# pylint: disable=W0622 +"""This module contains objects representing Telegram bot command scopes.""" +from typing import Any, Union, Optional, TYPE_CHECKING, Dict, Type + +from telegram import TelegramObject, constants +from telegram.utils.types import JSONDict + +if TYPE_CHECKING: + from telegram import Bot + + +class BotCommandScope(TelegramObject): + """Base class for objects that represent the scope to which bot commands are applied. + Currently, the following 7 scopes are supported: + + * :class:`telegram.BotCommandScopeDefault` + * :class:`telegram.BotCommandScopeAllPrivateChats` + * :class:`telegram.BotCommandScopeAllGroupChats` + * :class:`telegram.BotCommandScopeAllChatAdministrators` + * :class:`telegram.BotCommandScopeChat` + * :class:`telegram.BotCommandScopeChatAdministrators` + * :class:`telegram.BotCommandScopeChatMember` + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`type` is equal. For subclasses with additional attributes, + the notion of equality is overridden. + + Note: + Please see the `official docs`_ on how Telegram determines which commands to display. + + .. _`official docs`: https://core.telegram.org/bots/api#determining-list-of-commands + + .. versionadded:: 13.7 + + Args: + type (:obj:`str`): Scope type. + + Attributes: + type (:obj:`str`): Scope type. + """ + + __slots__ = ('type', '_id_attrs') + + DEFAULT = constants.BOT_COMMAND_SCOPE_DEFAULT + """:const:`telegram.constants.BOT_COMMAND_SCOPE_DEFAULT`""" + ALL_PRIVATE_CHATS = constants.BOT_COMMAND_SCOPE_ALL_PRIVATE_CHATS + """:const:`telegram.constants.BOT_COMMAND_SCOPE_ALL_PRIVATE_CHATS`""" + ALL_GROUP_CHATS = constants.BOT_COMMAND_SCOPE_ALL_GROUP_CHATS + """:const:`telegram.constants.BOT_COMMAND_SCOPE_ALL_GROUP_CHATS`""" + ALL_CHAT_ADMINISTRATORS = constants.BOT_COMMAND_SCOPE_ALL_CHAT_ADMINISTRATORS + """:const:`telegram.constants.BOT_COMMAND_SCOPE_ALL_CHAT_ADMINISTRATORS`""" + CHAT = constants.BOT_COMMAND_SCOPE_CHAT + """:const:`telegram.constants.BOT_COMMAND_SCOPE_CHAT`""" + CHAT_ADMINISTRATORS = constants.BOT_COMMAND_SCOPE_CHAT_ADMINISTRATORS + """:const:`telegram.constants.BOT_COMMAND_SCOPE_CHAT_ADMINISTRATORS`""" + CHAT_MEMBER = constants.BOT_COMMAND_SCOPE_CHAT_MEMBER + """:const:`telegram.constants.BOT_COMMAND_SCOPE_CHAT_MEMBER`""" + + def __init__(self, type: str, **_kwargs: Any): + self.type = type + self._id_attrs = (self.type,) + + @classmethod + def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['BotCommandScope']: + """Converts JSON data to the appropriate :class:`BotCommandScope` object, i.e. takes + care of selecting the correct subclass. + + Args: + data (Dict[:obj:`str`, ...]): The JSON data. + bot (:class:`telegram.Bot`): The bot associated with this object. + + Returns: + The Telegram object. + + """ + data = cls._parse_data(data) + + if not data: + return None + + _class_mapping: Dict[str, Type['BotCommandScope']] = { + cls.DEFAULT: BotCommandScopeDefault, + cls.ALL_PRIVATE_CHATS: BotCommandScopeAllPrivateChats, + cls.ALL_GROUP_CHATS: BotCommandScopeAllGroupChats, + cls.ALL_CHAT_ADMINISTRATORS: BotCommandScopeAllChatAdministrators, + cls.CHAT: BotCommandScopeChat, + cls.CHAT_ADMINISTRATORS: BotCommandScopeChatAdministrators, + cls.CHAT_MEMBER: BotCommandScopeChatMember, + } + + if cls is BotCommandScope: + return _class_mapping.get(data['type'], cls)(**data, bot=bot) + return cls(**data) + + +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. + + .. _`narrower scope`: https://core.telegram.org/bots/api#determining-list-of-commands + + .. versionadded:: 13.7 + + Attributes: + type (:obj:`str`): Scope type :attr:`telegram.BotCommandScope.DEFAULT`. + """ + + __slots__ = () + + def __init__(self, **_kwargs: Any): + super().__init__(type=BotCommandScope.DEFAULT) + + +class BotCommandScopeAllPrivateChats(BotCommandScope): + """Represents the scope of bot commands, covering all private chats. + + .. versionadded:: 13.7 + + Attributes: + type (:obj:`str`): Scope type :attr:`telegram.BotCommandScope.ALL_PRIVATE_CHATS`. + """ + + __slots__ = () + + def __init__(self, **_kwargs: Any): + super().__init__(type=BotCommandScope.ALL_PRIVATE_CHATS) + + +class BotCommandScopeAllGroupChats(BotCommandScope): + """Represents the scope of bot commands, covering all group and supergroup chats. + + .. versionadded:: 13.7 + + Attributes: + type (:obj:`str`): Scope type :attr:`telegram.BotCommandScope.ALL_GROUP_CHATS`. + """ + + __slots__ = () + + def __init__(self, **_kwargs: Any): + super().__init__(type=BotCommandScope.ALL_GROUP_CHATS) + + +class BotCommandScopeAllChatAdministrators(BotCommandScope): + """Represents the scope of bot commands, covering all group and supergroup chat administrators. + + .. versionadded:: 13.7 + + Attributes: + type (:obj:`str`): Scope type :attr:`telegram.BotCommandScope.ALL_CHAT_ADMINISTRATORS`. + """ + + __slots__ = () + + def __init__(self, **_kwargs: Any): + super().__init__(type=BotCommandScope.ALL_CHAT_ADMINISTRATORS) + + +class BotCommandScopeChat(BotCommandScope): + """Represents the scope of bot commands, covering a specific chat. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`type` and :attr:`chat_id` are equal. + + .. versionadded:: 13.7 + + Args: + chat_id (:obj:`str` | :obj:`int`): Unique identifier for the target chat or username of the + target supergroup (in the format ``@supergroupusername``) + + Attributes: + type (:obj:`str`): Scope type :attr:`telegram.BotCommandScope.CHAT`. + chat_id (:obj:`str` | :obj:`int`): Unique identifier for the target chat or username of the + target supergroup (in the format ``@supergroupusername``) + """ + + __slots__ = ('chat_id',) + + def __init__(self, chat_id: Union[str, int], **_kwargs: Any): + super().__init__(type=BotCommandScope.CHAT) + self.chat_id = ( + chat_id if isinstance(chat_id, str) and chat_id.startswith('@') else int(chat_id) + ) + self._id_attrs = (self.type, self.chat_id) + + +class BotCommandScopeChatAdministrators(BotCommandScope): + """Represents the scope of bot commands, covering all administrators of a specific group or + supergroup chat. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`type` and :attr:`chat_id` are equal. + + .. versionadded:: 13.7 + + Args: + chat_id (:obj:`str` | :obj:`int`): Unique identifier for the target chat or username of the + target supergroup (in the format ``@supergroupusername``) + + Attributes: + type (:obj:`str`): Scope type :attr:`telegram.BotCommandScope.CHAT_ADMINISTRATORS`. + chat_id (:obj:`str` | :obj:`int`): Unique identifier for the target chat or username of the + target supergroup (in the format ``@supergroupusername``) + """ + + __slots__ = ('chat_id',) + + def __init__(self, chat_id: Union[str, int], **_kwargs: Any): + super().__init__(type=BotCommandScope.CHAT_ADMINISTRATORS) + self.chat_id = ( + chat_id if isinstance(chat_id, str) and chat_id.startswith('@') else int(chat_id) + ) + self._id_attrs = (self.type, self.chat_id) + + +class BotCommandScopeChatMember(BotCommandScope): + """Represents the scope of bot commands, covering a specific member of a group or supergroup + chat. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`type`, :attr:`chat_id` and :attr:`user_id` are equal. + + .. versionadded:: 13.7 + + Args: + chat_id (:obj:`str` | :obj:`int`): Unique identifier for the target chat or username of the + target supergroup (in the format ``@supergroupusername``) + user_id (:obj:`int`): Unique identifier of the target user. + + Attributes: + type (:obj:`str`): Scope type :attr:`telegram.BotCommandScope.CHAT_MEMBER`. + chat_id (:obj:`str` | :obj:`int`): Unique identifier for the target chat or username of the + target supergroup (in the format ``@supergroupusername``) + user_id (:obj:`int`): Unique identifier of the target user. + """ + + __slots__ = ('chat_id', 'user_id') + + def __init__(self, chat_id: Union[str, int], user_id: int, **_kwargs: Any): + super().__init__(type=BotCommandScope.CHAT_MEMBER) + self.chat_id = ( + chat_id if isinstance(chat_id, str) and chat_id.startswith('@') else int(chat_id) + ) + self.user_id = int(user_id) + self._id_attrs = (self.type, self.chat_id, self.user_id) diff --git a/telegram/chat.py b/telegram/chat.py index ae7e2717d..4b5b6c844 100644 --- a/telegram/chat.py +++ b/telegram/chat.py @@ -18,11 +18,13 @@ # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains an object that represents a Telegram Chat.""" +import warnings from datetime import datetime from typing import TYPE_CHECKING, List, Optional, ClassVar, Union, Tuple, Any from telegram import ChatPhoto, TelegramObject, constants from telegram.utils.types import JSONDict, FileInput, ODVInput, DVInput +from telegram.utils.deprecate import TelegramDeprecationWarning from .chatpermissions import ChatPermissions from .chatlocation import ChatLocation @@ -318,19 +320,37 @@ class Chat(TelegramObject): def get_members_count( self, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None + ) -> int: + """ + Deprecated, use :func:`~telegram.Chat.get_member_count` instead. + + .. deprecated:: 13.7 + """ + warnings.warn( + '`Chat.get_members_count` is deprecated. Use `Chat.get_member_count` instead.', + TelegramDeprecationWarning, + stacklevel=2, + ) + + return self.get_member_count( + timeout=timeout, + api_kwargs=api_kwargs, + ) + + def get_member_count( + self, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None ) -> int: """Shortcut for:: - bot.get_chat_members_count(update.effective_chat.id, *args, **kwargs) + bot.get_chat_member_count(update.effective_chat.id, *args, **kwargs) For the documentation of the arguments, please see - :meth:`telegram.Bot.get_chat_members_count`. + :meth:`telegram.Bot.get_chat_member_count`. Returns: :obj:`int` - """ - return self.bot.get_chat_members_count( + return self.bot.get_chat_member_count( chat_id=self.id, timeout=timeout, api_kwargs=api_kwargs, @@ -366,18 +386,45 @@ class Chat(TelegramObject): until_date: Union[int, datetime] = None, api_kwargs: JSONDict = None, revoke_messages: bool = None, + ) -> bool: + """ + Deprecated, use :func:`~telegram.Chat.ban_member` instead. + + .. deprecated:: 13.7 + """ + warnings.warn( + '`Chat.kick_member` is deprecated. Use `Chat.ban_member` instead.', + TelegramDeprecationWarning, + stacklevel=2, + ) + + return self.ban_member( + user_id=user_id, + timeout=timeout, + until_date=until_date, + api_kwargs=api_kwargs, + revoke_messages=revoke_messages, + ) + + def ban_member( + self, + user_id: Union[str, int], + timeout: ODVInput[float] = DEFAULT_NONE, + until_date: Union[int, datetime] = None, + api_kwargs: JSONDict = None, + revoke_messages: bool = None, ) -> bool: """Shortcut for:: - bot.kick_chat_member(update.effective_chat.id, *args, **kwargs) + bot.ban_chat_member(update.effective_chat.id, *args, **kwargs) For the documentation of the arguments, please see - :meth:`telegram.Bot.kick_chat_member`. + :meth:`telegram.Bot.ban_chat_member`. Returns: :obj:`bool`: On success, :obj:`True` is returned. """ - return self.bot.kick_chat_member( + return self.bot.ban_chat_member( chat_id=self.id, user_id=user_id, timeout=timeout, diff --git a/telegram/chatmember.py b/telegram/chatmember.py index 3246c4b91..254836bd0 100644 --- a/telegram/chatmember.py +++ b/telegram/chatmember.py @@ -18,7 +18,7 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains an object that represents a Telegram ChatMember.""" import datetime -from typing import TYPE_CHECKING, Any, Optional, ClassVar +from typing import TYPE_CHECKING, Any, Optional, ClassVar, Dict, Type from telegram import TelegramObject, User, constants from telegram.utils.helpers import from_timestamp, to_timestamp @@ -29,113 +29,239 @@ if TYPE_CHECKING: class ChatMember(TelegramObject): - """This object contains information about one member of a chat. + """Base class for Telegram ChatMember Objects. + Currently, the following 6 types of chat members are supported: + + * :class:`telegram.ChatMemberOwner` + * :class:`telegram.ChatMemberAdministrator` + * :class:`telegram.ChatMemberMember` + * :class:`telegram.ChatMemberRestricted` + * :class:`telegram.ChatMemberLeft` + * :class:`telegram.ChatMemberBanned` Objects of this class are comparable in terms of equality. Two objects of this class are considered equal, if their :attr:`user` and :attr:`status` are equal. + Note: + As of Bot API 5.3, :class:`ChatMember` is nothing but the base class for the subclasses + listed above and is no longer returned directly by :meth:`~telegram.Bot.get_chat`. + Therefore, most of the arguments and attributes were deprecated and you should no longer + use :class:`ChatMember` directly. + Args: user (:class:`telegram.User`): Information about the user. - status (:obj:`str`): The member's status in the chat. Can be 'creator', 'administrator', - 'member', 'restricted', 'left' or 'kicked'. + status (:obj:`str`): The member's status in the chat. Can be + :attr:`~telegram.ChatMember.ADMINISTRATOR`, :attr:`~telegram.ChatMember.CREATOR`, + :attr:`~telegram.ChatMember.KICKED`, :attr:`~telegram.ChatMember.LEFT`, + :attr:`~telegram.ChatMember.MEMBER` or :attr:`~telegram.ChatMember.RESTRICTED`. custom_title (:obj:`str`, optional): Owner and administrators only. Custom title for this user. + + .. deprecated:: 13.7 + is_anonymous (:obj:`bool`, optional): Owner and administrators only. :obj:`True`, if the user's presence in the chat is hidden. + + .. deprecated:: 13.7 + until_date (:class:`datetime.datetime`, optional): Restricted and kicked only. Date when restrictions will be lifted for this user. + + .. deprecated:: 13.7 + can_be_edited (:obj:`bool`, optional): Administrators only. :obj:`True`, if the bot is allowed to edit administrator privileges of that user. + + .. deprecated:: 13.7 + can_manage_chat (:obj:`bool`, optional): Administrators only. :obj:`True`, if the administrator can access the chat event log, chat statistics, message statistics in channels, see channel members, see anonymous administrators in supergroups and ignore slow mode. Implied by any other administrator privilege. .. versionadded:: 13.4 + .. deprecated:: 13.7 can_manage_voice_chats (:obj:`bool`, optional): Administrators only. :obj:`True`, if the administrator can manage voice chats. .. versionadded:: 13.4 + .. deprecated:: 13.7 can_change_info (:obj:`bool`, optional): Administrators and restricted only. :obj:`True`, if the user can change the chat title, photo and other settings. + + .. deprecated:: 13.7 + can_post_messages (:obj:`bool`, optional): Administrators only. :obj:`True`, if the administrator can post in the channel, channels only. + + .. deprecated:: 13.7 + can_edit_messages (:obj:`bool`, optional): Administrators only. :obj:`True`, if the administrator can edit messages of other users and can pin messages; channels only. + + .. deprecated:: 13.7 + can_delete_messages (:obj:`bool`, optional): Administrators only. :obj:`True`, if the administrator can delete messages of other users. + + .. deprecated:: 13.7 + can_invite_users (:obj:`bool`, optional): Administrators and restricted only. :obj:`True`, if the user can invite new users to the chat. + + .. deprecated:: 13.7 + can_restrict_members (:obj:`bool`, optional): Administrators only. :obj:`True`, if the administrator can restrict, ban or unban chat members. + + .. deprecated:: 13.7 + can_pin_messages (:obj:`bool`, optional): Administrators and restricted only. :obj:`True`, if the user can pin messages, groups and supergroups only. + + .. deprecated:: 13.7 + can_promote_members (:obj:`bool`, optional): Administrators only. :obj:`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 the user). + + .. deprecated:: 13.7 + is_member (:obj:`bool`, optional): Restricted only. :obj:`True`, if the user is a member of the chat at the moment of the request. + + .. deprecated:: 13.7 + can_send_messages (:obj:`bool`, optional): Restricted only. :obj:`True`, if the user can send text messages, contacts, locations and venues. + + .. deprecated:: 13.7 + can_send_media_messages (:obj:`bool`, optional): Restricted only. :obj:`True`, if the user can send audios, documents, photos, videos, video notes and voice notes. + + .. deprecated:: 13.7 + can_send_polls (:obj:`bool`, optional): Restricted only. :obj:`True`, if the user is allowed to send polls. + + .. deprecated:: 13.7 + can_send_other_messages (:obj:`bool`, optional): Restricted only. :obj:`True`, if the user can send animations, games, stickers and use inline bots. + + .. deprecated:: 13.7 + can_add_web_page_previews (:obj:`bool`, optional): Restricted only. :obj:`True`, if user may add web page previews to his messages. + .. deprecated:: 13.7 + Attributes: user (:class:`telegram.User`): Information about the user. status (:obj:`str`): The member's status in the chat. custom_title (:obj:`str`): Optional. Custom title for owner and administrators. + + .. deprecated:: 13.7 + is_anonymous (:obj:`bool`): Optional. :obj:`True`, if the user's presence in the chat is hidden. + + .. deprecated:: 13.7 + until_date (:class:`datetime.datetime`): Optional. Date when restrictions will be lifted for this user. + + .. deprecated:: 13.7 + can_be_edited (:obj:`bool`): Optional. If the bot is allowed to edit administrator privileges of that user. + + .. deprecated:: 13.7 + can_manage_chat (:obj:`bool`): Optional. If the administrator can access the chat event log, chat statistics, message statistics in channels, see channel members, see anonymous administrators in supergroups and ignore slow mode. .. versionadded:: 13.4 + .. deprecated:: 13.7 can_manage_voice_chats (:obj:`bool`): Optional. if the administrator can manage voice chats. .. versionadded:: 13.4 + .. deprecated:: 13.7 can_change_info (:obj:`bool`): Optional. If the user can change the chat title, photo and other settings. + + .. deprecated:: 13.7 + can_post_messages (:obj:`bool`): Optional. If the administrator can post in the channel. + + .. deprecated:: 13.7 + can_edit_messages (:obj:`bool`): Optional. If the administrator can edit messages of other users. + + .. deprecated:: 13.7 + can_delete_messages (:obj:`bool`): Optional. If the administrator can delete messages of other users. + + .. deprecated:: 13.7 + can_invite_users (:obj:`bool`): Optional. If the user can invite new users to the chat. + + .. deprecated:: 13.7 + can_restrict_members (:obj:`bool`): Optional. If the administrator can restrict, ban or unban chat members. + + .. deprecated:: 13.7 + can_pin_messages (:obj:`bool`): Optional. If the user can pin messages. + + .. deprecated:: 13.7 + can_promote_members (:obj:`bool`): Optional. If the administrator can add new administrators. + + .. deprecated:: 13.7 + is_member (:obj:`bool`): Optional. Restricted only. :obj:`True`, if the user is a member of the chat at the moment of the request. + + .. deprecated:: 13.7 + can_send_messages (:obj:`bool`): Optional. If the user can send text messages, contacts, locations and venues. + + .. deprecated:: 13.7 + can_send_media_messages (:obj:`bool`): Optional. If the user can send media messages, implies can_send_messages. + + .. deprecated:: 13.7 + can_send_polls (:obj:`bool`): Optional. :obj:`True`, if the user is allowed to send polls. + + .. deprecated:: 13.7 + can_send_other_messages (:obj:`bool`): Optional. If the user can send animations, games, stickers and use inline bots, implies can_send_media_messages. + + .. deprecated:: 13.7 + can_add_web_page_previews (:obj:`bool`): Optional. If user may add web page previews to his messages, implies can_send_media_messages + .. deprecated:: 13.7 + """ __slots__ = ( @@ -242,6 +368,17 @@ class ChatMember(TelegramObject): data['user'] = User.de_json(data.get('user'), bot) data['until_date'] = from_timestamp(data.get('until_date', None)) + _class_mapping: Dict[str, Type['ChatMember']] = { + cls.CREATOR: ChatMemberOwner, + cls.ADMINISTRATOR: ChatMemberAdministrator, + cls.MEMBER: ChatMemberMember, + cls.RESTRICTED: ChatMemberRestricted, + cls.LEFT: ChatMemberLeft, + cls.KICKED: ChatMemberBanned, + } + + if cls is ChatMember: + return _class_mapping.get(data['status'], cls)(**data, bot=bot) return cls(**data) def to_dict(self) -> JSONDict: @@ -251,3 +388,328 @@ class ChatMember(TelegramObject): data['until_date'] = to_timestamp(self.until_date) return data + + +class ChatMemberOwner(ChatMember): + """ + Represents a chat member that owns the chat + and has all administrator privileges. + + .. versionadded:: 13.7 + + Args: + user (:class:`telegram.User`): Information about the user. + custom_title (:obj:`str`, optional): Custom title for this user. + is_anonymous (:obj:`bool`, optional): :obj:`True`, if the + user's presence in the chat is hidden. + + Attributes: + status (:obj:`str`): The member's status in the chat, + always :attr:`telegram.ChatMember.CREATOR`. + user (:class:`telegram.User`): Information about the user. + custom_title (:obj:`str`): Optional. Custom title for + this user. + is_anonymous (:obj:`bool`): Optional. :obj:`True`, if the user's + presence in the chat is hidden. + """ + + __slots__ = () + + def __init__( + self, + user: User, + custom_title: str = None, + is_anonymous: bool = None, + **_kwargs: Any, + ): + super().__init__( + status=ChatMember.CREATOR, + user=user, + custom_title=custom_title, + is_anonymous=is_anonymous, + ) + + +class ChatMemberAdministrator(ChatMember): + """ + Represents a chat member that has some additional privileges. + + .. versionadded:: 13.7 + + Args: + user (:class:`telegram.User`): Information about the user. + can_be_edited (:obj:`bool`, optional): :obj:`True`, if the bot + is allowed to edit administrator privileges of that user. + custom_title (:obj:`str`, optional): Custom title for this user. + is_anonymous (:obj:`bool`, optional): :obj:`True`, if the user's + presence in the chat is hidden. + can_manage_chat (:obj:`bool`, optional): :obj:`True`, if the administrator + can access the chat event log, chat statistics, message statistics in + channels, see channel members, see anonymous administrators in supergroups + and ignore slow mode. Implied by any other administrator privilege. + can_post_messages (:obj:`bool`, optional): :obj:`True`, if the + administrator can post in the channel, channels only. + can_edit_messages (:obj:`bool`, optional): :obj:`True`, if the + administrator can edit messages of other users and can pin + messages; channels only. + can_delete_messages (:obj:`bool`, optional): :obj:`True`, if the + administrator can delete messages of other users. + can_manage_voice_chats (:obj:`bool`, optional): :obj:`True`, if the + administrator can manage voice chats. + can_restrict_members (:obj:`bool`, optional): :obj:`True`, if the + administrator can restrict, ban or unban chat members. + can_promote_members (:obj:`bool`, optional): :obj:`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 the user). + can_change_info (:obj:`bool`, optional): :obj:`True`, if the user can change + the chat title, photo and other settings. + can_invite_users (:obj:`bool`, optional): :obj:`True`, if the user can invite + new users to the chat. + can_pin_messages (:obj:`bool`, optional): :obj:`True`, if the user is allowed + to pin messages; groups and supergroups only. + + Attributes: + status (:obj:`str`): The member's status in the chat, + always :attr:`telegram.ChatMember.ADMINISTRATOR`. + user (:class:`telegram.User`): Information about the user. + can_be_edited (:obj:`bool`): Optional. :obj:`True`, if the bot + is allowed to edit administrator privileges of that user. + custom_title (:obj:`str`): Optional. Custom title for this user. + is_anonymous (:obj:`bool`): Optional. :obj:`True`, if the user's + presence in the chat is hidden. + can_manage_chat (:obj:`bool`): Optional. :obj:`True`, if the administrator + can access the chat event log, chat statistics, message statistics in + channels, see channel members, see anonymous administrators in supergroups + and ignore slow mode. Implied by any other administrator privilege. + can_post_messages (:obj:`bool`): Optional. :obj:`True`, if the + administrator can post in the channel, channels only. + can_edit_messages (:obj:`bool`): Optional. :obj:`True`, if the + administrator can edit messages of other users and can pin + messages; channels only. + can_delete_messages (:obj:`bool`): Optional. :obj:`True`, if the + administrator can delete messages of other users. + can_manage_voice_chats (:obj:`bool`): Optional. :obj:`True`, if the + administrator can manage voice chats. + can_restrict_members (:obj:`bool`): Optional. :obj:`True`, if the + administrator can restrict, ban or unban chat members. + can_promote_members (:obj:`bool`): Optional. :obj:`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 the user). + can_change_info (:obj:`bool`): Optional. :obj:`True`, if the user can change + the chat title, photo and other settings. + can_invite_users (:obj:`bool`): Optional. :obj:`True`, if the user can invite + new users to the chat. + can_pin_messages (:obj:`bool`): Optional. :obj:`True`, if the user is allowed + to pin messages; groups and supergroups only. + """ + + __slots__ = () + + def __init__( + self, + user: User, + can_be_edited: bool = None, + custom_title: str = None, + is_anonymous: bool = None, + can_manage_chat: bool = None, + can_post_messages: bool = None, + can_edit_messages: bool = None, + can_delete_messages: bool = None, + can_manage_voice_chats: bool = None, + can_restrict_members: bool = None, + can_promote_members: bool = None, + can_change_info: bool = None, + can_invite_users: bool = None, + can_pin_messages: bool = None, + **_kwargs: Any, + ): + super().__init__( + status=ChatMember.ADMINISTRATOR, + user=user, + can_be_edited=can_be_edited, + custom_title=custom_title, + is_anonymous=is_anonymous, + can_manage_chat=can_manage_chat, + can_post_messages=can_post_messages, + can_edit_messages=can_edit_messages, + can_delete_messages=can_delete_messages, + can_manage_voice_chats=can_manage_voice_chats, + can_restrict_members=can_restrict_members, + can_promote_members=can_promote_members, + can_change_info=can_change_info, + can_invite_users=can_invite_users, + can_pin_messages=can_pin_messages, + ) + + +class ChatMemberMember(ChatMember): + """ + Represents a chat member that has no additional + privileges or restrictions. + + .. versionadded:: 13.7 + + Args: + user (:class:`telegram.User`): Information about the user. + + Attributes: + status (:obj:`str`): The member's status in the chat, + always :attr:`telegram.ChatMember.MEMBER`. + user (:class:`telegram.User`): Information about the user. + + """ + + __slots__ = () + + def __init__(self, user: User, **_kwargs: Any): + super().__init__(status=ChatMember.MEMBER, user=user) + + +class ChatMemberRestricted(ChatMember): + """ + Represents a chat member that is under certain restrictions + in the chat. Supergroups only. + + .. versionadded:: 13.7 + + Args: + user (:class:`telegram.User`): Information about the user. + is_member (:obj:`bool`, optional): :obj:`True`, if the user is a + member of the chat at the moment of the request. + can_change_info (:obj:`bool`, optional): :obj:`True`, if the user can change + the chat title, photo and other settings. + can_invite_users (:obj:`bool`, optional): :obj:`True`, if the user can invite + new users to the chat. + can_pin_messages (:obj:`bool`, optional): :obj:`True`, if the user is allowed + to pin messages; groups and supergroups only. + can_send_messages (:obj:`bool`, optional): :obj:`True`, if the user is allowed + to send text messages, contacts, locations and venues. + can_send_media_messages (:obj:`bool`, optional): :obj:`True`, if the user is allowed + to send audios, documents, photos, videos, video notes and voice notes. + can_send_polls (:obj:`bool`, optional): :obj:`True`, if the user is allowed + to send polls. + can_send_other_messages (:obj:`bool`, optional): :obj:`True`, if the user is allowed + to send animations, games, stickers and use inline bots. + can_add_web_page_previews (:obj:`bool`, optional): :obj:`True`, if the user is + allowed to add web page previews to their messages. + until_date (:class:`datetime.datetime`, optional): Date when restrictions + will be lifted for this user. + + Attributes: + status (:obj:`str`): The member's status in the chat, + always :attr:`telegram.ChatMember.RESTRICTED`. + user (:class:`telegram.User`): Information about the user. + is_member (:obj:`bool`): Optional. :obj:`True`, if the user is a + member of the chat at the moment of the request. + can_change_info (:obj:`bool`): Optional. :obj:`True`, if the user can change + the chat title, photo and other settings. + can_invite_users (:obj:`bool`): Optional. :obj:`True`, if the user can invite + new users to the chat. + can_pin_messages (:obj:`bool`): Optional. :obj:`True`, if the user is allowed + to pin messages; groups and supergroups only. + can_send_messages (:obj:`bool`): Optional. :obj:`True`, if the user is allowed + to send text messages, contacts, locations and venues. + can_send_media_messages (:obj:`bool`): Optional. :obj:`True`, if the user is allowed + to send audios, documents, photos, videos, video notes and voice notes. + can_send_polls (:obj:`bool`): Optional. :obj:`True`, if the user is allowed + to send polls. + can_send_other_messages (:obj:`bool`): Optional. :obj:`True`, if the user is allowed + to send animations, games, stickers and use inline bots. + can_add_web_page_previews (:obj:`bool`): Optional. :obj:`True`, if the user is + allowed to add web page previews to their messages. + until_date (:class:`datetime.datetime`): Optional. Date when restrictions + will be lifted for this user. + + """ + + __slots__ = () + + def __init__( + self, + user: User, + is_member: bool = None, + can_change_info: bool = None, + can_invite_users: bool = None, + can_pin_messages: bool = None, + can_send_messages: bool = None, + can_send_media_messages: bool = None, + can_send_polls: bool = None, + can_send_other_messages: bool = None, + can_add_web_page_previews: bool = None, + until_date: datetime.datetime = None, + **_kwargs: Any, + ): + super().__init__( + status=ChatMember.RESTRICTED, + user=user, + is_member=is_member, + can_change_info=can_change_info, + can_invite_users=can_invite_users, + can_pin_messages=can_pin_messages, + can_send_messages=can_send_messages, + can_send_media_messages=can_send_media_messages, + can_send_polls=can_send_polls, + can_send_other_messages=can_send_other_messages, + can_add_web_page_previews=can_add_web_page_previews, + until_date=until_date, + ) + + +class ChatMemberLeft(ChatMember): + """ + Represents a chat member that isn't currently a member of the chat, + but may join it themselves. + + .. versionadded:: 13.7 + + Args: + user (:class:`telegram.User`): Information about the user. + + Attributes: + status (:obj:`str`): The member's status in the chat, + always :attr:`telegram.ChatMember.LEFT`. + user (:class:`telegram.User`): Information about the user. + """ + + __slots__ = () + + def __init__(self, user: User, **_kwargs: Any): + super().__init__(status=ChatMember.LEFT, user=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. + + .. versionadded:: 13.7 + + Args: + user (:class:`telegram.User`): Information about the user. + until_date (:class:`datetime.datetime`, optional): Date when restrictions + will be lifted for this user. + + Attributes: + status (:obj:`str`): The member's status in the chat, + always :attr:`telegram.ChatMember.KICKED`. + user (:class:`telegram.User`): Information about the user. + until_date (:class:`datetime.datetime`): Optional. Date when restrictions + will be lifted for this user. + + """ + + __slots__ = () + + def __init__( + self, + user: User, + until_date: datetime.datetime = None, + **_kwargs: Any, + ): + super().__init__( + status=ChatMember.KICKED, + user=user, + until_date=until_date, + ) diff --git a/telegram/constants.py b/telegram/constants.py index 02cb2d25f..795f37203 100644 --- a/telegram/constants.py +++ b/telegram/constants.py @@ -21,7 +21,7 @@ The following constants were extracted from the `Telegram Bots API `_. Attributes: - BOT_API_VERSION (:obj:`str`): `5.2`. Telegram Bot API version supported by this + BOT_API_VERSION (:obj:`str`): `5.3`. Telegram Bot API version supported by this version of `python-telegram-bot`. Also available as ``telegram.bot_api_version``. .. versionadded:: 13.4 @@ -205,10 +205,35 @@ Attributes: .. versionadded:: 13.5 +:class:`telegram.BotCommandScope`: + +Attributes: + BOT_COMMAND_SCOPE_DEFAULT (:obj:`str`): ``'default'`` + + ..versionadded:: 13.7 + BOT_COMMAND_SCOPE_ALL_PRIVATE_CHATS (:obj:`str`): ``'all_private_chats'`` + + ..versionadded:: 13.7 + BOT_COMMAND_SCOPE_ALL_GROUP_CHATS (:obj:`str`): ``'all_group_chats'`` + + ..versionadded:: 13.7 + BOT_COMMAND_SCOPE_ALL_CHAT_ADMINISTRATORS (:obj:`str`): ``'all_chat_administrators'`` + + ..versionadded:: 13.7 + BOT_COMMAND_SCOPE_CHAT (:obj:`str`): ``'chat'`` + + ..versionadded:: 13.7 + BOT_COMMAND_SCOPE_CHAT_ADMINISTRATORS (:obj:`str`): ``'chat_administrators'`` + + ..versionadded:: 13.7 + BOT_COMMAND_SCOPE_CHAT_MEMBER (:obj:`str`): ``'chat_member'`` + + ..versionadded:: 13.7 + """ from typing import List -BOT_API_VERSION: str = '5.2' +BOT_API_VERSION: str = '5.3' MAX_MESSAGE_LENGTH: int = 4096 MAX_CAPTION_LENGTH: int = 1024 ANONYMOUS_ADMIN_ID: int = 1087968824 @@ -343,3 +368,11 @@ UPDATE_ALL_TYPES = [ UPDATE_MY_CHAT_MEMBER, UPDATE_CHAT_MEMBER, ] + +BOT_COMMAND_SCOPE_DEFAULT = 'default' +BOT_COMMAND_SCOPE_ALL_PRIVATE_CHATS = 'all_private_chats' +BOT_COMMAND_SCOPE_ALL_GROUP_CHATS = 'all_group_chats' +BOT_COMMAND_SCOPE_ALL_CHAT_ADMINISTRATORS = 'all_chat_administrators' +BOT_COMMAND_SCOPE_CHAT = 'chat' +BOT_COMMAND_SCOPE_CHAT_ADMINISTRATORS = 'chat_administrators' +BOT_COMMAND_SCOPE_CHAT_MEMBER = 'chat_member' diff --git a/telegram/ext/__init__.py b/telegram/ext/__init__.py index b4b4cc59a..731ad2c9e 100644 --- a/telegram/ext/__init__.py +++ b/telegram/ext/__init__.py @@ -32,7 +32,7 @@ from .dispatcher import Dispatcher, DispatcherHandlerStop, run_async # try-except is just here in case the __init__ is called twice (like in the tests) # this block is also the reason for the pylint-ignore at the top of the file try: - del Dispatcher.__slots__ # type: ignore[has-type] + del Dispatcher.__slots__ except AttributeError as exc: if str(exc) == '__slots__': pass diff --git a/telegram/forcereply.py b/telegram/forcereply.py index aaf7c733a..baa978281 100644 --- a/telegram/forcereply.py +++ b/telegram/forcereply.py @@ -37,25 +37,42 @@ class ForceReply(ReplyMarkup): selective (:obj:`bool`, optional): Use this parameter if you want to force reply from specific users only. Targets: - 1) Users that are @mentioned in the text of the Message object. - 2) If the bot's message is a reply (has reply_to_message_id), sender of the + 1) Users that are @mentioned in the :attr:`~telegram.Message.text` of the + :class:`telegram.Message` object. + 2) If the bot's message is a reply (has ``reply_to_message_id``), sender of the original message. + input_field_placeholder (:obj:`str`, optional): The placeholder to be shown in the input + field when the reply is active; 1-64 characters. + + .. versionadded:: 13.7 + **kwargs (:obj:`dict`): Arbitrary keyword arguments. Attributes: force_reply (:obj:`True`): Shows reply interface to the user, as if they manually selected the bots message and tapped 'Reply'. selective (:obj:`bool`): Optional. Force reply from specific users only. + input_field_placeholder (:obj:`str`): Optional. The placeholder shown in the input + field when the reply is active. + + .. versionadded:: 13.7 """ - __slots__ = ('selective', 'force_reply', '_id_attrs') + __slots__ = ('selective', 'force_reply', 'input_field_placeholder', '_id_attrs') - def __init__(self, force_reply: bool = True, selective: bool = False, **_kwargs: Any): + def __init__( + self, + force_reply: bool = True, + selective: bool = False, + input_field_placeholder: str = None, + **_kwargs: Any, + ): # Required self.force_reply = bool(force_reply) # Optionals self.selective = bool(selective) + self.input_field_placeholder = input_field_placeholder self._id_attrs = (self.selective,) diff --git a/telegram/replykeyboardmarkup.py b/telegram/replykeyboardmarkup.py index 490ce338c..1f365e6ab 100644 --- a/telegram/replykeyboardmarkup.py +++ b/telegram/replykeyboardmarkup.py @@ -48,12 +48,18 @@ class ReplyKeyboardMarkup(ReplyMarkup): selective (:obj:`bool`, optional): Use this parameter if you want to show the keyboard to specific users only. Targets: - 1) Users that are @mentioned in the text of the Message object. + 1) Users that are @mentioned in the :attr:`~telegram.Message.text` of the + :class:`telegram.Message` object. 2) If the bot's message is a reply (has ``reply_to_message_id``), sender of the original message. Defaults to :obj:`False`. + input_field_placeholder (:obj:`str`, optional): The placeholder to be shown in the input + field when the keyboard is active; 1-64 characters. + + .. versionadded:: 13.7 + **kwargs (:obj:`dict`): Arbitrary keyword arguments. Attributes: @@ -62,10 +68,21 @@ class ReplyKeyboardMarkup(ReplyMarkup): one_time_keyboard (:obj:`bool`): Optional. Requests clients to hide the keyboard as soon as it's been used. selective (:obj:`bool`): Optional. Show the keyboard to specific users only. + input_field_placeholder (:obj:`str`): Optional. The placeholder shown in the input + field when the reply is active. + + .. versionadded:: 13.7 """ - __slots__ = ('selective', 'keyboard', 'resize_keyboard', 'one_time_keyboard', '_id_attrs') + __slots__ = ( + 'selective', + 'keyboard', + 'resize_keyboard', + 'one_time_keyboard', + 'input_field_placeholder', + '_id_attrs', + ) def __init__( self, @@ -73,6 +90,7 @@ class ReplyKeyboardMarkup(ReplyMarkup): resize_keyboard: bool = False, one_time_keyboard: bool = False, selective: bool = False, + input_field_placeholder: str = None, **_kwargs: Any, ): # Required @@ -90,6 +108,7 @@ class ReplyKeyboardMarkup(ReplyMarkup): self.resize_keyboard = bool(resize_keyboard) self.one_time_keyboard = bool(one_time_keyboard) self.selective = bool(selective) + self.input_field_placeholder = input_field_placeholder self._id_attrs = (self.keyboard,) @@ -109,6 +128,7 @@ class ReplyKeyboardMarkup(ReplyMarkup): resize_keyboard: bool = False, one_time_keyboard: bool = False, selective: bool = False, + input_field_placeholder: str = None, **kwargs: object, ) -> 'ReplyKeyboardMarkup': """Shortcut for:: @@ -133,10 +153,15 @@ class ReplyKeyboardMarkup(ReplyMarkup): to specific users only. Targets: 1) Users that are @mentioned in the text of the Message object. - 2) If the bot's message is a reply (has reply_to_message_id), sender of the - original message. + 2) If the bot's message is a reply (has ``reply_to_message_id``), sender of the + original message. Defaults to :obj:`False`. + + input_field_placeholder (:obj:`str`): Optional. The placeholder shown in the input + field when the reply is active. + + .. versionadded:: 13.7 **kwargs (:obj:`dict`): Arbitrary keyword arguments. """ return cls( @@ -144,6 +169,7 @@ class ReplyKeyboardMarkup(ReplyMarkup): resize_keyboard=resize_keyboard, one_time_keyboard=one_time_keyboard, selective=selective, + input_field_placeholder=input_field_placeholder, **kwargs, ) @@ -154,6 +180,7 @@ class ReplyKeyboardMarkup(ReplyMarkup): resize_keyboard: bool = False, one_time_keyboard: bool = False, selective: bool = False, + input_field_placeholder: str = None, **kwargs: object, ) -> 'ReplyKeyboardMarkup': """Shortcut for:: @@ -178,10 +205,15 @@ class ReplyKeyboardMarkup(ReplyMarkup): to specific users only. Targets: 1) Users that are @mentioned in the text of the Message object. - 2) If the bot's message is a reply (has reply_to_message_id), sender of the - original message. + 2) If the bot's message is a reply (has ``reply_to_message_id``), sender of the + original message. Defaults to :obj:`False`. + + input_field_placeholder (:obj:`str`): Optional. The placeholder shown in the input + field when the reply is active. + + .. versionadded:: 13.7 **kwargs (:obj:`dict`): Arbitrary keyword arguments. """ @@ -190,6 +222,7 @@ class ReplyKeyboardMarkup(ReplyMarkup): resize_keyboard=resize_keyboard, one_time_keyboard=one_time_keyboard, selective=selective, + input_field_placeholder=input_field_placeholder, **kwargs, ) @@ -200,6 +233,7 @@ class ReplyKeyboardMarkup(ReplyMarkup): resize_keyboard: bool = False, one_time_keyboard: bool = False, selective: bool = False, + input_field_placeholder: str = None, **kwargs: object, ) -> 'ReplyKeyboardMarkup': """Shortcut for:: @@ -224,10 +258,15 @@ class ReplyKeyboardMarkup(ReplyMarkup): to specific users only. Targets: 1) Users that are @mentioned in the text of the Message object. - 2) If the bot's message is a reply (has reply_to_message_id), sender of the - original message. + 2) If the bot's message is a reply (has ``reply_to_message_id``), sender of the + original message. Defaults to :obj:`False`. + + input_field_placeholder (:obj:`str`): Optional. The placeholder shown in the input + field when the reply is active. + + .. versionadded:: 13.7 **kwargs (:obj:`dict`): Arbitrary keyword arguments. """ @@ -237,6 +276,7 @@ class ReplyKeyboardMarkup(ReplyMarkup): resize_keyboard=resize_keyboard, one_time_keyboard=one_time_keyboard, selective=selective, + input_field_placeholder=input_field_placeholder, **kwargs, ) diff --git a/tests/test_bot.py b/tests/test_bot.py index 782e80c75..d2a6dadff 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -51,6 +51,7 @@ from telegram import ( Chat, InlineQueryResultVoice, PollOption, + BotCommandScopeChat, ) from telegram.constants import MAX_INLINE_QUERY_RESULTS from telegram.ext import ExtBot, Defaults @@ -969,7 +970,7 @@ class TestBot: monkeypatch.delattr(bot, '_post') # TODO: Needs improvement. No feasible way to test until bots can add members. - def test_kick_chat_member(self, monkeypatch, bot): + def test_ban_chat_member(self, monkeypatch, bot): def test(url, data, *args, **kwargs): chat_id = data['chat_id'] == 2 user_id = data['user_id'] == 32 @@ -980,13 +981,13 @@ class TestBot: monkeypatch.setattr(bot.request, 'post', test) until = from_timestamp(1577887200) - assert bot.kick_chat_member(2, 32) - assert bot.kick_chat_member(2, 32, until_date=until) - assert bot.kick_chat_member(2, 32, until_date=1577887200) - assert bot.kick_chat_member(2, 32, revoke_messages=True) + assert bot.ban_chat_member(2, 32) + assert bot.ban_chat_member(2, 32, until_date=until) + assert bot.ban_chat_member(2, 32, until_date=1577887200) + assert bot.ban_chat_member(2, 32, revoke_messages=True) monkeypatch.delattr(bot.request, 'post') - def test_kick_chat_member_default_tz(self, monkeypatch, tz_bot): + def test_ban_chat_member_default_tz(self, monkeypatch, tz_bot): until = dtm.datetime(2020, 1, 11, 16, 13) until_timestamp = to_timestamp(until, tzinfo=tz_bot.defaults.tzinfo) @@ -998,9 +999,21 @@ class TestBot: monkeypatch.setattr(tz_bot.request, 'post', test) - assert tz_bot.kick_chat_member(2, 32) - assert tz_bot.kick_chat_member(2, 32, until_date=until) - assert tz_bot.kick_chat_member(2, 32, until_date=until_timestamp) + assert tz_bot.ban_chat_member(2, 32) + assert tz_bot.ban_chat_member(2, 32, until_date=until) + assert tz_bot.ban_chat_member(2, 32, until_date=until_timestamp) + + def test_kick_chat_member_warning(self, monkeypatch, bot, recwarn): + def test(url, data, *args, **kwargs): + chat_id = data['chat_id'] == 2 + user_id = data['user_id'] == 32 + return chat_id and user_id + + monkeypatch.setattr(bot.request, 'post', test) + bot.kick_chat_member(2, 32) + assert len(recwarn) == 1 + assert '`bot.kick_chat_member` is deprecated' in str(recwarn[0].message) + monkeypatch.delattr(bot.request, 'post') # TODO: Needs improvement. @pytest.mark.parametrize('only_if_banned', [True, False, None]) @@ -1338,11 +1351,21 @@ class TestBot: assert a.status in ('administrator', 'creator') @flaky(3, 1) - def test_get_chat_members_count(self, bot, channel_id): - count = bot.get_chat_members_count(channel_id) + def test_get_chat_member_count(self, bot, channel_id): + count = bot.get_chat_member_count(channel_id) assert isinstance(count, int) assert count > 3 + def test_get_chat_members_count_warning(self, bot, channel_id, recwarn): + bot.get_chat_members_count(channel_id) + assert len(recwarn) == 1 + assert '`bot.get_chat_members_count` is deprecated' in str(recwarn[0].message) + + def test_bot_command_property_warning(self, bot, recwarn): + _ = bot.commands + assert len(recwarn) == 1 + assert 'Bot.commands has been deprecated since there can' in str(recwarn[0].message) + @flaky(3, 1) def test_get_chat_member(self, bot, channel_id, chat_id): chat_member = bot.get_chat_member(channel_id, chat_id) @@ -1943,6 +1966,44 @@ class TestBot: assert bc[1].command == 'cmd2' assert bc[1].description == 'descr2' + @flaky(3, 1) + def test_get_set_delete_my_commands_with_scope(self, bot, super_group_id, chat_id): + group_cmds = [BotCommand('group_cmd', 'visible to this supergroup only')] + private_cmds = [BotCommand('private_cmd', 'visible to this private chat only')] + group_scope = BotCommandScopeChat(super_group_id) + private_scope = BotCommandScopeChat(chat_id) + + # Set supergroup command list with lang code and check if the same can be returned from api + bot.set_my_commands(group_cmds, scope=group_scope, language_code='en') + gotten_group_cmds = bot.get_my_commands(scope=group_scope, language_code='en') + + assert len(gotten_group_cmds) == len(group_cmds) + assert gotten_group_cmds[0].command == group_cmds[0].command + + # Set private command list and check if same can be returned from the api + bot.set_my_commands(private_cmds, scope=private_scope) + gotten_private_cmd = bot.get_my_commands(scope=private_scope) + + assert len(gotten_private_cmd) == len(private_cmds) + assert gotten_private_cmd[0].command == private_cmds[0].command + + assert len(bot.commands) == 2 # set from previous test. Makes sure this hasn't changed. + assert bot.commands[0].command == 'cmd1' + + # Delete command list from that supergroup and private chat- + bot.delete_my_commands(private_scope) + bot.delete_my_commands(group_scope, 'en') + + # Check if its been deleted- + deleted_priv_cmds = bot.get_my_commands(scope=private_scope) + deleted_grp_cmds = bot.get_my_commands(scope=group_scope, language_code='en') + + assert len(deleted_grp_cmds) == 0 == len(group_cmds) - 1 + assert len(deleted_priv_cmds) == 0 == len(private_cmds) - 1 + + bot.delete_my_commands() # Delete commands from default scope + assert not bot.commands # Check if this has been updated to reflect the deletion. + def test_log_out(self, monkeypatch, bot): # We don't actually make a request as to not break the test setup def assertion(url, data, *args, **kwargs): diff --git a/tests/test_botcommandscope.py b/tests/test_botcommandscope.py new file mode 100644 index 000000000..25e5d5877 --- /dev/null +++ b/tests/test_botcommandscope.py @@ -0,0 +1,205 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2021 +# Leandro Toledo de Souza +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser Public License for more details. +# +# You should have received a copy of the GNU Lesser Public License +# along with this program. If not, see [http://www.gnu.org/licenses/]. +from copy import deepcopy + +import pytest + +from telegram import ( + Dice, + BotCommandScope, + BotCommandScopeDefault, + BotCommandScopeAllPrivateChats, + BotCommandScopeAllGroupChats, + BotCommandScopeAllChatAdministrators, + BotCommandScopeChat, + BotCommandScopeChatAdministrators, + BotCommandScopeChatMember, +) + + +@pytest.fixture(scope="class", params=['str', 'int']) +def chat_id(request): + if request.param == 'str': + return '@supergroupusername' + return 43 + + +@pytest.fixture( + scope="class", + params=[ + BotCommandScope.DEFAULT, + BotCommandScope.ALL_PRIVATE_CHATS, + BotCommandScope.ALL_GROUP_CHATS, + BotCommandScope.ALL_CHAT_ADMINISTRATORS, + BotCommandScope.CHAT, + BotCommandScope.CHAT_ADMINISTRATORS, + BotCommandScope.CHAT_MEMBER, + ], +) +def scope_type(request): + return request.param + + +@pytest.fixture( + scope="class", + params=[ + BotCommandScopeDefault, + BotCommandScopeAllPrivateChats, + BotCommandScopeAllGroupChats, + BotCommandScopeAllChatAdministrators, + BotCommandScopeChat, + BotCommandScopeChatAdministrators, + BotCommandScopeChatMember, + ], + ids=[ + BotCommandScope.DEFAULT, + BotCommandScope.ALL_PRIVATE_CHATS, + BotCommandScope.ALL_GROUP_CHATS, + BotCommandScope.ALL_CHAT_ADMINISTRATORS, + BotCommandScope.CHAT, + BotCommandScope.CHAT_ADMINISTRATORS, + BotCommandScope.CHAT_MEMBER, + ], +) +def scope_class(request): + return request.param + + +@pytest.fixture( + scope="class", + params=[ + (BotCommandScopeDefault, BotCommandScope.DEFAULT), + (BotCommandScopeAllPrivateChats, BotCommandScope.ALL_PRIVATE_CHATS), + (BotCommandScopeAllGroupChats, BotCommandScope.ALL_GROUP_CHATS), + (BotCommandScopeAllChatAdministrators, BotCommandScope.ALL_CHAT_ADMINISTRATORS), + (BotCommandScopeChat, BotCommandScope.CHAT), + (BotCommandScopeChatAdministrators, BotCommandScope.CHAT_ADMINISTRATORS), + (BotCommandScopeChatMember, BotCommandScope.CHAT_MEMBER), + ], + ids=[ + BotCommandScope.DEFAULT, + BotCommandScope.ALL_PRIVATE_CHATS, + BotCommandScope.ALL_GROUP_CHATS, + BotCommandScope.ALL_CHAT_ADMINISTRATORS, + BotCommandScope.CHAT, + BotCommandScope.CHAT_ADMINISTRATORS, + BotCommandScope.CHAT_MEMBER, + ], +) +def scope_class_and_type(request): + return request.param + + +@pytest.fixture(scope='class') +def bot_command_scope(scope_class_and_type, chat_id): + return scope_class_and_type[0](type=scope_class_and_type[1], chat_id=chat_id, user_id=42) + + +# All the scope types are very similar, so we test everything via parametrization +class TestBotCommandScope: + def test_slot_behaviour(self, bot_command_scope, mro_slots, recwarn): + for attr in bot_command_scope.__slots__: + assert getattr(bot_command_scope, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert not bot_command_scope.__dict__, f"got missing slot(s): {bot_command_scope.__dict__}" + assert len(mro_slots(bot_command_scope)) == len( + set(mro_slots(bot_command_scope)) + ), "duplicate slot" + bot_command_scope.custom, bot_command_scope.type = 'warning!', bot_command_scope.type + assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list + + def test_de_json(self, bot, scope_class_and_type, chat_id): + cls = scope_class_and_type[0] + type_ = scope_class_and_type[1] + + assert cls.de_json({}, bot) is None + + json_dict = {'type': type_, 'chat_id': chat_id, 'user_id': 42} + bot_command_scope = BotCommandScope.de_json(json_dict, bot) + + assert isinstance(bot_command_scope, BotCommandScope) + assert isinstance(bot_command_scope, cls) + assert bot_command_scope.type == type_ + if 'chat_id' in cls.__slots__: + assert bot_command_scope.chat_id == chat_id + if 'user_id' in cls.__slots__: + assert bot_command_scope.user_id == 42 + + def test_de_json_invalid_type(self, bot): + json_dict = {'type': 'invalid', 'chat_id': chat_id, 'user_id': 42} + bot_command_scope = BotCommandScope.de_json(json_dict, bot) + + assert type(bot_command_scope) is BotCommandScope + assert bot_command_scope.type == 'invalid' + + def test_de_json_subclass(self, scope_class, bot, chat_id): + """This makes sure that e.g. BotCommandScopeDefault(data) never returns a + BotCommandScopeChat instance.""" + json_dict = {'type': 'invalid', 'chat_id': chat_id, 'user_id': 42} + assert type(scope_class.de_json(json_dict, bot)) is scope_class + + def test_to_dict(self, bot_command_scope): + bot_command_scope_dict = bot_command_scope.to_dict() + + assert isinstance(bot_command_scope_dict, dict) + assert bot_command_scope['type'] == bot_command_scope.type + if hasattr(bot_command_scope, 'chat_id'): + assert bot_command_scope['chat_id'] == bot_command_scope.chat_id + if hasattr(bot_command_scope, 'user_id'): + assert bot_command_scope['user_id'] == bot_command_scope.user_id + + def test_equality(self, bot_command_scope, bot): + a = BotCommandScope('base_type') + b = BotCommandScope('base_type') + c = bot_command_scope + d = deepcopy(bot_command_scope) + e = Dice(4, 'emoji') + + assert a == b + assert hash(a) == hash(b) + + assert a != c + assert hash(a) != hash(c) + + assert a != d + assert hash(a) != hash(d) + + assert a != e + assert hash(a) != hash(e) + + assert c == d + assert hash(c) == hash(d) + + assert c != e + assert hash(c) != hash(e) + + if hasattr(c, 'chat_id'): + json_dict = c.to_dict() + json_dict['chat_id'] = 0 + f = c.__class__.de_json(json_dict, bot) + + assert c != f + assert hash(c) != hash(f) + + if hasattr(c, 'user_id'): + json_dict = c.to_dict() + json_dict['user_id'] = 0 + g = c.__class__.de_json(json_dict, bot) + + assert c != g + assert hash(c) != hash(g) diff --git a/tests/test_chat.py b/tests/test_chat.py index 0f87251ca..a60956c48 100644 --- a/tests/test_chat.py +++ b/tests/test_chat.py @@ -176,18 +176,27 @@ class TestChat: monkeypatch.setattr(chat.bot, 'get_chat_administrators', make_assertion) assert chat.get_administrators() - def test_get_members_count(self, monkeypatch, chat): + def test_get_member_count(self, monkeypatch, chat): def make_assertion(*_, **kwargs): return kwargs['chat_id'] == chat.id assert check_shortcut_signature( - Chat.get_members_count, Bot.get_chat_members_count, ['chat_id'], [] + Chat.get_member_count, Bot.get_chat_member_count, ['chat_id'], [] ) - assert check_shortcut_call(chat.get_members_count, chat.bot, 'get_chat_members_count') - assert check_defaults_handling(chat.get_members_count, chat.bot) + assert check_shortcut_call(chat.get_member_count, chat.bot, 'get_chat_member_count') + assert check_defaults_handling(chat.get_member_count, chat.bot) - monkeypatch.setattr(chat.bot, 'get_chat_members_count', make_assertion) + monkeypatch.setattr(chat.bot, 'get_chat_member_count', make_assertion) + assert chat.get_member_count() + + def test_get_members_count_warning(self, chat, monkeypatch, recwarn): + def make_assertion(*_, **kwargs): + return kwargs['chat_id'] == chat.id + + monkeypatch.setattr(chat.bot, 'get_chat_member_count', make_assertion) assert chat.get_members_count() + assert len(recwarn) == 1 + assert '`Chat.get_members_count` is deprecated' in str(recwarn[0].message) def test_get_member(self, monkeypatch, chat): def make_assertion(*_, **kwargs): @@ -202,19 +211,31 @@ class TestChat: monkeypatch.setattr(chat.bot, 'get_chat_member', make_assertion) assert chat.get_member(user_id=42) - def test_kick_member(self, monkeypatch, chat): + def test_ban_member(self, monkeypatch, chat): def make_assertion(*_, **kwargs): chat_id = kwargs['chat_id'] == chat.id user_id = kwargs['user_id'] == 42 until = kwargs['until_date'] == 43 return chat_id and user_id and until - assert check_shortcut_signature(Chat.kick_member, Bot.kick_chat_member, ['chat_id'], []) - assert check_shortcut_call(chat.kick_member, chat.bot, 'kick_chat_member') - assert check_defaults_handling(chat.kick_member, chat.bot) + assert check_shortcut_signature(Chat.ban_member, Bot.ban_chat_member, ['chat_id'], []) + assert check_shortcut_call(chat.ban_member, chat.bot, 'ban_chat_member') + assert check_defaults_handling(chat.ban_member, chat.bot) - monkeypatch.setattr(chat.bot, 'kick_chat_member', make_assertion) + monkeypatch.setattr(chat.bot, 'ban_chat_member', make_assertion) + assert chat.ban_member(user_id=42, until_date=43) + + def test_kick_member_warning(self, chat, monkeypatch, recwarn): + def make_assertion(*_, **kwargs): + chat_id = kwargs['chat_id'] == chat.id + user_id = kwargs['user_id'] == 42 + until = kwargs['until_date'] == 43 + return chat_id and user_id and until + + monkeypatch.setattr(chat.bot, 'ban_chat_member', make_assertion) assert chat.kick_member(user_id=42, until_date=43) + assert len(recwarn) == 1 + assert '`Chat.kick_member` is deprecated' in str(recwarn[0].message) @pytest.mark.parametrize('only_if_banned', [True, False, None]) def test_unban_member(self, monkeypatch, chat, only_if_banned): diff --git a/tests/test_chatmember.py b/tests/test_chatmember.py index 967eb3df1..ce4f0757c 100644 --- a/tests/test_chatmember.py +++ b/tests/test_chatmember.py @@ -17,11 +17,22 @@ # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. import datetime +from copy import deepcopy import pytest -from telegram import User, ChatMember from telegram.utils.helpers import to_timestamp +from telegram import ( + User, + ChatMember, + ChatMemberOwner, + ChatMemberAdministrator, + ChatMemberMember, + ChatMemberRestricted, + ChatMemberLeft, + ChatMemberBanned, + Dice, +) @pytest.fixture(scope='class') @@ -29,38 +40,68 @@ def user(): return User(1, 'First name', False) +@pytest.fixture( + scope="class", + params=[ + (ChatMemberOwner, ChatMember.CREATOR), + (ChatMemberAdministrator, ChatMember.ADMINISTRATOR), + (ChatMemberMember, ChatMember.MEMBER), + (ChatMemberRestricted, ChatMember.RESTRICTED), + (ChatMemberLeft, ChatMember.LEFT), + (ChatMemberBanned, ChatMember.KICKED), + ], + ids=[ + ChatMember.CREATOR, + ChatMember.ADMINISTRATOR, + ChatMember.MEMBER, + ChatMember.RESTRICTED, + ChatMember.LEFT, + ChatMember.KICKED, + ], +) +def chat_member_class_and_status(request): + return request.param + + @pytest.fixture(scope='class') -def chat_member(user): - return ChatMember(user, TestChatMember.status) +def chat_member_types(chat_member_class_and_status, user): + return chat_member_class_and_status[0](status=chat_member_class_and_status[1], user=user) class TestChatMember: - status = ChatMember.CREATOR - - def test_slot_behaviour(self, chat_member, recwarn, mro_slots): - for attr in chat_member.__slots__: - assert getattr(chat_member, attr, 'err') != 'err', f"got extra slot '{attr}'" - assert not chat_member.__dict__, f"got missing slot(s): {chat_member.__dict__}" - assert len(mro_slots(chat_member)) == len(set(mro_slots(chat_member))), "duplicate slot" - chat_member.custom, chat_member.status = 'should give warning', self.status + def test_slot_behaviour(self, chat_member_types, mro_slots, recwarn): + for attr in chat_member_types.__slots__: + assert getattr(chat_member_types, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert not chat_member_types.__dict__, f"got missing slot(s): {chat_member_types.__dict__}" + assert len(mro_slots(chat_member_types)) == len( + set(mro_slots(chat_member_types)) + ), "duplicate slot" + chat_member_types.custom, chat_member_types.status = 'warning!', chat_member_types.status assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list - def test_de_json_required_args(self, bot, user): - json_dict = {'user': user.to_dict(), 'status': self.status} + def test_de_json_required_args(self, bot, chat_member_class_and_status, user): + cls = chat_member_class_and_status[0] + status = chat_member_class_and_status[1] - chat_member = ChatMember.de_json(json_dict, bot) + assert cls.de_json({}, bot) is None - assert chat_member.user == user - assert chat_member.status == self.status + json_dict = {'status': status, 'user': user.to_dict()} + chat_member_type = ChatMember.de_json(json_dict, bot) - def test_de_json_all_args(self, bot, user): + assert isinstance(chat_member_type, ChatMember) + assert isinstance(chat_member_type, cls) + assert chat_member_type.status == status + assert chat_member_type.user == user + + def test_de_json_all_args(self, bot, chat_member_class_and_status, user): + cls = chat_member_class_and_status[0] + status = chat_member_class_and_status[1] time = datetime.datetime.utcnow() - custom_title = 'custom_title' json_dict = { 'user': user.to_dict(), - 'status': self.status, - 'custom_title': custom_title, + 'status': status, + 'custom_title': 'PTB', 'is_anonymous': True, 'until_date': to_timestamp(time), 'can_be_edited': False, @@ -80,48 +121,134 @@ class TestChatMember: 'can_manage_chat': True, 'can_manage_voice_chats': True, } + chat_member_type = ChatMember.de_json(json_dict, bot) - chat_member = ChatMember.de_json(json_dict, bot) + assert isinstance(chat_member_type, ChatMember) + assert isinstance(chat_member_type, cls) + assert chat_member_type.user == user + assert chat_member_type.status == status + if chat_member_type.custom_title is not None: + assert chat_member_type.custom_title == 'PTB' + assert type(chat_member_type) in {ChatMemberOwner, ChatMemberAdministrator} + if chat_member_type.is_anonymous is not None: + assert chat_member_type.is_anonymous is True + assert type(chat_member_type) in {ChatMemberOwner, ChatMemberAdministrator} + if chat_member_type.until_date is not None: + assert type(chat_member_type) in {ChatMemberBanned, ChatMemberRestricted} + if chat_member_type.can_be_edited is not None: + assert chat_member_type.can_be_edited is False + assert type(chat_member_type) == ChatMemberAdministrator + if chat_member_type.can_change_info is not None: + assert chat_member_type.can_change_info is True + assert type(chat_member_type) in {ChatMemberAdministrator, ChatMemberRestricted} + if chat_member_type.can_post_messages is not None: + assert chat_member_type.can_post_messages is False + assert type(chat_member_type) == ChatMemberAdministrator + if chat_member_type.can_edit_messages is not None: + assert chat_member_type.can_edit_messages is True + assert type(chat_member_type) == ChatMemberAdministrator + if chat_member_type.can_delete_messages is not None: + assert chat_member_type.can_delete_messages is True + assert type(chat_member_type) == ChatMemberAdministrator + if chat_member_type.can_invite_users is not None: + assert chat_member_type.can_invite_users is False + assert type(chat_member_type) in {ChatMemberAdministrator, ChatMemberRestricted} + if chat_member_type.can_restrict_members is not None: + assert chat_member_type.can_restrict_members is True + assert type(chat_member_type) == ChatMemberAdministrator + if chat_member_type.can_pin_messages is not None: + assert chat_member_type.can_pin_messages is False + assert type(chat_member_type) in {ChatMemberAdministrator, ChatMemberRestricted} + if chat_member_type.can_promote_members is not None: + assert chat_member_type.can_promote_members is True + assert type(chat_member_type) == ChatMemberAdministrator + if chat_member_type.can_send_messages is not None: + assert chat_member_type.can_send_messages is False + assert type(chat_member_type) == ChatMemberRestricted + if chat_member_type.can_send_media_messages is not None: + assert chat_member_type.can_send_media_messages is True + assert type(chat_member_type) == ChatMemberRestricted + if chat_member_type.can_send_polls is not None: + assert chat_member_type.can_send_polls is False + assert type(chat_member_type) == ChatMemberRestricted + if chat_member_type.can_send_other_messages is not None: + assert chat_member_type.can_send_other_messages is True + assert type(chat_member_type) == ChatMemberRestricted + if chat_member_type.can_add_web_page_previews is not None: + assert chat_member_type.can_add_web_page_previews is False + assert type(chat_member_type) == ChatMemberRestricted + if chat_member_type.can_manage_chat is not None: + assert chat_member_type.can_manage_chat is True + assert type(chat_member_type) == ChatMemberAdministrator + if chat_member_type.can_manage_voice_chats is not None: + assert chat_member_type.can_manage_voice_chats is True + assert type(chat_member_type) == ChatMemberAdministrator - assert chat_member.user == user - assert chat_member.status == self.status - assert chat_member.custom_title == custom_title - assert chat_member.is_anonymous is True - assert chat_member.can_be_edited is False - assert chat_member.can_change_info is True - assert chat_member.can_post_messages is False - assert chat_member.can_edit_messages is True - assert chat_member.can_delete_messages is True - assert chat_member.can_invite_users is False - assert chat_member.can_restrict_members is True - assert chat_member.can_pin_messages is False - assert chat_member.can_promote_members is True - assert chat_member.can_send_messages is False - assert chat_member.can_send_media_messages is True - assert chat_member.can_send_polls is False - assert chat_member.can_send_other_messages is True - assert chat_member.can_add_web_page_previews is False - assert chat_member.can_manage_chat is True - assert chat_member.can_manage_voice_chats is True + def test_de_json_invalid_status(self, bot, user): + json_dict = {'status': 'invalid', 'user': user.to_dict()} + chat_member_type = ChatMember.de_json(json_dict, bot) + + assert type(chat_member_type) is ChatMember + assert chat_member_type.status == 'invalid' + + def test_de_json_subclass(self, chat_member_class_and_status, bot, chat_id, user): + """This makes sure that e.g. ChatMemberAdministrator(data, bot) never returns a + ChatMemberKicked instance.""" + cls = chat_member_class_and_status[0] + time = datetime.datetime.utcnow() + json_dict = { + 'user': user.to_dict(), + 'status': 'status', + 'custom_title': 'PTB', + 'is_anonymous': True, + 'until_date': to_timestamp(time), + 'can_be_edited': False, + 'can_change_info': True, + 'can_post_messages': False, + 'can_edit_messages': True, + 'can_delete_messages': True, + 'can_invite_users': False, + 'can_restrict_members': True, + 'can_pin_messages': False, + 'can_promote_members': True, + 'can_send_messages': False, + 'can_send_media_messages': True, + 'can_send_polls': False, + 'can_send_other_messages': True, + 'can_add_web_page_previews': False, + 'can_manage_chat': True, + 'can_manage_voice_chats': True, + } + assert type(cls.de_json(json_dict, bot)) is cls + + def test_to_dict(self, chat_member_types, user): + chat_member_dict = chat_member_types.to_dict() - def test_to_dict(self, chat_member): - chat_member_dict = chat_member.to_dict() assert isinstance(chat_member_dict, dict) - assert chat_member_dict['user'] == chat_member.user.to_dict() - assert chat_member['status'] == chat_member.status + assert chat_member_dict['status'] == chat_member_types.status + assert chat_member_dict['user'] == user.to_dict() - def test_equality(self): - a = ChatMember(User(1, '', False), ChatMember.ADMINISTRATOR) - b = ChatMember(User(1, '', False), ChatMember.ADMINISTRATOR) - d = ChatMember(User(2, '', False), ChatMember.ADMINISTRATOR) - d2 = ChatMember(User(1, '', False), ChatMember.CREATOR) + def test_equality(self, chat_member_types, user): + a = ChatMember(status='status', user=user) + b = ChatMember(status='status', user=user) + c = chat_member_types + d = deepcopy(chat_member_types) + e = Dice(4, 'emoji') assert a == b assert hash(a) == hash(b) - assert a is not b + + assert a != c + assert hash(a) != hash(c) assert a != d assert hash(a) != hash(d) - assert a != d2 - assert hash(a) != hash(d2) + assert a != e + assert hash(a) != hash(e) + + assert c == d + assert hash(c) == hash(d) + + assert c != e + assert hash(c) != hash(e) diff --git a/tests/test_forcereply.py b/tests/test_forcereply.py index 80b2d5590..f5f09b26d 100644 --- a/tests/test_forcereply.py +++ b/tests/test_forcereply.py @@ -25,12 +25,17 @@ from telegram import ForceReply, ReplyKeyboardRemove @pytest.fixture(scope='class') def force_reply(): - return ForceReply(TestForceReply.force_reply, TestForceReply.selective) + return ForceReply( + TestForceReply.force_reply, + TestForceReply.selective, + TestForceReply.input_field_placeholder, + ) class TestForceReply: force_reply = True selective = True + input_field_placeholder = 'force replies can be annoying if not used properly' def test_slot_behaviour(self, force_reply, recwarn, mro_slots): for attr in force_reply.__slots__: @@ -49,6 +54,7 @@ class TestForceReply: def test_expected(self, force_reply): assert force_reply.force_reply == self.force_reply assert force_reply.selective == self.selective + assert force_reply.input_field_placeholder == self.input_field_placeholder def test_to_dict(self, force_reply): force_reply_dict = force_reply.to_dict() @@ -56,6 +62,7 @@ class TestForceReply: assert isinstance(force_reply_dict, dict) assert force_reply_dict['force_reply'] == force_reply.force_reply assert force_reply_dict['selective'] == force_reply.selective + assert force_reply_dict['input_field_placeholder'] == force_reply.input_field_placeholder def test_equality(self): a = ForceReply(True, False) diff --git a/tests/test_official.py b/tests/test_official.py index 33ca6b67c..f522ee266 100644 --- a/tests/test_official.py +++ b/tests/test_official.py @@ -118,9 +118,13 @@ def check_object(h4): if field == 'from': field = 'from_user' elif ( - name.startswith('InlineQueryResult') or name.startswith('InputMedia') + name.startswith('InlineQueryResult') + or name.startswith('InputMedia') + or name.startswith('BotCommandScope') ) and field == 'type': continue + elif (name.startswith('ChatMember')) and field == 'status': + continue elif ( name.startswith('PassportElementError') and field == 'source' ) or field == 'remove_keyboard': @@ -136,7 +140,34 @@ def check_object(h4): if name == 'InputFile': return if name == 'InlineQueryResult': - ignored |= {'id', 'type'} + ignored |= {'id', 'type'} # attributes common to all subclasses + if name == 'ChatMember': + ignored |= {'user', 'status'} # attributes common to all subclasses + if name == 'ChatMember': + ignored |= { + 'can_add_web_page_previews', # for backwards compatibility + 'can_be_edited', + 'can_change_info', + 'can_delete_messages', + 'can_edit_messages', + 'can_invite_users', + 'can_manage_chat', + 'can_manage_voice_chats', + 'can_pin_messages', + 'can_post_messages', + 'can_promote_members', + 'can_restrict_members', + 'can_send_media_messages', + 'can_send_messages', + 'can_send_other_messages', + 'can_send_polls', + 'custom_title', + 'is_anonymous', + 'is_member', + 'until_date', + } + if name == 'BotCommandScope': + ignored |= {'type'} # attributes common to all subclasses elif name == 'User': ignored |= {'type'} # TODO: Deprecation elif name in ('PassportFile', 'EncryptedPassportElement'): diff --git a/tests/test_replykeyboardmarkup.py b/tests/test_replykeyboardmarkup.py index c5a94ac9b..67587a49b 100644 --- a/tests/test_replykeyboardmarkup.py +++ b/tests/test_replykeyboardmarkup.py @@ -29,6 +29,7 @@ def reply_keyboard_markup(): resize_keyboard=TestReplyKeyboardMarkup.resize_keyboard, one_time_keyboard=TestReplyKeyboardMarkup.one_time_keyboard, selective=TestReplyKeyboardMarkup.selective, + input_field_placeholder=TestReplyKeyboardMarkup.input_field_placeholder, ) @@ -37,6 +38,7 @@ class TestReplyKeyboardMarkup: resize_keyboard = True one_time_keyboard = True selective = True + input_field_placeholder = 'lol a keyboard' def test_slot_behaviour(self, reply_keyboard_markup, mro_slots, recwarn): inst = reply_keyboard_markup @@ -101,6 +103,7 @@ class TestReplyKeyboardMarkup: assert reply_keyboard_markup.resize_keyboard == self.resize_keyboard assert reply_keyboard_markup.one_time_keyboard == self.one_time_keyboard assert reply_keyboard_markup.selective == self.selective + assert reply_keyboard_markup.input_field_placeholder == self.input_field_placeholder def test_to_dict(self, reply_keyboard_markup): reply_keyboard_markup_dict = reply_keyboard_markup.to_dict() @@ -122,6 +125,10 @@ class TestReplyKeyboardMarkup: == reply_keyboard_markup.one_time_keyboard ) assert reply_keyboard_markup_dict['selective'] == reply_keyboard_markup.selective + assert ( + reply_keyboard_markup_dict['input_field_placeholder'] + == reply_keyboard_markup.input_field_placeholder + ) def test_equality(self): a = ReplyKeyboardMarkup.from_column(['button1', 'button2', 'button3'])