From ac02bce10952ec5c10131086baef08769a19e492 Mon Sep 17 00:00:00 2001 From: Poolitzer <25934244+Poolitzer@users.noreply.github.com> Date: Sun, 14 Mar 2021 16:41:35 +0100 Subject: [PATCH] API 5.1 (#2424) * Feat: New invite links * Fix: doc strings Co-authored-by: Bibo-Joshi * new dice, new admin privilege, revoke_messages, update and fix some docs * add missing param to shortcut * Add ChatMemberUpdated * Add voicechat related objects Signed-off-by: starry69 * add versionadd tags Signed-off-by: starry69 * Fix filter tests * Update tg.Update * ChatMemberHandler * Add versioning directives * add can_manage_voice_chats attr and fix docs Signed-off-by: starry69 * fix chat shortcut Signed-off-by: starry69 * address review * MADTC * Chat.message_auto_delete_time * Some doc fixes * address review Signed-off-by: starry69 * welp Signed-off-by: starry69 * Add voicechat related filters Signed-off-by: starry69 * Fix: Addressing review change place of version adding, added obj:True as doc string, changing how member limit is initiated * feat: adding chat shortcuts for invite links * fix: changing equality of chatinviteobjects * Non-test comments * Some test fixes * A bit more tests * Bump API version in both readmes * Increase coverage * Add Bot API Version in telegram.constants (#2429) * add bot api version in constants Signed-off-by: starry69 * addressing review Signed-off-by: starry69 * add versioning directive Co-authored-by: Bibo-Joshi * pre-commit & coverage Co-authored-by: Bibo-Joshi Co-authored-by: Harshil Co-authored-by: starry69 --- .github/pull_request_template.md | 2 +- README.rst | 4 +- README_RAW.rst | 4 +- docs/source/telegram.chatinvitelink.rst | 6 + docs/source/telegram.chatmemberupdated.rst | 6 + .../source/telegram.ext.chatmemberhandler.rst | 6 + docs/source/telegram.ext.rst | 1 + ...telegram.messageautodeletetimerchanged.rst | 6 + docs/source/telegram.rst | 6 + docs/source/telegram.voicechatended.rst | 7 + .../telegram.voicechatparticipantsinvited.rst | 7 + docs/source/telegram.voicechatstarted.rst | 7 + telegram/__init__.py | 12 +- telegram/__main__.py | 2 + telegram/base.py | 6 +- telegram/bot.py | 267 +++++++++++++++--- telegram/callbackquery.py | 2 +- telegram/chat.py | 135 ++++++++- telegram/chatinvitelink.py | 102 +++++++ telegram/chatmember.py | 27 ++ telegram/chatmemberupdated.py | 115 ++++++++ telegram/choseninlineresult.py | 2 +- telegram/constants.py | 13 + telegram/dice.py | 14 +- telegram/ext/__init__.py | 2 + telegram/ext/chatmemberhandler.py | 146 ++++++++++ telegram/ext/filters.py | 63 +++++ telegram/inline/inlinequery.py | 2 +- telegram/inline/inlinequeryresult.py | 4 + telegram/message.py | 59 +++- telegram/messageautodeletetimerchanged.py | 53 ++++ telegram/payment/precheckoutquery.py | 2 +- telegram/payment/shippingquery.py | 2 +- telegram/update.py | 45 +++ telegram/version.py | 3 + telegram/voicechat.py | 111 ++++++++ tests/test_bot.py | 113 +++++++- tests/test_chat.py | 61 ++++ tests/test_chatinvitelink.py | 119 ++++++++ tests/test_chatmember.py | 4 + tests/test_chatmemberhandler.py | 221 +++++++++++++++ tests/test_chatmemberupdated.py | 177 ++++++++++++ tests/test_filters.py | 29 +- tests/test_message.py | 16 ++ tests/test_messageautodeletetimerchanged.py | 51 ++++ tests/test_update.py | 17 ++ tests/test_voicechat.py | 114 ++++++++ 47 files changed, 2109 insertions(+), 64 deletions(-) create mode 100644 docs/source/telegram.chatinvitelink.rst create mode 100644 docs/source/telegram.chatmemberupdated.rst create mode 100644 docs/source/telegram.ext.chatmemberhandler.rst create mode 100644 docs/source/telegram.messageautodeletetimerchanged.rst create mode 100644 docs/source/telegram.voicechatended.rst create mode 100644 docs/source/telegram.voicechatparticipantsinvited.rst create mode 100644 docs/source/telegram.voicechatstarted.rst create mode 100644 telegram/chatinvitelink.py create mode 100644 telegram/chatmemberupdated.py create mode 100644 telegram/ext/chatmemberhandler.py create mode 100644 telegram/messageautodeletetimerchanged.py create mode 100644 telegram/voicechat.py create mode 100644 tests/test_chatinvitelink.py create mode 100644 tests/test_chatmemberhandler.py create mode 100644 tests/test_chatmemberupdated.py create mode 100644 tests/test_messageautodeletetimerchanged.py create mode 100644 tests/test_voicechat.py diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index efe496def..bd33a19cb 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -27,4 +27,4 @@ Hey! You're PRing? Cool! Please have a look at the below checklist. It's here to - [ ] Added new handlers for new update types - [ ] Added new filters for new message (sub)types - [ ] Added or updated documentation for the changed class(es) and/or method(s) - - [ ] Updated the Bot API version number in all places in `README.rst` and `README_RAW.rst`, including the badge + - [ ] Updated the Bot API version number in all places: `README.rst` and `README_RAW.rst` (including the badge), as well as `telegram.constants.BOT_API_VERSION` diff --git a/README.rst b/README.rst index 54545e923..8338df67d 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.0-blue?logo=telegram +.. image:: https://img.shields.io/badge/Bot%20API-5.1-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.0** are supported. +All types and methods of the Telegram Bot API **5.1** are supported. ========== Installing diff --git a/README_RAW.rst b/README_RAW.rst index 6c8b61111..9fd11b173 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.0-blue?logo=telegram +.. image:: https://img.shields.io/badge/Bot%20API-5.1-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.0** are supported. +All types and methods of the Telegram Bot API **5.1** are supported. ========== Installing diff --git a/docs/source/telegram.chatinvitelink.rst b/docs/source/telegram.chatinvitelink.rst new file mode 100644 index 000000000..ca24e5340 --- /dev/null +++ b/docs/source/telegram.chatinvitelink.rst @@ -0,0 +1,6 @@ +telegram.ChatInviteLink +======================= + +.. autoclass:: telegram.ChatInviteLink + :members: + :show-inheritance: diff --git a/docs/source/telegram.chatmemberupdated.rst b/docs/source/telegram.chatmemberupdated.rst new file mode 100644 index 000000000..d6feecc44 --- /dev/null +++ b/docs/source/telegram.chatmemberupdated.rst @@ -0,0 +1,6 @@ +telegram.ChatMemberUpdated +========================== + +.. autoclass:: telegram.ChatMemberUpdated + :members: + :show-inheritance: diff --git a/docs/source/telegram.ext.chatmemberhandler.rst b/docs/source/telegram.ext.chatmemberhandler.rst new file mode 100644 index 000000000..59675b954 --- /dev/null +++ b/docs/source/telegram.ext.chatmemberhandler.rst @@ -0,0 +1,6 @@ +telegram.ext.ChatMemberHandler +============================== + +.. autoclass:: telegram.ext.ChatMemberHandler + :members: + :show-inheritance: diff --git a/docs/source/telegram.ext.rst b/docs/source/telegram.ext.rst index 8bd50adcb..9bd855a87 100644 --- a/docs/source/telegram.ext.rst +++ b/docs/source/telegram.ext.rst @@ -21,6 +21,7 @@ Handlers telegram.ext.handler telegram.ext.callbackqueryhandler telegram.ext.choseninlineresulthandler + telegram.ext.chatmemberhandler telegram.ext.commandhandler telegram.ext.conversationhandler telegram.ext.inlinequeryhandler diff --git a/docs/source/telegram.messageautodeletetimerchanged.rst b/docs/source/telegram.messageautodeletetimerchanged.rst new file mode 100644 index 000000000..ae1fd5472 --- /dev/null +++ b/docs/source/telegram.messageautodeletetimerchanged.rst @@ -0,0 +1,6 @@ +telegram.MessageAutoDeleteTimerChanged +====================================== + +.. autoclass:: telegram.MessageAutoDeleteTimerChanged + :members: + :show-inheritance: diff --git a/docs/source/telegram.rst b/docs/source/telegram.rst index daa1f9656..f0817a606 100644 --- a/docs/source/telegram.rst +++ b/docs/source/telegram.rst @@ -13,8 +13,10 @@ telegram package telegram.callbackquery telegram.chat telegram.chataction + telegram.chatinvitelink telegram.chatlocation telegram.chatmember + telegram.chatmemberupdated telegram.chatpermissions telegram.chatphoto telegram.constants @@ -38,6 +40,7 @@ telegram package telegram.location telegram.loginurl telegram.message + telegram.messageautodeletetimerchanged telegram.messageid telegram.messageentity telegram.parsemode @@ -57,6 +60,9 @@ telegram package telegram.video telegram.videonote telegram.voice + telegram.voicechatstarted + telegram.voicechatended + telegram.voicechatparticipantsinvited telegram.webhookinfo Stickers diff --git a/docs/source/telegram.voicechatended.rst b/docs/source/telegram.voicechatended.rst new file mode 100644 index 000000000..1183f632f --- /dev/null +++ b/docs/source/telegram.voicechatended.rst @@ -0,0 +1,7 @@ +telegram.VoiceChatEnded +======================= + +.. autoclass:: telegram.VoiceChatEnded + :members: + :show-inheritance: + diff --git a/docs/source/telegram.voicechatparticipantsinvited.rst b/docs/source/telegram.voicechatparticipantsinvited.rst new file mode 100644 index 000000000..c2e70a9a8 --- /dev/null +++ b/docs/source/telegram.voicechatparticipantsinvited.rst @@ -0,0 +1,7 @@ +telegram.VoiceChatParticipantsInvited +===================================== + +.. autoclass:: telegram.VoiceChatParticipantsInvited + :members: + :show-inheritance: + diff --git a/docs/source/telegram.voicechatstarted.rst b/docs/source/telegram.voicechatstarted.rst new file mode 100644 index 000000000..31981b95a --- /dev/null +++ b/docs/source/telegram.voicechatstarted.rst @@ -0,0 +1,7 @@ +telegram.VoiceChatStarted +========================= + +.. autoclass:: telegram.VoiceChatStarted + :members: + :show-inheritance: + diff --git a/telegram/__init__.py b/telegram/__init__.py index 19f6386f9..9a2a3b1e5 100644 --- a/telegram/__init__.py +++ b/telegram/__init__.py @@ -24,7 +24,9 @@ from .user import User from .files.chatphoto import ChatPhoto from .chat import Chat from .chatlocation import ChatLocation +from .chatinvitelink import ChatInviteLink from .chatmember import ChatMember +from .chatmemberupdated import ChatMemberUpdated from .chatpermissions import ChatPermissions from .files.photosize import PhotoSize from .files.audio import Audio @@ -54,6 +56,7 @@ from .messageentity import MessageEntity from .messageid import MessageId from .games.game import Game from .poll import Poll, PollOption, PollAnswer +from .voicechat import VoiceChatStarted, VoiceChatEnded, VoiceChatParticipantsInvited from .loginurl import LoginUrl from .proximityalerttriggered import ProximityAlertTriggered from .games.callbackgame import CallbackGame @@ -68,6 +71,7 @@ from .passport.encryptedpassportelement import EncryptedPassportElement from .passport.passportdata import PassportData from .inline.inlinekeyboardbutton import InlineKeyboardButton from .inline.inlinekeyboardmarkup import InlineKeyboardMarkup +from .messageautodeletetimerchanged import MessageAutoDeleteTimerChanged from .message import Message from .callbackquery import CallbackQuery from .choseninlineresult import ChosenInlineResult @@ -144,7 +148,7 @@ from .passport.credentials import ( TelegramDecryptionError, ) from .bot import Bot -from .version import __version__ # noqa: F401 +from .version import __version__, bot_api_version # noqa: F401 __author__ = 'devs@python-telegram-bot.org' @@ -157,8 +161,10 @@ __all__ = ( # Keep this alphabetically ordered 'CallbackQuery', 'Chat', 'ChatAction', + 'ChatInviteLink', 'ChatLocation', 'ChatMember', + 'ChatMemberUpdated', 'ChatPermissions', 'ChatPhoto', 'ChosenInlineResult', @@ -226,6 +232,7 @@ __all__ = ( # Keep this alphabetically ordered 'MAX_MESSAGE_LENGTH', 'MaskPosition', 'Message', + 'MessageAutoDeleteTimerChanged', 'MessageEntity', 'MessageId', 'OrderInfo', @@ -272,5 +279,8 @@ __all__ = ( # Keep this alphabetically ordered 'Video', 'VideoNote', 'Voice', + 'VoiceChatStarted', + 'VoiceChatEnded', + 'VoiceChatParticipantsInvited', 'WebhookInfo', ) diff --git a/telegram/__main__.py b/telegram/__main__.py index c4a91cc74..9532748e2 100644 --- a/telegram/__main__.py +++ b/telegram/__main__.py @@ -24,6 +24,7 @@ from typing import Optional import certifi from . import __version__ as telegram_ver +from .constants import BOT_API_VERSION def _git_revision() -> Optional[str]: @@ -39,6 +40,7 @@ def _git_revision() -> Optional[str]: def print_ver_info() -> None: git_revision = _git_revision() print(f'python-telegram-bot {telegram_ver}' + (f' ({git_revision})' if git_revision else '')) + print(f'Bot API {BOT_API_VERSION}') print(f'certifi {certifi.__version__}') # type: ignore[attr-defined] sys_version = sys.version.replace('\n', ' ') print(f'Python {sys_version}') diff --git a/telegram/base.py b/telegram/base.py index a3f9b0b46..030ea7b78 100644 --- a/telegram/base.py +++ b/telegram/base.py @@ -46,15 +46,13 @@ class TelegramObject: @staticmethod def parse_data(data: Optional[JSONDict]) -> Optional[JSONDict]: - if not data: - return None - return data.copy() + return None if data is None else data.copy() @classmethod def de_json(cls: Type[TO], data: Optional[JSONDict], bot: 'Bot') -> Optional[TO]: data = cls.parse_data(data) - if not data: + if data is None: return None if cls == TelegramObject: diff --git a/telegram/bot.py b/telegram/bot.py index 2e2f673a3..95a6080d0 100644 --- a/telegram/bot.py +++ b/telegram/bot.py @@ -85,6 +85,7 @@ from telegram import ( Voice, WebhookInfo, InlineKeyboardMarkup, + ChatInviteLink, ) from telegram.constants import MAX_INLINE_QUERY_RESULTS from telegram.error import InvalidToken, TelegramError @@ -297,7 +298,7 @@ class Bot(TelegramObject): if result is True: return result - return Message.de_json(result, self) # type: ignore[arg-type,return-value] + return Message.de_json(result, self) # type: ignore[return-value, arg-type] @property def request(self) -> Request: @@ -406,7 +407,7 @@ class Bot(TelegramObject): """ result = self._post('getMe', timeout=timeout, api_kwargs=api_kwargs) - self._bot = User.de_json(result, self) # type: ignore + self._bot = User.de_json(result, self) # type: ignore[return-value, arg-type] return self._bot # type: ignore[return-value] @@ -2185,7 +2186,7 @@ class Bot(TelegramObject): result = self._post('getUserProfilePhotos', data, timeout=timeout, api_kwargs=api_kwargs) - return UserProfilePhotos.de_json(result, self) # type: ignore + return UserProfilePhotos.de_json(result, self) # type: ignore[return-value, arg-type] @log def get_file( @@ -2245,7 +2246,7 @@ class Bot(TelegramObject): self.base_file_url, result['file_path'] # type: ignore[index] ) - return File.de_json(result, self) # type: ignore + return File.de_json(result, self) # type: ignore[return-value, arg-type] @log def kick_chat_member( @@ -2255,25 +2256,33 @@ class Bot(TelegramObject): timeout: ODVInput[float] = DEFAULT_NONE, until_date: Union[int, datetime] = None, api_kwargs: JSONDict = None, + revoke_messages: bool = None, ) -> bool: """ - Use this method to kick a user from a group or a supergroup or a channel. In the case of + Use this method to kick 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 - group for this to work. + chat for this to work and must have the appropriate admin rights. Args: - chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username - of the target channel (in the format @channelusername). + chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target group or username + of the target supergroup or channel (in the format @channelusername). user_id (:obj:`int`): Unique identifier of the target user. 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). until_date (:obj:`int` | :obj:`datetime.datetime`, optional): Date when the user will be unbanned, unix time. If user is banned for more than 366 days or less than 30 - seconds from the current time they are considered to be banned forever. + seconds from the current time they are considered to be banned forever. Applied + for supergroups and channels only. For timezone naive :obj:`datetime.datetime` objects, the default timezone of the bot will be used. + revoke_messages (:obj:`bool`, optional): Pass :obj:`True` to delete all messages from + the chat for the user that is being removed. If :obj:`False`, the user will be able + to see messages in the group that were sent before the user was removed. + Always :obj:`True` for supergroups and channels. + + .. versionadded:: 13.4 api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the Telegram API. @@ -2293,6 +2302,9 @@ class Bot(TelegramObject): ) data['until_date'] = until_date + if revoke_messages is not None: + data['revoke_messages'] = revoke_messages + result = self._post('kickChatMember', data, timeout=timeout, api_kwargs=api_kwargs) return result # type: ignore[return-value] @@ -2711,10 +2723,11 @@ class Bot(TelegramObject): updates you want your bot to receive. For example, specify ["message", "edited_channel_post", "callback_query"] to only receive updates of these types. See :class:`telegram.Update` for a complete list of available update types. - Specify an empty list to receive all updates regardless of type (default). If not - specified, the previous setting will be used. Please note that this parameter - doesn't affect updates created before the call to the get_updates, so unwanted - updates may be received for a short period of time. + Specify an empty list to receive all updates except + :attr:`telegram.Update.chat_member` (default). If not specified, the previous + setting will be used. Please note that this parameter doesn't affect updates + created before the call to the get_updates, so unwanted updates may be received for + a short period of time. api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the Telegram API. @@ -2799,10 +2812,11 @@ class Bot(TelegramObject): updates you want your bot to receive. For example, specify ["message", "edited_channel_post", "callback_query"] to only receive updates of these types. See :class:`telegram.Update` for a complete list of available update types. - Specify an empty list to receive all updates regardless of type (default). If not - specified, the previous setting will be used. Please note that this parameter - doesn't affect updates created before the call to the set_webhook, so unwanted - updates may be received for a short period of time. + Specify an empty list to receive all updates except + :attr:`telegram.Update.chat_member` (default). If not specified, the previous + setting will be used. Please note that this parameter doesn't affect updates + created before the call to the set_webhook, so unwanted updates may be received for + a short period of time. drop_pending_updates (:obj:`bool`, optional): Pass :obj:`True` to drop all pending updates. timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as @@ -2948,7 +2962,7 @@ class Bot(TelegramObject): result = self._post('getChat', data, timeout=timeout, api_kwargs=api_kwargs) - return Chat.de_json(result, self) # type: ignore + return Chat.de_json(result, self) # type: ignore[return-value, arg-type] @log def get_chat_administrators( @@ -3047,7 +3061,7 @@ class Bot(TelegramObject): result = self._post('getChatMember', data, timeout=timeout, api_kwargs=api_kwargs) - return ChatMember.de_json(result, self) # type: ignore + return ChatMember.de_json(result, self) # type: ignore[return-value, arg-type] @log def set_chat_sticker_set( @@ -3132,7 +3146,7 @@ class Bot(TelegramObject): """ result = self._post('getWebhookInfo', None, timeout=timeout, api_kwargs=api_kwargs) - return WebhookInfo.de_json(result, self) # type: ignore + return WebhookInfo.de_json(result, self) # type: ignore[return-value, arg-type] @log def set_game_score( @@ -3588,6 +3602,8 @@ class Bot(TelegramObject): timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, is_anonymous: bool = None, + can_manage_chat: bool = None, + can_manage_voice_chats: bool = None, ) -> bool: """ Use this method to promote or demote a user in a supergroup or a channel. The bot must be @@ -3596,16 +3612,28 @@ class Bot(TelegramObject): Args: chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username - of the target supergroup (in the format @supergroupusername). + of the target channel (in the format @channelusername). user_id (:obj:`int`): Unique identifier of the target user. is_anonymous (:obj:`bool`, optional): Pass :obj:`True`, if the administrator's presence in the chat is hidden. + can_manage_chat (:obj:`bool`, optional): Pass :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 + + can_manage_voice_chats (:obj:`bool`, optional): Pass :obj:`True`, if the administrator + can manage voice chats, supergroups only. + + .. versionadded:: 13.4 + can_change_info (:obj:`bool`, optional): Pass :obj:`True`, if the administrator can change chat title, photo and other settings. can_post_messages (:obj:`bool`, optional): Pass :obj:`True`, if the administrator can create channel posts, channels only. can_edit_messages (:obj:`bool`, optional): Pass :obj:`True`, if the administrator can - edit messages of other users, channels only. + edit messages of other users and can pin messages, channels only. can_delete_messages (:obj:`bool`, optional): Pass :obj:`True`, if the administrator can delete messages of other users. can_invite_users (:obj:`bool`, optional): Pass :obj:`True`, if the administrator can @@ -3651,6 +3679,10 @@ class Bot(TelegramObject): data['can_pin_messages'] = can_pin_messages if can_promote_members is not None: data['can_promote_members'] = can_promote_members + if can_manage_chat is not None: + data['can_manage_chat'] = can_manage_chat + if can_manage_voice_chats is not None: + data['can_manage_voice_chats'] = can_manage_voice_chats result = self._post('promoteChatMember', data, timeout=timeout, api_kwargs=api_kwargs) @@ -3740,9 +3772,9 @@ class Bot(TelegramObject): api_kwargs: JSONDict = None, ) -> str: """ - Use this method to generate a new invite link for a chat; any previously generated link - is revoked. The bot must be an administrator in the chat for this to work and must have - the appropriate admin rights. + Use this method to generate a new primary invite link for a chat; any previously generated + link is revoked. The bot must be an administrator in the chat for this to work and must + have the appropriate admin rights. Args: chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username @@ -3753,6 +3785,13 @@ class Bot(TelegramObject): api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the Telegram API. + Note: + Each administrator in a chat generates their own invite links. Bots can't use invite + links generated by other administrators. If you want your bot to work with invite + links, it will need to generate its own link using :meth:`export_chat_invite_link` or + by calling the :meth:`get_chat` method. If your bot needs to generate a new primary + invite link replacing its previous one, use :attr:`export_chat_invite_link` again. + Returns: :obj:`str`: New invite link on success. @@ -3766,6 +3805,155 @@ class Bot(TelegramObject): return result # type: ignore[return-value] + @log + def create_chat_invite_link( + self, + chat_id: Union[str, int], + expire_date: Union[int, datetime] = None, + member_limit: int = None, + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> ChatInviteLink: + """ + Use this method to create an additional invite link for a chat. The bot must be an + administrator in the chat for this to work and must have the appropriate admin rights. + The link can be revoked using the method :meth:`revoke_chat_invite_link`. + + .. versionadded:: 13.4 + + Args: + chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username + of the target channel (in the format @channelusername). + expire_date (:obj:`int` | :obj:`datetime.datetime`, optional): Date when the link will + expire. + For timezone naive :obj:`datetime.datetime` objects, the default timezone of the + bot will be used. + member_limit (:obj:`int`, optional): Maximum number of users that can be members of + the chat simultaneously after joining the chat via this invite link; 1-99999. + 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: + :class:`telegram.ChatInviteLink` + + Raises: + :class:`telegram.error.TelegramError` + + """ + data: JSONDict = { + 'chat_id': chat_id, + } + + if expire_date is not None: + if isinstance(expire_date, datetime): + expire_date = to_timestamp( + expire_date, tzinfo=self.defaults.tzinfo if self.defaults else None + ) + data['expire_date'] = expire_date + + if member_limit is not None: + data['member_limit'] = member_limit + + result = self._post('createChatInviteLink', data, timeout=timeout, api_kwargs=api_kwargs) + + return ChatInviteLink.de_json(result, self) # type: ignore[return-value, arg-type] + + @log + def edit_chat_invite_link( + self, + chat_id: Union[str, int], + invite_link: str, + expire_date: Union[int, datetime] = None, + member_limit: int = None, + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> ChatInviteLink: + """ + Use this method to edit a non-primary invite link created by the bot. The bot must be an + administrator in the chat for this to work and must have the appropriate admin rights. + + .. versionadded:: 13.4 + + Args: + chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username + of the target channel (in the format @channelusername). + invite_link (:obj:`str`): The invite link to edit. + expire_date (:obj:`int` | :obj:`datetime.datetime`, optional): Date when the link will + expire. + For timezone naive :obj:`datetime.datetime` objects, the default timezone of the + bot will be used. + member_limit (:obj:`int`, optional): Maximum number of users that can be members of + the chat simultaneously after joining the chat via this invite link; 1-99999. + 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: + :class:`telegram.ChatInviteLink` + + Raises: + :class:`telegram.error.TelegramError` + + """ + data: JSONDict = {'chat_id': chat_id, 'invite_link': invite_link} + + if expire_date is not None: + if isinstance(expire_date, datetime): + expire_date = to_timestamp( + expire_date, tzinfo=self.defaults.tzinfo if self.defaults else None + ) + data['expire_date'] = expire_date + + if member_limit is not None: + data['member_limit'] = member_limit + + result = self._post('editChatInviteLink', data, timeout=timeout, api_kwargs=api_kwargs) + + return ChatInviteLink.de_json(result, self) # type: ignore[return-value, arg-type] + + @log + def revoke_chat_invite_link( + self, + chat_id: Union[str, int], + invite_link: str, + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> ChatInviteLink: + """ + Use this method to revoke an invite link created by the bot. If the primary link is + revoked, a new link is automatically generated. The bot must be an administrator in the + chat for this to work and must have the appropriate admin rights. + + .. versionadded:: 13.4 + + Args: + chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username + of the target channel (in the format @channelusername). + invite_link (:obj:`str`): The invite link to edit. + 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: + :class:`telegram.ChatInviteLink` + + Raises: + :class:`telegram.error.TelegramError` + + """ + data: JSONDict = {'chat_id': chat_id, 'invite_link': invite_link} + + result = self._post('revokeChatInviteLink', data, timeout=timeout, api_kwargs=api_kwargs) + + return ChatInviteLink.de_json(result, self) # type: ignore[return-value, arg-type] + @log def set_chat_photo( self, @@ -4061,7 +4249,7 @@ class Bot(TelegramObject): result = self._post('getStickerSet', data, timeout=timeout, api_kwargs=api_kwargs) - return StickerSet.de_json(result, self) # type: ignore + return StickerSet.de_json(result, self) # type: ignore[return-value, arg-type] @log def upload_sticker_file( @@ -4106,7 +4294,7 @@ class Bot(TelegramObject): result = self._post('uploadStickerFile', data, timeout=timeout, api_kwargs=api_kwargs) - return File.de_json(result, self) # type: ignore + return File.de_json(result, self) # type: ignore[return-value, arg-type] @log def create_new_sticker_set( @@ -4599,7 +4787,7 @@ class Bot(TelegramObject): result = self._post('stopPoll', data, timeout=timeout, api_kwargs=api_kwargs) - return Poll.de_json(result, self) # type: ignore + return Poll.de_json(result, self) # type: ignore[return-value, arg-type] @log def send_dice( @@ -4614,15 +4802,18 @@ class Bot(TelegramObject): allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, ) -> Message: """ - Use this method to send an animated emoji, which will have a random value. On success, the - sent Message is returned. + Use this method to send an animated emoji that will display a random value. Args: - chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target private chat. + chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username + of the target channel (in the format @channelusername). emoji (:obj:`str`, optional): Emoji on which the dice throw animation is based. - Currently, must be one of “🎲”, “🎯”, “🏀”, “⚽”, or “🎰”. Dice can have values 1-6 - for “🎲” and “🎯”, values 1-5 for “🏀” and “⚽”, and values 1-64 for “🎰”. Defaults - to “🎲”. + Currently, must be one of “🎲”, “🎯”, “🏀”, “⚽”, "🎳", or “🎰”. Dice can have + values 1-6 for “🎲”, “🎯” and "🎳", values 1-5 for “🏀” and “⚽”, and values 1-64 + for “🎰”. Defaults to “🎲”. + + .. versionchanged:: 13.4 + Added the "🎳" emoji. disable_notification (:obj:`bool`, optional): Sends the message silently. Users will receive a notification with no sound. reply_to_message_id (:obj:`int`, optional): If the message is a reply, ID of the @@ -4852,7 +5043,7 @@ class Bot(TelegramObject): data['reply_markup'] = reply_markup result = self._post('copyMessage', data, timeout=timeout, api_kwargs=api_kwargs) - return MessageId.de_json(result, self) # type: ignore + return MessageId.de_json(result, self) # type: ignore[return-value, arg-type] def to_dict(self) -> JSONDict: data: JSONDict = {'id': self.id, 'username': self.username, 'first_name': self.first_name} @@ -4971,6 +5162,12 @@ class Bot(TelegramObject): """Alias for :attr:`set_chat_administrator_custom_title`""" exportChatInviteLink = export_chat_invite_link """Alias for :attr:`export_chat_invite_link`""" + createChatInviteLink = create_chat_invite_link + """Alias for :attr:`create_chat_invite_link`""" + editChatInviteLink = edit_chat_invite_link + """Alias for :attr:`edit_chat_invite_link`""" + revokeChatInviteLink = revoke_chat_invite_link + """Alias for :attr:`revoke_chat_invite_link`""" setChatPhoto = set_chat_photo """Alias for :attr:`set_chat_photo`""" deleteChatPhoto = delete_chat_photo diff --git a/telegram/callbackquery.py b/telegram/callbackquery.py index 3bafb8d31..00ba5cbf5 100644 --- a/telegram/callbackquery.py +++ b/telegram/callbackquery.py @@ -47,7 +47,7 @@ class CallbackQuery(TelegramObject): considered equal, if their :attr:`id` is equal. Note: - * In Python `from` is a reserved word, use `from_user` instead. + * In Python ``from`` is a reserved word, use ``from_user`` instead. * Exactly one of the fields :attr:`data` or :attr:`game_short_name` will be present. * After the user presses an inline button, Telegram clients will display a progress bar until you call :attr:`answer`. It is, therefore, necessary to react diff --git a/telegram/chat.py b/telegram/chat.py index b14cc5f29..93e26ba3b 100644 --- a/telegram/chat.py +++ b/telegram/chat.py @@ -32,6 +32,7 @@ if TYPE_CHECKING: from telegram import ( Bot, ChatMember, + ChatInviteLink, Message, MessageId, ReplyMarkup, @@ -80,10 +81,8 @@ class Chat(TelegramObject): :meth:`telegram.Bot.get_chat`. description (:obj:`str`, optional): Description, for groups, supergroups and channel chats. Returned only in :meth:`telegram.Bot.get_chat`. - invite_link (:obj:`str`, optional): Chat invite link, for groups, supergroups and channel - chats. Each administrator in a chat generates their own invite links, so the bot must - first generate the link using ``export_chat_invite_link()``. Returned only - in :meth:`telegram.Bot.get_chat`. + invite_link (:obj:`str`, optional): Primary invite link, for groups, supergroups and + channel. Returned only in :meth:`telegram.Bot.get_chat`. pinned_message (:class:`telegram.Message`, optional): The most recent pinned message (by sending date). Returned only in :meth:`telegram.Bot.get_chat`. permissions (:class:`telegram.ChatPermissions`): Optional. Default chat member permissions, @@ -91,6 +90,11 @@ class Chat(TelegramObject): slow_mode_delay (:obj:`int`, optional): For supergroups, the minimum allowed delay between consecutive messages sent by each unprivileged user. Returned only in :meth:`telegram.Bot.get_chat`. + message_auto_delete_time (:obj:`int`, optional): The time after which all messages sent to + the chat will be automatically deleted; in seconds. Returned only in + :meth:`telegram.Bot.get_chat`. + + .. versionadded:: 13.4 bot (:class:`telegram.Bot`, optional): The Bot to use for instance methods. sticker_set_name (:obj:`str`, optional): For supergroups, name of group sticker set. Returned only in :meth:`telegram.Bot.get_chat`. @@ -114,7 +118,8 @@ class Chat(TelegramObject): bio (:obj:`str`): Optional. Bio of the other party in a private chat. Returned only in :meth:`telegram.Bot.get_chat`. description (:obj:`str`): Optional. Description, for groups, supergroups and channel chats. - invite_link (:obj:`str`): Optional. Chat invite link, for supergroups and channel chats. + invite_link (:obj:`str`): Optional. Primary invite link, for groups, supergroups and + channel. Returned only in :meth:`telegram.Bot.get_chat`. pinned_message (:class:`telegram.Message`): Optional. The most recent pinned message (by sending date). Returned only in :meth:`telegram.Bot.get_chat`. permissions (:class:`telegram.ChatPermissions`): Optional. Default chat member permissions, @@ -122,6 +127,11 @@ class Chat(TelegramObject): slow_mode_delay (:obj:`int`): Optional. For supergroups, the minimum allowed delay between consecutive messages sent by each unprivileged user. Returned only in :meth:`telegram.Bot.get_chat`. + message_auto_delete_time (:obj:`int`): Optional. The time after which all messages sent to + the chat will be automatically deleted; in seconds. Returned only in + :meth:`telegram.Bot.get_chat`. + + .. versionadded:: 13.4 sticker_set_name (:obj:`str`): Optional. For supergroups, name of Group sticker set. can_set_sticker_set (:obj:`bool`): Optional. :obj:`True`, if the bot can change group the sticker set. @@ -162,6 +172,7 @@ class Chat(TelegramObject): bio: str = None, linked_chat_id: int = None, location: ChatLocation = None, + message_auto_delete_time: int = None, **_kwargs: Any, ): # Required @@ -181,6 +192,9 @@ class Chat(TelegramObject): self.pinned_message = pinned_message self.permissions = permissions self.slow_mode_delay = slow_mode_delay + self.message_auto_delete_time = ( + int(message_auto_delete_time) if message_auto_delete_time is not None else None + ) self.sticker_set_name = sticker_set_name self.can_set_sticker_set = can_set_sticker_set self.linked_chat_id = linked_chat_id @@ -216,7 +230,7 @@ class Chat(TelegramObject): return None @classmethod - def de_json(cls, data: JSONDict, bot: 'Bot') -> Optional['Chat']: + def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['Chat']: data = cls.parse_data(data) if not data: @@ -320,6 +334,7 @@ class Chat(TelegramObject): timeout: ODVInput[float] = DEFAULT_NONE, until_date: Union[int, datetime] = None, api_kwargs: JSONDict = None, + revoke_messages: bool = None, ) -> bool: """Shortcut for:: @@ -343,6 +358,7 @@ class Chat(TelegramObject): timeout=timeout, until_date=until_date, api_kwargs=api_kwargs, + revoke_messages=revoke_messages, ) def unban_member( @@ -384,6 +400,8 @@ class Chat(TelegramObject): timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, is_anonymous: bool = None, + can_manage_chat: bool = None, + can_manage_voice_chats: bool = None, ) -> bool: """Shortcut for:: @@ -412,6 +430,8 @@ class Chat(TelegramObject): timeout=timeout, api_kwargs=api_kwargs, is_anonymous=is_anonymous, + can_manage_chat=can_manage_chat, + can_manage_voice_chats=can_manage_voice_chats, ) def restrict_member( @@ -1391,3 +1411,106 @@ class Chat(TelegramObject): timeout=timeout, api_kwargs=api_kwargs, ) + + def export_invite_link( + self, + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> str: + """Shortcut for:: + + bot.export_chat_invite_link(chat_id=update.effective_chat.id, *args, **kwargs) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.export_chat_invite_link`. + + .. versionadded:: 13.4 + + Returns: + :obj:`str`: New invite link on success. + + """ + return self.bot.export_chat_invite_link( + chat_id=self.id, timeout=timeout, api_kwargs=api_kwargs + ) + + def create_invite_link( + self, + expire_date: Union[int, datetime] = None, + member_limit: int = None, + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> 'ChatInviteLink': + """Shortcut for:: + + bot.create_chat_invite_link(chat_id=update.effective_chat.id, *args, **kwargs) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.create_chat_invite_link`. + + .. versionadded:: 13.4 + + Returns: + :class:`telegram.ChatInviteLink` + + """ + return self.bot.create_chat_invite_link( + chat_id=self.id, + expire_date=expire_date, + member_limit=member_limit, + timeout=timeout, + api_kwargs=api_kwargs, + ) + + def edit_invite_link( + self, + invite_link: str, + expire_date: Union[int, datetime] = None, + member_limit: int = None, + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> 'ChatInviteLink': + """Shortcut for:: + + bot.edit_chat_invite_link(chat_id=update.effective_chat.id, *args, **kwargs) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.edit_chat_invite_link`. + + .. versionadded:: 13.4 + + Returns: + :class:`telegram.ChatInviteLink` + + """ + return self.bot.edit_chat_invite_link( + chat_id=self.id, + invite_link=invite_link, + expire_date=expire_date, + member_limit=member_limit, + timeout=timeout, + api_kwargs=api_kwargs, + ) + + def revoke_invite_link( + self, + invite_link: str, + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> 'ChatInviteLink': + """Shortcut for:: + + bot.revoke_chat_invite_link(chat_id=update.effective_chat.id, *args, **kwargs) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.revoke_chat_invite_link`. + + .. versionadded:: 13.4 + + Returns: + :class:`telegram.ChatInviteLink` + + """ + return self.bot.revoke_chat_invite_link( + chat_id=self.id, invite_link=invite_link, timeout=timeout, api_kwargs=api_kwargs + ) diff --git a/telegram/chatinvitelink.py b/telegram/chatinvitelink.py new file mode 100644 index 000000000..a06782229 --- /dev/null +++ b/telegram/chatinvitelink.py @@ -0,0 +1,102 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-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/]. +"""This module contains an object that represents an invite link for a chat.""" +import datetime +from typing import TYPE_CHECKING, Any, Optional + +from telegram import TelegramObject, User +from telegram.utils.helpers import from_timestamp, to_timestamp +from telegram.utils.types import JSONDict + +if TYPE_CHECKING: + from telegram import Bot + + +class ChatInviteLink(TelegramObject): + """This object represents an invite link for a chat. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`invite_link`, :attr:`creator`, :attr:`is_primary` and + :attr:`is_revoked` are equal. + + .. versionadded:: 13.4 + + Args: + invite_link (:obj:`str`): The invite link. + creator (:class:`telegram.User`): Creator of the link. + is_primary (:obj:`bool`): :obj:`True`, if the link is primary. + is_revoked (:obj:`bool`): :obj:`True`, if the link is revoked. + expire_date (:class:`datetime.datetime`, optional): Date when the link will expire or + has been expired. + member_limit (:obj:`int`, optional): Maximum number of users that can be members of the + chat simultaneously after joining the chat via this invite link; 1-99999. + + Attributes: + invite_link (:obj:`str`): The invite link. If the link was created by another chat + administrator, then the second part of the link will be replaced with ``'…'``. + creator (:class:`telegram.User`): Creator of the link. + is_primary (:obj:`bool`): :obj:`True`, if the link is primary. + is_revoked (:obj:`bool`): :obj:`True`, if the link is revoked. + expire_date (:class:`datetime.datetime`): Optional. Date when the link will expire or + has been expired. + member_limit (:obj:`int`): Optional. Maximum number of users that can be members + of the chat simultaneously after joining the chat via this invite link; 1-99999. + + """ + + def __init__( + self, + invite_link: str, + creator: User, + is_primary: bool, + is_revoked: bool, + expire_date: datetime.datetime = None, + member_limit: int = None, + **_kwargs: Any, + ): + # Required + self.invite_link = invite_link + self.creator = creator + self.is_primary = is_primary + self.is_revoked = is_revoked + + # Optionals + self.expire_date = expire_date + self.member_limit = int(member_limit) if member_limit is not None else None + + self._id_attrs = (self.invite_link, self.creator, self.is_primary, self.is_revoked) + + @classmethod + def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['ChatInviteLink']: + data = cls.parse_data(data) + + if not data: + return None + + data['creator'] = User.de_json(data.get('creator'), bot) + data['expire_date'] = from_timestamp(data.get('expire_date', None)) + + return cls(**data) + + def to_dict(self) -> JSONDict: + data = super().to_dict() + + data['expire_date'] = to_timestamp(self.expire_date) + + return data diff --git a/telegram/chatmember.py b/telegram/chatmember.py index 8d449ef7a..68141cd3d 100644 --- a/telegram/chatmember.py +++ b/telegram/chatmember.py @@ -46,6 +46,18 @@ class ChatMember(TelegramObject): restrictions will be lifted for this user. can_be_edited (:obj:`bool`, optional): Administrators only. :obj:`True`, if the bot is allowed to edit administrator privileges of that user. + 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 + + can_manage_voice_chats (:obj:`bool`, optional): Administrators only. :obj:`True`, if the + administrator can manage voice chats. + + .. versionadded:: 13.4 + can_change_info (:obj:`bool`, optional): Administrators and restricted only. :obj:`True`, if the user can change the chat title, photo and other settings. can_post_messages (:obj:`bool`, optional): Administrators only. :obj:`True`, if the @@ -87,6 +99,17 @@ class ChatMember(TelegramObject): for this user. can_be_edited (:obj:`bool`): Optional. If the bot is allowed to edit administrator privileges of that user. + 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 + + can_manage_voice_chats (:obj:`bool`): Optional. if the administrator can manage + voice chats. + + .. versionadded:: 13.4 + can_change_info (:obj:`bool`): Optional. If the user can change the chat title, photo and other settings. can_post_messages (:obj:`bool`): Optional. If the administrator can post in the channel. @@ -150,6 +173,8 @@ class ChatMember(TelegramObject): is_member: bool = None, custom_title: str = None, is_anonymous: bool = None, + can_manage_chat: bool = None, + can_manage_voice_chats: bool = None, **_kwargs: Any, ): # Required @@ -175,6 +200,8 @@ class ChatMember(TelegramObject): self.can_send_other_messages = can_send_other_messages self.can_add_web_page_previews = can_add_web_page_previews self.is_member = is_member + self.can_manage_chat = can_manage_chat + self.can_manage_voice_chats = can_manage_voice_chats self._id_attrs = (self.user, self.status) diff --git a/telegram/chatmemberupdated.py b/telegram/chatmemberupdated.py new file mode 100644 index 000000000..25dc215e3 --- /dev/null +++ b/telegram/chatmemberupdated.py @@ -0,0 +1,115 @@ +#!/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/]. +"""This module contains an object that represents a Telegram ChatMemberUpdated.""" +import datetime +from typing import TYPE_CHECKING, Any, Optional + +from telegram import TelegramObject, User, Chat, ChatMember, ChatInviteLink +from telegram.utils.helpers import from_timestamp, to_timestamp +from telegram.utils.types import JSONDict + +if TYPE_CHECKING: + from telegram import Bot + + +class ChatMemberUpdated(TelegramObject): + """This object represents changes in the status of a chat member. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`chat`, :attr:`from_user`, :attr:`date`, + :attr:`old_chat_member` and :attr:`new_chat_member` are equal. + + .. versionadded:: 13.4 + + Note: + In Python ``from`` is a reserved word, use ``from_user`` instead. + + Args: + chat (:class:`telegram.Chat`): Chat the user belongs to. + from_user (:class:`telegram.User`): Performer of the action, which resulted in the change. + date (:class:`datetime.datetime`): Date the change was done in Unix time. Converted to + :class:`datetime.datetime`. + old_chat_member (:class:`telegram.ChatMember`): Previous information about the chat member. + new_chat_member (:class:`telegram.ChatMember`): New information about the chat member. + invite_link (:class:`telegram.ChatInviteLink`, optional): Chat invite link, which was used + by the user to join the chat. For joining by invite link events only. + + Attributes: + chat (:class:`telegram.Chat`): Chat the user belongs to. + from_user (:class:`telegram.User`): Performer of the action, which resulted in the change. + date (:class:`datetime.datetime`): Date the change was done in Unix time. Converted to + :class:`datetime.datetime`. + old_chat_member (:class:`telegram.ChatMember`): Previous information about the chat member. + new_chat_member (:class:`telegram.ChatMember`): New information about the chat member. + invite_link (:class:`telegram.ChatInviteLink`): Optional. Chat invite link, which was used + by the user to join the chat. + + """ + + def __init__( + self, + chat: Chat, + from_user: User, + date: datetime.datetime, + old_chat_member: ChatMember, + new_chat_member: ChatMember, + invite_link: ChatInviteLink = None, + **_kwargs: Any, + ): + # Required + self.chat = chat + self.from_user = from_user + self.date = date + self.old_chat_member = old_chat_member + self.new_chat_member = new_chat_member + + # Optionals + self.invite_link = invite_link + + self._id_attrs = ( + self.chat, + self.from_user, + self.date, + self.old_chat_member, + self.new_chat_member, + ) + + @classmethod + def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['ChatMemberUpdated']: + data = cls.parse_data(data) + + if not data: + return None + + data['chat'] = Chat.de_json(data.get('chat'), bot) + data['from_user'] = User.de_json(data.get('from'), bot) + data['date'] = from_timestamp(data.get('date')) + data['old_chat_member'] = ChatMember.de_json(data.get('old_chat_member'), bot) + data['new_chat_member'] = ChatMember.de_json(data.get('new_chat_member'), bot) + data['invite_link'] = ChatInviteLink.de_json(data.get('invite_link'), bot) + + return cls(**data) + + def to_dict(self) -> JSONDict: + data = super().to_dict() + + # Required + data['date'] = to_timestamp(self.date) + + return data diff --git a/telegram/choseninlineresult.py b/telegram/choseninlineresult.py index 8b266da6b..24416728e 100644 --- a/telegram/choseninlineresult.py +++ b/telegram/choseninlineresult.py @@ -37,7 +37,7 @@ class ChosenInlineResult(TelegramObject): considered equal, if their :attr:`result_id` is equal. Note: - * In Python `from` is a reserved word, use `from_user` instead. + * In Python ``from`` is a reserved word, use ``from_user`` instead. * It is necessary to enable inline feedback via `@Botfather `_ in order to receive these objects in updates. diff --git a/telegram/constants.py b/telegram/constants.py index ceb574d9c..2e5e81a77 100644 --- a/telegram/constants.py +++ b/telegram/constants.py @@ -21,6 +21,10 @@ The following constants were extracted from the `Telegram Bots API `_. Attributes: + BOT_API_VERSION (:obj:`str`): `5.1`. Telegram Bot API version supported by this + version of `python-telegram-bot`. Also available as ``telegram.bot_api_version``. + + .. versionadded:: 13.4 MAX_MESSAGE_LENGTH (:obj:`int`): 4096 MAX_CAPTION_LENGTH (:obj:`int`): 1024 SUPPORTED_WEBHOOK_PORTS (List[:obj:`int`]): [443, 80, 88, 8443] @@ -88,8 +92,14 @@ Attributes: DICE_BASKETBALL (:obj:`str`): '🏀' DICE_FOOTBALL (:obj:`str`): '⚽' DICE_SLOT_MACHINE (:obj:`str`): '🎰' + DICE_BOWLING (:obj:`str`): '🎳' + + .. versionadded:: 13.4 DICE_ALL_EMOJI (List[:obj:`str`]): List of all supported base emoji. + .. versionchanged:: 13.4 + Added :attr:`DICE_BOWLING` + :class:`telegram.MessageEntity`: Attributes: @@ -136,6 +146,7 @@ Attributes: """ from typing import List +BOT_API_VERSION: str = '5.1' MAX_MESSAGE_LENGTH: int = 4096 MAX_CAPTION_LENGTH: int = 1024 ANONYMOUS_ADMIN_ID: int = 1087968824 @@ -182,12 +193,14 @@ DICE_DARTS: str = '🎯' DICE_BASKETBALL: str = '🏀' DICE_FOOTBALL: str = '⚽' DICE_SLOT_MACHINE: str = '🎰' +DICE_BOWLING: str = '🎳' DICE_ALL_EMOJI: List[str] = [ DICE_DICE, DICE_DARTS, DICE_BASKETBALL, DICE_FOOTBALL, DICE_SLOT_MACHINE, + DICE_BOWLING, ] MESSAGEENTITY_MENTION: str = 'mention' diff --git a/telegram/dice.py b/telegram/dice.py index 1b1300af3..3c7f8296d 100644 --- a/telegram/dice.py +++ b/telegram/dice.py @@ -45,13 +45,17 @@ class Dice(TelegramObject): 3 indicates that the goal was missed. However, this behaviour is undocumented and might be changed by Telegram. + If :attr:`emoji` is "🎳", a value of 6 knocks all the pins, while a value of 1 means all + the pins were missed. However, this behaviour is undocumented and might be changed by + Telegram. + If :attr:`emoji` is "🎰", each value corresponds to a unique combination of symbols, which can be found at our `wiki `_. However, this behaviour is undocumented and might be changed by Telegram. Args: - value (:obj:`int`): Value of the dice. 1-6 for dice and darts, 1-5 for basketball and - football/soccer ball, 1-64 for slot machine. + value (:obj:`int`): Value of the dice. 1-6 for dice, darts and bowling balls, 1-5 for + basketball and football/soccer ball, 1-64 for slot machine. emoji (:obj:`str`): Emoji on which the dice throw animation is based. Attributes: @@ -76,5 +80,11 @@ class Dice(TelegramObject): """:const:`telegram.constants.DICE_FOOTBALL`""" SLOT_MACHINE: ClassVar[str] = constants.DICE_SLOT_MACHINE """:const:`telegram.constants.DICE_SLOT_MACHINE`""" + BOWLING: ClassVar[str] = constants.DICE_BOWLING + """ + :const:`telegram.constants.DICE_BOWLING` + + .. versionadded:: 13.4 + """ ALL_EMOJI: ClassVar[List[str]] = constants.DICE_ALL_EMOJI """:const:`telegram.constants.DICE_ALL_EMOJI`""" diff --git a/telegram/ext/__init__.py b/telegram/ext/__init__.py index 380a0e418..f536c2be2 100644 --- a/telegram/ext/__init__.py +++ b/telegram/ext/__init__.py @@ -43,6 +43,7 @@ from .messagequeue import MessageQueue from .messagequeue import DelayQueue from .pollanswerhandler import PollAnswerHandler from .pollhandler import PollHandler +from .chatmemberhandler import ChatMemberHandler from .defaults import Defaults __all__ = ( @@ -78,5 +79,6 @@ __all__ = ( 'PrefixHandler', 'PollAnswerHandler', 'PollHandler', + 'ChatMemberHandler', 'Defaults', ) diff --git a/telegram/ext/chatmemberhandler.py b/telegram/ext/chatmemberhandler.py new file mode 100644 index 000000000..35ce49e2a --- /dev/null +++ b/telegram/ext/chatmemberhandler.py @@ -0,0 +1,146 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2019-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/]. +"""This module contains the ChatMemberHandler classes.""" +from typing import ClassVar, TypeVar, Union, Callable, TYPE_CHECKING + +from telegram import Update +from telegram.utils.helpers import DefaultValue, DEFAULT_FALSE +from .handler import Handler + +if TYPE_CHECKING: + from telegram.ext import CallbackContext + +RT = TypeVar('RT') + + +class ChatMemberHandler(Handler[Update]): + """Handler class to handle Telegram updates that contain a chat member update. + + .. versionadded:: 13.4 + + Note: + :attr:`pass_user_data` and :attr:`pass_chat_data` determine whether a ``dict`` you + can use to keep any data in will be sent to the :attr:`callback` function. Related to + either the user or the chat that the update was sent in. For each update from the same user + or in the same chat, it will be the same ``dict``. + + Note that this is DEPRECATED, and you should use context based callbacks. See + https://git.io/fxJuV for more info. + + Warning: + When setting ``run_async`` to :obj:`True`, you cannot rely on adding custom + attributes to :class:`telegram.ext.CallbackContext`. See its docs for more info. + + Args: + callback (:obj:`callable`): The callback function for this handler. Will be called when + :attr:`check_update` has determined that an update should be processed by this handler. + Callback signature for context based API: + + ``def callback(update: Update, context: CallbackContext)`` + + The return value of the callback is usually ignored except for the special case of + :class:`telegram.ext.ConversationHandler`. + chat_member_types (:obj:`int`, optional): Pass one of :attr:`MY_CHAT_MEMBER`, + :attr:`CHAT_MEMBER` or :attr:`ANY_CHAT_MEMBER` to specify if this handler should handle + only updates with :attr:`telegram.Update.my_chat_member`, + :attr:`telegram.Update.chat_member` or both. Defaults to :attr:`MY_CHAT_MEMBER`. + pass_update_queue (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called + ``update_queue`` will be passed to the callback function. It will be the ``Queue`` + instance used by the :class:`telegram.ext.Updater` and :class:`telegram.ext.Dispatcher` + that contains new updates which can be used to insert updates. Default is :obj:`False`. + DEPRECATED: Please switch to context based callbacks. + pass_job_queue (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called + ``job_queue`` will be passed to the callback function. It will be a + :class:`telegram.ext.JobQueue` instance created by the :class:`telegram.ext.Updater` + which can be used to schedule new jobs. Default is :obj:`False`. + DEPRECATED: Please switch to context based callbacks. + pass_user_data (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called + ``user_data`` will be passed to the callback function. Default is :obj:`False`. + DEPRECATED: Please switch to context based callbacks. + pass_chat_data (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called + ``chat_data`` will be passed to the callback function. Default is :obj:`False`. + DEPRECATED: Please switch to context based callbacks. + run_async (:obj:`bool`): Determines whether the callback will run asynchronously. + Defaults to :obj:`False`. + + Attributes: + callback (:obj:`callable`): The callback function for this handler. + chat_member_types (:obj:`int`, optional): Specifies if this handler should handle + only updates with :attr:`telegram.Update.my_chat_member`, + :attr:`telegram.Update.chat_member` or both. + pass_update_queue (:obj:`bool`): Determines whether ``update_queue`` will be + passed to the callback function. + pass_job_queue (:obj:`bool`): Determines whether ``job_queue`` will be passed to + the callback function. + pass_user_data (:obj:`bool`): Determines whether ``user_data`` will be passed to + the callback function. + pass_chat_data (:obj:`bool`): Determines whether ``chat_data`` will be passed to + the callback function. + run_async (:obj:`bool`): Determines whether the callback will run asynchronously. + + """ + + MY_CHAT_MEMBER: ClassVar[int] = -1 + """:obj:`int`: Used as a constant to handle only :attr:`telegram.Update.my_chat_member`.""" + CHAT_MEMBER: ClassVar[int] = 0 + """:obj:`int`: Used as a constant to handle only :attr:`telegram.Update.chat_member`.""" + ANY_CHAT_MEMBER: ClassVar[int] = 1 + """:obj:`int`: Used as a constant to handle bot :attr:`telegram.Update.my_chat_member` + and :attr:`telegram.Update.chat_member`.""" + + def __init__( + self, + callback: Callable[[Update, 'CallbackContext'], RT], + chat_member_types: int = MY_CHAT_MEMBER, + pass_update_queue: bool = False, + pass_job_queue: bool = False, + pass_user_data: bool = False, + pass_chat_data: bool = False, + run_async: Union[bool, DefaultValue] = DEFAULT_FALSE, + ): + super().__init__( + callback, + pass_update_queue=pass_update_queue, + pass_job_queue=pass_job_queue, + pass_user_data=pass_user_data, + pass_chat_data=pass_chat_data, + run_async=run_async, + ) + + self.chat_member_types = chat_member_types + + def check_update(self, update: object) -> bool: + """Determines whether an update should be passed to this handlers :attr:`callback`. + + Args: + update (:class:`telegram.Update` | :obj:`object`): Incoming update. + + Returns: + :obj:`bool` + + """ + if isinstance(update, Update): + if not (update.my_chat_member or update.chat_member): + return False + if self.chat_member_types == self.ANY_CHAT_MEMBER: + return True + if self.chat_member_types == self.CHAT_MEMBER: + return bool(update.chat_member) + return bool(update.my_chat_member) + return False diff --git a/telegram/ext/filters.py b/telegram/ext/filters.py index 6b6d1c06b..0268fb31d 100644 --- a/telegram/ext/filters.py +++ b/telegram/ext/filters.py @@ -1005,6 +1005,15 @@ officedocument.wordprocessingml.document")``. :attr: `telegram.Message.supergroup_chat_created` or :attr: `telegram.Message.channel_chat_created`.""" + class _MessageAutoDeleteTimerChanged(MessageFilter): + name = 'MessageAutoDeleteTimerChanged' + + def filter(self, message: Message) -> bool: + return bool(message.message_auto_delete_timer_changed) + + message_auto_delete_timer_changed = _MessageAutoDeleteTimerChanged() + """Messages that contain :attr:`message_auto_delete_timer_changed`""" + class _Migrate(MessageFilter): name = 'Filters.status_update.migrate' @@ -1042,6 +1051,33 @@ officedocument.wordprocessingml.document")``. proximity_alert_triggered = _ProximityAlertTriggered() """Messages that contain :attr:`telegram.Message.proximity_alert_triggered`.""" + class _VoiceChatStarted(MessageFilter): + name = 'Filters.status_update.voice_chat_started' + + def filter(self, message: Message) -> bool: + return bool(message.voice_chat_started) + + voice_chat_started = _VoiceChatStarted() + """Messages that contain :attr:`telegram.Message.voice_chat_started`.""" + + class _VoiceChatEnded(MessageFilter): + name = 'Filters.status_update.voice_chat_ended' + + def filter(self, message: Message) -> bool: + return bool(message.voice_chat_ended) + + voice_chat_ended = _VoiceChatEnded() + """Messages that contain :attr:`telegram.Message.voice_chat_ended`.""" + + class _VoiceChatParticipantsInvited(MessageFilter): + name = 'Filters.status_update.voice_chat_participants_invited' + + def filter(self, message: Message) -> bool: + return bool(message.voice_chat_participants_invited) + + voice_chat_participants_invited = _VoiceChatParticipantsInvited() + """Messages that contain :attr:`telegram.Message.voice_chat_participants_invited`.""" + name = 'Filters.status_update' def filter(self, message: Update) -> bool: @@ -1052,10 +1088,14 @@ officedocument.wordprocessingml.document")``. or self.new_chat_photo(message) or self.delete_chat_photo(message) or self.chat_created(message) + or self.message_auto_delete_timer_changed(message) or self.migrate(message) or self.pinned_message(message) or self.connected_website(message) or self.proximity_alert_triggered(message) + or self.voice_chat_started(message) + or self.voice_chat_ended(message) + or self.voice_chat_participants_invited(message) ) status_update = _StatusUpdate() @@ -1085,10 +1125,27 @@ officedocument.wordprocessingml.document")``. :attr:`telegram.Message.new_chat_photo`. new_chat_title: Messages that contain :attr:`telegram.Message.new_chat_title`. + message_auto_delete_timer_changed: Messages that contain + :attr:`message_auto_delete_timer_changed`. + + .. versionadded:: 13.4 pinned_message: Messages that contain :attr:`telegram.Message.pinned_message`. proximity_alert_triggered: Messages that contain :attr:`telegram.Message.proximity_alert_triggered`. + voice_chat_started: Messages that contain + :attr:`telegram.Message.voice_chat_started`. + + .. versionadded:: 13.4 + voice_chat_ended: Messages that contain + :attr:`telegram.Message.voice_chat_ended`. + + .. versionadded:: 13.4 + voice_chat_participants_invited: Messages that contain + :attr:`telegram.Message.voice_chat_participants_invited`. + + .. versionadded:: 13.4 + """ class _Forwarded(MessageFilter): @@ -1831,6 +1888,7 @@ officedocument.wordprocessingml.document")``. basketball = _DiceEmoji('🏀', 'basketball') football = _DiceEmoji('⚽') slot_machine = _DiceEmoji('🎰') + bowling = _DiceEmoji('🎳', 'bowling') dice = _Dice() """Dice Messages. If an integer or a list of integers is passed, it filters messages to only @@ -1863,6 +1921,11 @@ officedocument.wordprocessingml.document")``. as for :attr:`Filters.dice`. slot_machine: Dice messages with the emoji 🎰. Passing a list of integers is supported just as for :attr:`Filters.dice`. + bowling: Dice messages with the emoji 🎳. Passing a list of integers is supported just + as for :attr:`Filters.dice`. + + .. versionadded:: 13.4 + """ class language(MessageFilter): diff --git a/telegram/inline/inlinequery.py b/telegram/inline/inlinequery.py index 727fa5a49..fd2fea861 100644 --- a/telegram/inline/inlinequery.py +++ b/telegram/inline/inlinequery.py @@ -38,7 +38,7 @@ class InlineQuery(TelegramObject): considered equal, if their :attr:`id` is equal. Note: - * In Python `from` is a reserved word, use `from_user` instead. + * In Python ``from`` is a reserved word, use ``from_user`` instead. Args: id (:obj:`str`): Unique identifier for this query. diff --git a/telegram/inline/inlinequeryresult.py b/telegram/inline/inlinequeryresult.py index 0280f6c9e..cb7c38acc 100644 --- a/telegram/inline/inlinequeryresult.py +++ b/telegram/inline/inlinequeryresult.py @@ -31,6 +31,10 @@ class InlineQueryResult(TelegramObject): Objects of this class are comparable in terms of equality. Two objects of this class are considered equal, if their :attr:`id` is equal. + Note: + All URLs passed in inline query results will be available to end users and therefore must + be assumed to be public. + Args: type (:obj:`str`): Type of the result. id (:obj:`str`): Unique identifier for this result, 1-64 Bytes. diff --git a/telegram/message.py b/telegram/message.py index 92fc529a1..e942db659 100644 --- a/telegram/message.py +++ b/telegram/message.py @@ -47,8 +47,12 @@ from telegram import ( Video, VideoNote, Voice, + VoiceChatStarted, + VoiceChatEnded, + VoiceChatParticipantsInvited, ProximityAlertTriggered, ReplyMarkup, + MessageAutoDeleteTimerChanged, ) from telegram.utils.helpers import ( escape_markdown, @@ -83,7 +87,7 @@ class Message(TelegramObject): considered equal, if their :attr:`message_id` and :attr:`chat` are equal. Note: - In Python `from` is a reserved word, use `from_user` instead. + In Python ``from`` is a reserved word, use ``from_user`` instead. Args: message_id (:obj:`int`): Unique message identifier inside this chat. @@ -165,6 +169,10 @@ class Message(TelegramObject): created. This field can't be received in a message coming through updates, because bot can't be a member of a channel when it is created. It can only be found in :attr:`reply_to_message` if someone replies to a very first message in a channel. + message_auto_delete_timer_changed (:class:`telegram.MessageAutoDeleteTimerChanged`, \ + optional): Service message: auto-delete timer settings changed in the chat. + + .. versionadded:: 13.4 migrate_to_chat_id (:obj:`int`, optional): The group has been migrated to a supergroup with the specified identifier. This number may be greater than 32 bits and some programming languages may have difficulty/silent defects in interpreting it. But it is smaller than @@ -196,6 +204,18 @@ class Message(TelegramObject): proximity_alert_triggered (:class:`telegram.ProximityAlertTriggered`, optional): Service message. A user in the chat triggered another user's proximity alert while sharing Live Location. + voice_chat_started (:class:`telegram.VoiceChatStarted`, optional): Service message: voice + chat started. + + .. versionadded:: 13.4 + voice_chat_ended (:class:`telegram.VoiceChatEnded`, optional): Service message: voice chat + ended. + + .. versionadded:: 13.4 + voice_chat_participants_invited (:class:`telegram.VoiceChatParticipantsInvited` optional): + Service message: new participants invited to a voice chat. + + .. versionadded:: 13.4 reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): Inline keyboard attached to the message. ``login_url`` buttons are represented as ordinary url buttons. bot (:class:`telegram.Bot`, optional): The Bot to use for instance methods. @@ -257,6 +277,10 @@ class Message(TelegramObject): group_chat_created (:obj:`bool`): Optional. The group has been created. supergroup_chat_created (:obj:`bool`): Optional. The supergroup has been created. channel_chat_created (:obj:`bool`): Optional. The channel has been created. + message_auto_delete_timer_changed (:class:`telegram.MessageAutoDeleteTimerChanged`): + Optional. Service message: auto-delete timer settings changed in the chat. + + .. versionadded:: 13.4 migrate_to_chat_id (:obj:`int`): Optional. The group has been migrated to a supergroup with the specified identifier. migrate_from_chat_id (:obj:`int`): Optional. The supergroup has been migrated from a group @@ -281,6 +305,18 @@ class Message(TelegramObject): proximity_alert_triggered (:class:`telegram.ProximityAlertTriggered`): Optional. Service message. A user in the chat triggered another user's proximity alert while sharing Live Location. + voice_chat_started (:class:`telegram.VoiceChatStarted`): Optional. Service message: voice + chat started + + .. versionadded:: 13.4 + voice_chat_ended (:class:`telegram.VoiceChatEnded`): Optional. Service message: voice chat + ended. + + .. versionadded:: 13.4 + voice_chat_participants_invited (:class:`telegram.VoiceChatParticipantsInvited`): Optional. + Service message: new participants invited to a voice chat. + + .. versionadded:: 13.4 reply_markup (:class:`telegram.InlineKeyboardMarkup`): Optional. Inline keyboard attached to the message. bot (:class:`telegram.Bot`): Optional. The Bot to use for instance methods. @@ -316,6 +352,7 @@ class Message(TelegramObject): 'group_chat_created', 'supergroup_chat_created', 'channel_chat_created', + 'message_auto_delete_timer_changed', 'migrate_to_chat_id', 'migrate_from_chat_id', 'pinned_message', @@ -323,6 +360,9 @@ class Message(TelegramObject): 'dice', 'passport_data', 'proximity_alert_triggered', + 'voice_chat_started', + 'voice_chat_ended', + 'voice_chat_participants_invited', ] + ATTACHMENT_TYPES def __init__( @@ -379,6 +419,10 @@ class Message(TelegramObject): via_bot: User = None, proximity_alert_triggered: ProximityAlertTriggered = None, sender_chat: Chat = None, + voice_chat_started: VoiceChatStarted = None, + voice_chat_ended: VoiceChatEnded = None, + voice_chat_participants_invited: VoiceChatParticipantsInvited = None, + message_auto_delete_timer_changed: MessageAutoDeleteTimerChanged = None, **_kwargs: Any, ): # Required @@ -418,6 +462,7 @@ class Message(TelegramObject): self.migrate_to_chat_id = migrate_to_chat_id self.migrate_from_chat_id = migrate_from_chat_id self.channel_chat_created = bool(channel_chat_created) + self.message_auto_delete_timer_changed = message_auto_delete_timer_changed self.pinned_message = pinned_message self.forward_from_message_id = forward_from_message_id self.invoice = invoice @@ -433,6 +478,9 @@ class Message(TelegramObject): self.dice = dice self.via_bot = via_bot self.proximity_alert_triggered = proximity_alert_triggered + self.voice_chat_started = voice_chat_started + self.voice_chat_ended = voice_chat_ended + self.voice_chat_participants_invited = voice_chat_participants_invited self.reply_markup = reply_markup self.bot = bot @@ -489,6 +537,9 @@ class Message(TelegramObject): data['new_chat_members'] = User.de_list(data.get('new_chat_members'), bot) data['left_chat_member'] = User.de_json(data.get('left_chat_member'), bot) data['new_chat_photo'] = PhotoSize.de_list(data.get('new_chat_photo'), bot) + data['message_auto_delete_timer_changed'] = MessageAutoDeleteTimerChanged.de_json( + data.get('message_auto_delete_timer_changed'), bot + ) data['pinned_message'] = Message.de_json(data.get('pinned_message'), bot) data['invoice'] = Invoice.de_json(data.get('invoice'), bot) data['successful_payment'] = SuccessfulPayment.de_json(data.get('successful_payment'), bot) @@ -500,7 +551,11 @@ class Message(TelegramObject): data.get('proximity_alert_triggered'), bot ) data['reply_markup'] = InlineKeyboardMarkup.de_json(data.get('reply_markup'), bot) - + data['voice_chat_started'] = VoiceChatStarted.de_json(data.get('voice_chat_started'), bot) + data['voice_chat_ended'] = VoiceChatEnded.de_json(data.get('voice_chat_ended'), bot) + data['voice_chat_participants_invited'] = VoiceChatParticipantsInvited.de_json( + data.get('voice_chat_participants_invited'), bot + ) return cls(bot=bot, **data) @property diff --git a/telegram/messageautodeletetimerchanged.py b/telegram/messageautodeletetimerchanged.py new file mode 100644 index 000000000..842bd162a --- /dev/null +++ b/telegram/messageautodeletetimerchanged.py @@ -0,0 +1,53 @@ +#!/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/]. +"""This module contains an object that represents a change in the Telegram message auto +deletion.""" + +from typing import Any + +from telegram import TelegramObject + + +class MessageAutoDeleteTimerChanged(TelegramObject): + """This object represents a service message about a change in auto-delete timer settings. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`message_auto_delete_time` is equal. + + .. versionadded:: 13.4 + + Args: + message_auto_delete_time (:obj:`int`): New auto-delete time for messages in the + chat. + **kwargs (:obj:`dict`): Arbitrary keyword arguments. + + Attributes: + message_auto_delete_time (:obj:`int`): New auto-delete time for messages in the + chat. + + """ + + def __init__( + self, + message_auto_delete_time: int, + **_kwargs: Any, + ): + self.message_auto_delete_time = int(message_auto_delete_time) + + self._id_attrs = (self.message_auto_delete_time,) diff --git a/telegram/payment/precheckoutquery.py b/telegram/payment/precheckoutquery.py index 60ea90ca0..264a822dc 100644 --- a/telegram/payment/precheckoutquery.py +++ b/telegram/payment/precheckoutquery.py @@ -35,7 +35,7 @@ class PreCheckoutQuery(TelegramObject): considered equal, if their :attr:`id` is equal. Note: - In Python `from` is a reserved word, use `from_user` instead. + In Python ``from`` is a reserved word, use ``from_user`` instead. Args: id (:obj:`str`): Unique query identifier. diff --git a/telegram/payment/shippingquery.py b/telegram/payment/shippingquery.py index df57a6b7a..f9b372302 100644 --- a/telegram/payment/shippingquery.py +++ b/telegram/payment/shippingquery.py @@ -35,7 +35,7 @@ class ShippingQuery(TelegramObject): considered equal, if their :attr:`id` is equal. Note: - In Python `from` is a reserved word, use `from_user` instead. + In Python ``from`` is a reserved word, use ``from_user`` instead. Args: id (:obj:`str`): Unique query identifier. diff --git a/telegram/update.py b/telegram/update.py index bb7cd8f2f..06a5de019 100644 --- a/telegram/update.py +++ b/telegram/update.py @@ -29,6 +29,7 @@ from telegram import ( PreCheckoutQuery, ShippingQuery, TelegramObject, + ChatMemberUpdated, ) from telegram.poll import PollAnswer from telegram.utils.types import JSONDict @@ -74,6 +75,19 @@ class Update(TelegramObject): poll_answer (:class:`telegram.PollAnswer`, optional): A user changed their answer in a non-anonymous poll. Bots receive new votes only in polls that were sent by the bot itself. + my_chat_member (:class:`telegram.ChatMemberUpdated`, optional): The bot's chat member + status was updated in a chat. For private chats, this update is received only when the + bot is blocked or unblocked by the user. + + .. versionadded:: 13.4 + chat_member (:class:`telegram.ChatMemberUpdated`, optional): A chat member's status was + updated in a chat. The bot must be an administrator in the chat and must explicitly + specify ``'chat_member'`` in the list of ``'allowed_updates'`` to receive these + updates (see :meth:`telegram.Bot.get_updates`, :meth:`telegram.Bot.set_webhook`, + :meth:`telegram.ext.Updater.start_polling` and + :meth:`telegram.ext.Updater.start_webhook`). + + .. versionadded:: 13.4 **kwargs (:obj:`dict`): Arbitrary keyword arguments. Attributes: @@ -94,6 +108,19 @@ class Update(TelegramObject): poll_answer (:class:`telegram.PollAnswer`): Optional. A user changed their answer in a non-anonymous poll. Bots receive new votes only in polls that were sent by the bot itself. + my_chat_member (:class:`telegram.ChatMemberUpdated`): Optional. The bot's chat member + status was updated in a chat. For private chats, this update is received only when the + bot is blocked or unblocked by the user. + + .. versionadded:: 13.4 + chat_member (:class:`telegram.ChatMemberUpdated`): Optional. A chat member's status was + updated in a chat. The bot must be an administrator in the chat and must explicitly + specify ``'chat_member'`` in the list of ``'allowed_updates'`` to receive these + updates (see :meth:`telegram.Bot.get_updates`, :meth:`telegram.Bot.set_webhook`, + :meth:`telegram.ext.Updater.start_polling` and + :meth:`telegram.ext.Updater.start_webhook`). + + .. versionadded:: 13.4 """ @@ -111,6 +138,8 @@ class Update(TelegramObject): pre_checkout_query: PreCheckoutQuery = None, poll: Poll = None, poll_answer: PollAnswer = None, + my_chat_member: ChatMemberUpdated = None, + chat_member: ChatMemberUpdated = None, **_kwargs: Any, ): # Required @@ -127,6 +156,8 @@ class Update(TelegramObject): self.edited_channel_post = edited_channel_post self.poll = poll self.poll_answer = poll_answer + self.my_chat_member = my_chat_member + self.chat_member = chat_member self._effective_user: Optional['User'] = None self._effective_chat: Optional['Chat'] = None @@ -170,6 +201,12 @@ class Update(TelegramObject): elif self.poll_answer: user = self.poll_answer.user + elif self.my_chat_member: + user = self.my_chat_member.from_user + + elif self.chat_member: + user = self.chat_member.from_user + self._effective_user = user return user @@ -203,6 +240,12 @@ class Update(TelegramObject): elif self.edited_channel_post: chat = self.edited_channel_post.chat + elif self.my_chat_member: + chat = self.my_chat_member.chat + + elif self.chat_member: + chat = self.chat_member.chat + self._effective_chat = chat return chat @@ -259,5 +302,7 @@ class Update(TelegramObject): data['edited_channel_post'] = Message.de_json(data.get('edited_channel_post'), bot) data['poll'] = Poll.de_json(data.get('poll'), bot) data['poll_answer'] = PollAnswer.de_json(data.get('poll_answer'), bot) + data['my_chat_member'] = ChatMemberUpdated.de_json(data.get('my_chat_member'), bot) + data['chat_member'] = ChatMemberUpdated.de_json(data.get('chat_member'), bot) return cls(**data) diff --git a/telegram/version.py b/telegram/version.py index 5cd507ee1..cd24eb009 100644 --- a/telegram/version.py +++ b/telegram/version.py @@ -18,4 +18,7 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. # pylint: disable=C0114 +from telegram import constants + __version__ = '13.3' +bot_api_version = constants.BOT_API_VERSION # pylint: disable=C0103 diff --git a/telegram/voicechat.py b/telegram/voicechat.py new file mode 100644 index 000000000..1f9d75233 --- /dev/null +++ b/telegram/voicechat.py @@ -0,0 +1,111 @@ +#!/usr/bin/env python +# pylint: disable=R0903 +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-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/]. +"""This module contains objects related to Telegram voice chats.""" + +from typing import TYPE_CHECKING, Any, Optional, List + +from telegram import TelegramObject, User +from telegram.utils.types import JSONDict + +if TYPE_CHECKING: + from telegram import Bot + + +class VoiceChatStarted(TelegramObject): + """ + This object represents a service message about a voice + chat started in the chat. Currently holds no information. + + .. versionadded:: 13.4 + """ + + def __init__(self, **_kwargs: Any): + pass + + +class VoiceChatEnded(TelegramObject): + """ + This object represents a service message about a + voice chat ended in the chat. + + Objects of this class are comparable in terms of equality. + Two objects of this class are considered equal, if their + :attr:`duration` are equal. + + .. versionadded:: 13.4 + + Args: + duration (:obj:`int`): Voice chat duration in seconds. + + Attributes: + duration (:obj:`int`): Voice chat duration in seconds. + + """ + + def __init__(self, duration: int, **_kwargs: Any) -> None: + self.duration = int(duration) if duration is not None else None + self._id_attrs = (self.duration,) + + +class VoiceChatParticipantsInvited(TelegramObject): + """ + This object represents a service message about + new members invited to a voice chat. + + Objects of this class are comparable in terms of equality. + Two objects of this class are considered equal, if their + :attr:`users` are equal. + + .. versionadded:: 13.4 + + Args: + users (List[:class:`telegram.User`]): New members that + were invited to the voice chat. + + Attributes: + users (List[:class:`telegram.User`]): New members that + were invited to the voice chat. + + """ + + def __init__(self, users: List[User], **_kwargs: Any) -> None: + self.users = users + self._id_attrs = (self.users,) + + def __hash__(self) -> int: + return hash(tuple(self.users)) + + @classmethod + def de_json( + cls, data: Optional[JSONDict], bot: 'Bot' + ) -> Optional['VoiceChatParticipantsInvited']: + data = cls.parse_data(data) + + if not data: + return None + + data['users'] = User.de_list(data.get('users', []), bot) + return cls(**data) + + def to_dict(self) -> JSONDict: + data = super().to_dict() + + data["users"] = [u.to_dict() for u in self.users] + return data diff --git a/tests/test_bot.py b/tests/test_bot.py index 48d33e612..f0ee0a500 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -23,6 +23,7 @@ from pathlib import Path from platform import python_implementation import pytest +import pytz from flaky import flaky from telegram import ( @@ -868,13 +869,14 @@ class TestBot: assert bot.token not in resulting_path assert resulting_path == path - # TODO: Needs improvement. No feasable way to test until bots can add members. + # TODO: Needs improvement. No feasible way to test until bots can add members. def test_kick_chat_member(self, monkeypatch, bot): def test(url, data, *args, **kwargs): chat_id = data['chat_id'] == 2 user_id = data['user_id'] == 32 until_date = data.get('until_date', 1577887200) == 1577887200 - return chat_id and user_id and until_date + revoke_msgs = data.get('revoke_messages', True) is True + return chat_id and user_id and until_date and revoke_msgs monkeypatch.setattr(bot.request, 'post', test) until = from_timestamp(1577887200) @@ -882,6 +884,7 @@ class TestBot: 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) def test_kick_chat_member_default_tz(self, monkeypatch, tz_bot): until = dtm.datetime(2020, 1, 11, 16, 13) @@ -1497,7 +1500,7 @@ class TestBot: @flaky(3, 1) @pytest.mark.timeout(10) - def test_promote_chat_member(self, bot, channel_id): + def test_promote_chat_member(self, bot, channel_id, monkeypatch): # TODO: Add bot to supergroup so this can be tested properly / give bot perms with pytest.raises(BadRequest, match='Not enough rights'): assert bot.promote_chat_member( @@ -1512,8 +1515,46 @@ class TestBot: can_restrict_members=True, can_pin_messages=True, can_promote_members=True, + can_manage_chat=True, + can_manage_voice_chats=True, ) + # Test that we pass the correct params to TG + def make_assertion(*args, **_): + data = args[1] + return ( + data.get('chat_id') == channel_id + and data.get('user_id') == 95205500 + and data.get('is_anonymous') == 1 + and data.get('can_change_info') == 2 + and data.get('can_post_messages') == 3 + and data.get('can_edit_messages') == 4 + and data.get('can_delete_messages') == 5 + and data.get('can_invite_users') == 6 + and data.get('can_restrict_members') == 7 + and data.get('can_pin_messages') == 8 + and data.get('can_promote_members') == 9 + and data.get('can_manage_chat') == 10 + and data.get('can_manage_voice_chats') == 11 + ) + + monkeypatch.setattr(bot, '_post', make_assertion) + assert bot.promote_chat_member( + channel_id, + 95205500, + is_anonymous=1, + can_change_info=2, + can_post_messages=3, + can_edit_messages=4, + can_delete_messages=5, + can_invite_users=6, + can_restrict_members=7, + can_pin_messages=8, + can_promote_members=9, + can_manage_chat=10, + can_manage_voice_chats=11, + ) + @flaky(3, 1) @pytest.mark.timeout(10) def test_export_chat_invite_link(self, bot, channel_id): @@ -1522,6 +1563,72 @@ class TestBot: assert isinstance(invite_link, str) assert invite_link != '' + @flaky(3, 1) + @pytest.mark.timeout(10) + @pytest.mark.parametrize('datetime', argvalues=[True, False], ids=['datetime', 'integer']) + def test_advanced_chat_invite_links(self, bot, channel_id, datetime): + # we are testing this all in one function in order to save api calls + timestamp = dtm.datetime.utcnow() + add_seconds = dtm.timedelta(0, 70) + time_in_future = timestamp + add_seconds + expire_time = time_in_future if datetime else to_timestamp(time_in_future) + aware_time_in_future = pytz.UTC.localize(time_in_future) + + invite_link = bot.create_chat_invite_link( + channel_id, expire_date=expire_time, member_limit=10 + ) + assert invite_link.invite_link != '' + assert not invite_link.invite_link.endswith('...') + assert pytest.approx(invite_link.expire_date == aware_time_in_future) + assert invite_link.member_limit == 10 + + add_seconds = dtm.timedelta(0, 80) + time_in_future = timestamp + add_seconds + expire_time = time_in_future if datetime else to_timestamp(time_in_future) + aware_time_in_future = pytz.UTC.localize(time_in_future) + + edited_invite_link = bot.edit_chat_invite_link( + channel_id, invite_link.invite_link, expire_date=expire_time, member_limit=20 + ) + assert edited_invite_link.invite_link == invite_link.invite_link + assert pytest.approx(edited_invite_link.expire_date == aware_time_in_future) + assert edited_invite_link.member_limit == 20 + + revoked_invite_link = bot.revoke_chat_invite_link(channel_id, invite_link.invite_link) + assert revoked_invite_link.invite_link == invite_link.invite_link + assert revoked_invite_link.is_revoked is True + + @flaky(3, 1) + @pytest.mark.timeout(10) + def test_advanced_chat_invite_links_default_tzinfo(self, tz_bot, channel_id): + # we are testing this all in one function in order to save api calls + add_seconds = dtm.timedelta(0, 70) + aware_expire_date = dtm.datetime.now(tz=tz_bot.defaults.tzinfo) + add_seconds + time_in_future = aware_expire_date.replace(tzinfo=None) + + invite_link = tz_bot.create_chat_invite_link( + channel_id, expire_date=time_in_future, member_limit=10 + ) + assert invite_link.invite_link != '' + assert not invite_link.invite_link.endswith('...') + assert pytest.approx(invite_link.expire_date == aware_expire_date) + assert invite_link.member_limit == 10 + + add_seconds = dtm.timedelta(0, 80) + aware_expire_date += add_seconds + time_in_future = aware_expire_date.replace(tzinfo=None) + + edited_invite_link = tz_bot.edit_chat_invite_link( + channel_id, invite_link.invite_link, expire_date=time_in_future, member_limit=20 + ) + assert edited_invite_link.invite_link == invite_link.invite_link + assert pytest.approx(edited_invite_link.expire_date == aware_expire_date) + assert edited_invite_link.member_limit == 20 + + revoked_invite_link = tz_bot.revoke_chat_invite_link(channel_id, invite_link.invite_link) + assert revoked_invite_link.invite_link == invite_link.invite_link + assert revoked_invite_link.is_revoked is True + @flaky(3, 1) @pytest.mark.timeout(10) def test_set_chat_photo(self, bot, channel_id): diff --git a/tests/test_chat.py b/tests/test_chat.py index 5d313cf18..bde3883f7 100644 --- a/tests/test_chat.py +++ b/tests/test_chat.py @@ -37,6 +37,7 @@ def chat(bot): can_set_sticker_set=TestChat.can_set_sticker_set, permissions=TestChat.permissions, slow_mode_delay=TestChat.slow_mode_delay, + message_auto_delete_time=TestChat.message_auto_delete_time, bio=TestChat.bio, linked_chat_id=TestChat.linked_chat_id, location=TestChat.location, @@ -57,6 +58,7 @@ class TestChat: can_invite_users=True, ) slow_mode_delay = 30 + message_auto_delete_time = 42 bio = "I'm a Barbie Girl in a Barbie World" linked_chat_id = 11880 location = ChatLocation(Location(123, 456), 'Barbie World') @@ -72,6 +74,7 @@ class TestChat: 'can_set_sticker_set': self.can_set_sticker_set, 'permissions': self.permissions.to_dict(), 'slow_mode_delay': self.slow_mode_delay, + 'message_auto_delete_time': self.message_auto_delete_time, 'bio': self.bio, 'linked_chat_id': self.linked_chat_id, 'location': self.location.to_dict(), @@ -87,6 +90,7 @@ class TestChat: assert chat.can_set_sticker_set == self.can_set_sticker_set assert chat.permissions == self.permissions assert chat.slow_mode_delay == self.slow_mode_delay + assert chat.message_auto_delete_time == self.message_auto_delete_time assert chat.bio == self.bio assert chat.linked_chat_id == self.linked_chat_id assert chat.location.location == self.location.location @@ -103,6 +107,7 @@ class TestChat: assert chat_dict['all_members_are_administrators'] == chat.all_members_are_administrators assert chat_dict['permissions'] == chat.permissions.to_dict() assert chat_dict['slow_mode_delay'] == chat.slow_mode_delay + assert chat_dict['message_auto_delete_time'] == chat.message_auto_delete_time assert chat_dict['bio'] == chat.bio assert chat_dict['linked_chat_id'] == chat.linked_chat_id assert chat_dict['location'] == chat.location.to_dict() @@ -556,6 +561,62 @@ class TestChat: monkeypatch.setattr(chat.bot, 'copy_message', make_assertion) assert chat.copy_message(chat_id='test_copy', message_id=42) + def test_export_invite_link(self, monkeypatch, chat): + def make_assertion(*_, **kwargs): + return kwargs['chat_id'] == chat.id + + assert check_shortcut_signature( + Chat.export_invite_link, Bot.export_chat_invite_link, ['chat_id'], [] + ) + assert check_shortcut_call(chat.export_invite_link, chat.bot, 'export_chat_invite_link') + assert check_defaults_handling(chat.export_invite_link, chat.bot) + + monkeypatch.setattr(chat.bot, 'export_chat_invite_link', make_assertion) + assert chat.export_invite_link() + + def test_create_invite_link(self, monkeypatch, chat): + def make_assertion(*_, **kwargs): + return kwargs['chat_id'] == chat.id + + assert check_shortcut_signature( + Chat.create_invite_link, Bot.create_chat_invite_link, ['chat_id'], [] + ) + assert check_shortcut_call(chat.create_invite_link, chat.bot, 'create_chat_invite_link') + assert check_defaults_handling(chat.create_invite_link, chat.bot) + + monkeypatch.setattr(chat.bot, 'create_chat_invite_link', make_assertion) + assert chat.create_invite_link() + + def test_edit_invite_link(self, monkeypatch, chat): + link = "ThisIsALink" + + def make_assertion(*_, **kwargs): + return kwargs['chat_id'] == chat.id and kwargs['invite_link'] == link + + assert check_shortcut_signature( + Chat.edit_invite_link, Bot.edit_chat_invite_link, ['chat_id'], [] + ) + assert check_shortcut_call(chat.edit_invite_link, chat.bot, 'edit_chat_invite_link') + assert check_defaults_handling(chat.edit_invite_link, chat.bot) + + monkeypatch.setattr(chat.bot, 'edit_chat_invite_link', make_assertion) + assert chat.edit_invite_link(invite_link=link) + + def test_revoke_invite_link(self, monkeypatch, chat): + link = "ThisIsALink" + + def make_assertion(*_, **kwargs): + return kwargs['chat_id'] == chat.id and kwargs['invite_link'] == link + + assert check_shortcut_signature( + Chat.revoke_invite_link, Bot.revoke_chat_invite_link, ['chat_id'], [] + ) + assert check_shortcut_call(chat.revoke_invite_link, chat.bot, 'revoke_chat_invite_link') + assert check_defaults_handling(chat.revoke_invite_link, chat.bot) + + monkeypatch.setattr(chat.bot, 'revoke_chat_invite_link', make_assertion) + assert chat.revoke_invite_link(invite_link=link) + def test_equality(self): a = Chat(self.id_, self.title, self.type_) b = Chat(self.id_, self.title, self.type_) diff --git a/tests/test_chatinvitelink.py b/tests/test_chatinvitelink.py new file mode 100644 index 000000000..5661deab0 --- /dev/null +++ b/tests/test_chatinvitelink.py @@ -0,0 +1,119 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-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/]. +import datetime + +import pytest + +from telegram import User, ChatInviteLink +from telegram.utils.helpers import to_timestamp + + +@pytest.fixture(scope='class') +def creator(): + return User(1, 'First name', False) + + +@pytest.fixture(scope='class') +def invite_link(creator): + return ChatInviteLink( + TestChatInviteLink.link, + creator, + TestChatInviteLink.primary, + TestChatInviteLink.revoked, + expire_date=TestChatInviteLink.expire_date, + member_limit=TestChatInviteLink.member_limit, + ) + + +class TestChatInviteLink: + + link = "thisialink" + primary = True + revoked = False + expire_date = datetime.datetime.utcnow() + member_limit = 42 + + def test_de_json_required_args(self, bot, creator): + json_dict = { + 'invite_link': self.link, + 'creator': creator.to_dict(), + 'is_primary': self.primary, + 'is_revoked': self.revoked, + } + + invite_link = ChatInviteLink.de_json(json_dict, bot) + + assert invite_link.invite_link == self.link + assert invite_link.creator == creator + assert invite_link.is_primary == self.primary + assert invite_link.is_revoked == self.revoked + + def test_de_json_all_args(self, bot, creator): + json_dict = { + 'invite_link': self.link, + 'creator': creator.to_dict(), + 'is_primary': self.primary, + 'is_revoked': self.revoked, + 'expire_date': to_timestamp(self.expire_date), + 'member_limit': self.member_limit, + } + + invite_link = ChatInviteLink.de_json(json_dict, bot) + + assert invite_link.invite_link == self.link + assert invite_link.creator == creator + assert invite_link.is_primary == self.primary + assert invite_link.is_revoked == self.revoked + assert pytest.approx(invite_link.expire_date == self.expire_date) + assert to_timestamp(invite_link.expire_date) == to_timestamp(self.expire_date) + assert invite_link.member_limit == self.member_limit + + def test_to_dict(self, invite_link): + invite_link_dict = invite_link.to_dict() + assert isinstance(invite_link_dict, dict) + assert invite_link_dict['creator'] == invite_link.creator.to_dict() + assert invite_link_dict['invite_link'] == invite_link.invite_link + assert invite_link_dict['is_primary'] == self.primary + assert invite_link_dict['is_revoked'] == self.revoked + assert invite_link_dict['expire_date'] == to_timestamp(self.expire_date) + assert invite_link_dict['member_limit'] == self.member_limit + + def test_equality(self): + a = ChatInviteLink("link", User(1, '', False), True, True) + b = ChatInviteLink("link", User(1, '', False), True, True) + d = ChatInviteLink("link", User(2, '', False), False, True) + d2 = ChatInviteLink("notalink", User(1, '', False), False, True) + d3 = ChatInviteLink("notalink", User(1, '', False), True, True) + e = User(1, '', False) + + assert a == b + assert hash(a) == hash(b) + assert a is not b + + assert a != d + assert hash(a) != hash(d) + + assert a != d2 + assert hash(a) != hash(d2) + + assert d2 != d3 + assert hash(d2) != hash(d3) + + assert a != e + assert hash(a) != hash(e) diff --git a/tests/test_chatmember.py b/tests/test_chatmember.py index 65c4b1d05..e6e34d0e7 100644 --- a/tests/test_chatmember.py +++ b/tests/test_chatmember.py @@ -69,6 +69,8 @@ class TestChatMember: 'can_send_polls': False, 'can_send_other_messages': True, 'can_add_web_page_previews': False, + 'can_manage_chat': True, + 'can_manage_voice_chats': True, } chat_member = ChatMember.de_json(json_dict, bot) @@ -91,6 +93,8 @@ class TestChatMember: 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_to_dict(self, chat_member): chat_member_dict = chat_member.to_dict() diff --git a/tests/test_chatmemberhandler.py b/tests/test_chatmemberhandler.py new file mode 100644 index 000000000..85459ede6 --- /dev/null +++ b/tests/test_chatmemberhandler.py @@ -0,0 +1,221 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-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/]. +import time +from queue import Queue + +import pytest + +from telegram import ( + Update, + Bot, + Message, + User, + Chat, + CallbackQuery, + ChosenInlineResult, + ShippingQuery, + PreCheckoutQuery, + ChatMemberUpdated, + ChatMember, +) +from telegram.ext import CallbackContext, JobQueue, ChatMemberHandler +from telegram.utils.helpers import from_timestamp + +message = Message(1, None, Chat(1, ''), from_user=User(1, '', False), text='Text') + +params = [ + {'message': message}, + {'edited_message': message}, + {'callback_query': CallbackQuery(1, User(1, '', False), 'chat', message=message)}, + {'channel_post': message}, + {'edited_channel_post': message}, + {'chosen_inline_result': ChosenInlineResult('id', User(1, '', False), '')}, + {'shipping_query': ShippingQuery('id', User(1, '', False), '', None)}, + {'pre_checkout_query': PreCheckoutQuery('id', User(1, '', False), '', 0, '')}, + {'callback_query': CallbackQuery(1, User(1, '', False), 'chat')}, +] + +ids = ( + 'message', + 'edited_message', + 'callback_query', + 'channel_post', + 'edited_channel_post', + 'chosen_inline_result', + 'shipping_query', + 'pre_checkout_query', + 'callback_query_without_message', +) + + +@pytest.fixture(scope='class', params=params, ids=ids) +def false_update(request): + return Update(update_id=2, **request.param) + + +@pytest.fixture(scope='class') +def chat_member_updated(): + return ChatMemberUpdated( + Chat(1, 'chat'), + User(1, '', False), + from_timestamp(int(time.time())), + ChatMember(User(1, '', False), ChatMember.CREATOR), + ChatMember(User(1, '', False), ChatMember.CREATOR), + ) + + +@pytest.fixture(scope='function') +def chat_member(bot, chat_member_updated): + return Update(0, my_chat_member=chat_member_updated) + + +class TestChatMemberHandler: + test_flag = False + + @pytest.fixture(autouse=True) + def reset(self): + self.test_flag = False + + def callback_basic(self, bot, update): + test_bot = isinstance(bot, Bot) + test_update = isinstance(update, Update) + self.test_flag = test_bot and test_update + + def callback_data_1(self, bot, update, user_data=None, chat_data=None): + self.test_flag = (user_data is not None) or (chat_data is not None) + + def callback_data_2(self, bot, update, user_data=None, chat_data=None): + self.test_flag = (user_data is not None) and (chat_data is not None) + + def callback_queue_1(self, bot, update, job_queue=None, update_queue=None): + self.test_flag = (job_queue is not None) or (update_queue is not None) + + def callback_queue_2(self, bot, update, job_queue=None, update_queue=None): + self.test_flag = (job_queue is not None) and (update_queue is not None) + + def callback_context(self, update, context): + self.test_flag = ( + isinstance(context, CallbackContext) + and isinstance(context.bot, Bot) + and isinstance(update, Update) + and isinstance(context.update_queue, Queue) + and isinstance(context.job_queue, JobQueue) + and isinstance(context.user_data, dict) + and isinstance(context.chat_data, dict) + and isinstance(context.bot_data, dict) + and isinstance(update.chat_member or update.my_chat_member, ChatMemberUpdated) + ) + + def test_basic(self, dp, chat_member): + handler = ChatMemberHandler(self.callback_basic) + dp.add_handler(handler) + + assert handler.check_update(chat_member) + + dp.process_update(chat_member) + assert self.test_flag + + @pytest.mark.parametrize( + argnames=['allowed_types', 'expected'], + argvalues=[ + (ChatMemberHandler.MY_CHAT_MEMBER, (True, False)), + (ChatMemberHandler.CHAT_MEMBER, (False, True)), + (ChatMemberHandler.ANY_CHAT_MEMBER, (True, True)), + ], + ids=['MY_CHAT_MEMBER', 'CHAT_MEMBER', 'ANY_CHAT_MEMBER'], + ) + def test_chat_member_types( + self, dp, chat_member_updated, chat_member, expected, allowed_types + ): + result_1, result_2 = expected + + handler = ChatMemberHandler(self.callback_basic, chat_member_types=allowed_types) + dp.add_handler(handler) + + assert handler.check_update(chat_member) == result_1 + dp.process_update(chat_member) + assert self.test_flag == result_1 + + self.test_flag = False + chat_member.my_chat_member = None + chat_member.chat_member = chat_member_updated + + assert handler.check_update(chat_member) == result_2 + dp.process_update(chat_member) + assert self.test_flag == result_2 + + def test_pass_user_or_chat_data(self, dp, chat_member): + handler = ChatMemberHandler(self.callback_data_1, pass_user_data=True) + dp.add_handler(handler) + + dp.process_update(chat_member) + assert self.test_flag + + dp.remove_handler(handler) + handler = ChatMemberHandler(self.callback_data_1, pass_chat_data=True) + dp.add_handler(handler) + + self.test_flag = False + dp.process_update(chat_member) + assert self.test_flag + + dp.remove_handler(handler) + handler = ChatMemberHandler(self.callback_data_2, pass_chat_data=True, pass_user_data=True) + dp.add_handler(handler) + + self.test_flag = False + dp.process_update(chat_member) + assert self.test_flag + + def test_pass_job_or_update_queue(self, dp, chat_member): + handler = ChatMemberHandler(self.callback_queue_1, pass_job_queue=True) + dp.add_handler(handler) + + dp.process_update(chat_member) + assert self.test_flag + + dp.remove_handler(handler) + handler = ChatMemberHandler(self.callback_queue_1, pass_update_queue=True) + dp.add_handler(handler) + + self.test_flag = False + dp.process_update(chat_member) + assert self.test_flag + + dp.remove_handler(handler) + handler = ChatMemberHandler( + self.callback_queue_2, pass_job_queue=True, pass_update_queue=True + ) + dp.add_handler(handler) + + self.test_flag = False + dp.process_update(chat_member) + assert self.test_flag + + def test_other_update_types(self, false_update): + handler = ChatMemberHandler(self.callback_basic) + assert not handler.check_update(false_update) + assert not handler.check_update(True) + + def test_context(self, cdp, chat_member): + handler = ChatMemberHandler(self.callback_context) + cdp.add_handler(handler) + + cdp.process_update(chat_member) + assert self.test_flag diff --git a/tests/test_chatmemberupdated.py b/tests/test_chatmemberupdated.py new file mode 100644 index 000000000..1f3bd28c9 --- /dev/null +++ b/tests/test_chatmemberupdated.py @@ -0,0 +1,177 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-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/]. +import datetime + +import pytest +import pytz + +from telegram import User, ChatMember, Chat, ChatMemberUpdated, ChatInviteLink +from telegram.utils.helpers import to_timestamp + + +@pytest.fixture(scope='class') +def user(): + return User(1, 'First name', False) + + +@pytest.fixture(scope='class') +def chat(): + return Chat(1, Chat.SUPERGROUP, 'Chat') + + +@pytest.fixture(scope='class') +def old_chat_member(user): + return ChatMember(user, TestChatMemberUpdated.old_status) + + +@pytest.fixture(scope='class') +def new_chat_member(user): + return ChatMember(user, TestChatMemberUpdated.new_status) + + +@pytest.fixture(scope='class') +def time(): + return datetime.datetime.now(tz=pytz.utc) + + +@pytest.fixture(scope='class') +def invite_link(user): + return ChatInviteLink('link', user, True, True) + + +@pytest.fixture(scope='class') +def chat_member_updated(user, chat, old_chat_member, new_chat_member, invite_link, time): + return ChatMemberUpdated(chat, user, time, old_chat_member, new_chat_member, invite_link) + + +class TestChatMemberUpdated: + old_status = ChatMember.MEMBER + new_status = ChatMember.ADMINISTRATOR + + def test_de_json_required_args(self, bot, user, chat, old_chat_member, new_chat_member, time): + json_dict = { + 'chat': chat.to_dict(), + 'from': user.to_dict(), + 'date': to_timestamp(time), + 'old_chat_member': old_chat_member.to_dict(), + 'new_chat_member': new_chat_member.to_dict(), + } + + chat_member_updated = ChatMemberUpdated.de_json(json_dict, bot) + + assert chat_member_updated.chat == chat + assert chat_member_updated.from_user == user + assert pytest.approx(chat_member_updated.date == time) + assert to_timestamp(chat_member_updated.date) == to_timestamp(time) + assert chat_member_updated.old_chat_member == old_chat_member + assert chat_member_updated.new_chat_member == new_chat_member + assert chat_member_updated.invite_link is None + + def test_de_json_all_args( + self, bot, user, time, invite_link, chat, old_chat_member, new_chat_member + ): + json_dict = { + 'chat': chat.to_dict(), + 'from': user.to_dict(), + 'date': to_timestamp(time), + 'old_chat_member': old_chat_member.to_dict(), + 'new_chat_member': new_chat_member.to_dict(), + 'invite_link': invite_link.to_dict(), + } + + chat_member_updated = ChatMemberUpdated.de_json(json_dict, bot) + + assert chat_member_updated.chat == chat + assert chat_member_updated.from_user == user + assert pytest.approx(chat_member_updated.date == time) + assert to_timestamp(chat_member_updated.date) == to_timestamp(time) + assert chat_member_updated.old_chat_member == old_chat_member + assert chat_member_updated.new_chat_member == new_chat_member + assert chat_member_updated.invite_link == invite_link + + def test_to_dict(self, chat_member_updated): + chat_member_updated_dict = chat_member_updated.to_dict() + assert isinstance(chat_member_updated_dict, dict) + assert chat_member_updated_dict['chat'] == chat_member_updated.chat.to_dict() + assert chat_member_updated_dict['from'] == chat_member_updated.from_user.to_dict() + assert chat_member_updated_dict['date'] == to_timestamp(chat_member_updated.date) + assert ( + chat_member_updated_dict['old_chat_member'] + == chat_member_updated.old_chat_member.to_dict() + ) + assert ( + chat_member_updated_dict['new_chat_member'] + == chat_member_updated.new_chat_member.to_dict() + ) + assert chat_member_updated_dict['invite_link'] == chat_member_updated.invite_link.to_dict() + + def test_equality(self, time, old_chat_member, new_chat_member, invite_link): + a = ChatMemberUpdated( + Chat(1, 'chat'), + User(1, '', False), + time, + old_chat_member, + new_chat_member, + invite_link, + ) + b = ChatMemberUpdated( + Chat(1, 'chat'), User(1, '', False), time, old_chat_member, new_chat_member + ) + # wrong date + c = ChatMemberUpdated( + Chat(1, 'chat'), + User(1, '', False), + time + datetime.timedelta(hours=1), + old_chat_member, + new_chat_member, + ) + # wrong chat & form_user + d = ChatMemberUpdated( + Chat(42, 'wrong_chat'), + User(42, 'wrong_user', False), + time, + old_chat_member, + new_chat_member, + ) + # wrong old_chat_member + e = ChatMemberUpdated( + Chat(1, 'chat'), + User(1, '', False), + time, + ChatMember(User(1, '', False), ChatMember.CREATOR), + new_chat_member, + ) + # wrong new_chat_member + f = ChatMemberUpdated( + Chat(1, 'chat'), + User(1, '', False), + time, + old_chat_member, + ChatMember(User(1, '', False), ChatMember.CREATOR), + ) + # wrong type + g = ChatMember(User(1, '', False), ChatMember.CREATOR) + + assert a == b + assert hash(a) == hash(b) + assert a is not b + + for other in [c, d, e, f, g]: + assert a != other + assert hash(a) != hash(other) diff --git a/tests/test_filters.py b/tests/test_filters.py index f8d95edad..d337d1ab1 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -44,7 +44,7 @@ def update(): @pytest.fixture(scope='function', params=MessageEntity.ALL_TYPES) def message_entity(request): - return MessageEntity(request.param, 0, 0, url='', user='') + return MessageEntity(request.param, 0, 0, url='', user=User(1, 'first_name', False)) @pytest.fixture( @@ -828,6 +828,11 @@ class TestFilters: assert Filters.status_update.chat_created(update) update.message.channel_chat_created = False + update.message.message_auto_delete_timer_changed = True + assert Filters.status_update(update) + assert Filters.status_update.message_auto_delete_timer_changed(update) + update.message.message_auto_delete_timer_changed = False + update.message.migrate_to_chat_id = 100 assert Filters.status_update(update) assert Filters.status_update.migrate(update) @@ -853,6 +858,21 @@ class TestFilters: assert Filters.status_update.proximity_alert_triggered(update) update.message.proximity_alert_triggered = None + update.message.voice_chat_started = 'hello' + assert Filters.status_update(update) + assert Filters.status_update.voice_chat_started(update) + update.message.voice_chat_started = None + + update.message.voice_chat_ended = 'bye' + assert Filters.status_update(update) + assert Filters.status_update.voice_chat_ended(update) + update.message.voice_chat_ended = None + + update.message.voice_chat_participants_invited = 'invited' + assert Filters.status_update(update) + assert Filters.status_update.voice_chat_participants_invited(update) + update.message.voice_chat_participants_invited = None + def test_filters_forwarded(self, update): assert not Filters.forwarded(update) update.message.forward_date = datetime.datetime.utcnow() @@ -1453,6 +1473,13 @@ class TestFilters: assert not Filters.dice.darts(update) assert not Filters.dice.slot_machine([4])(update) + update.message.dice = Dice(5, '🎳') + assert Filters.dice.bowling(update) + assert Filters.dice.bowling([4, 5])(update) + assert not Filters.dice.dice(update) + assert not Filters.dice.darts(update) + assert not Filters.dice.bowling([4])(update) + def test_language_filter_single(self, update): update.message.from_user.language_code = 'en_US' assert (Filters.language('en_US'))(update) diff --git a/tests/test_message.py b/tests/test_message.py index 2959f81d6..638718d9b 100644 --- a/tests/test_message.py +++ b/tests/test_message.py @@ -48,6 +48,10 @@ from telegram import ( Dice, Bot, ChatAction, + VoiceChatStarted, + VoiceChatEnded, + VoiceChatParticipantsInvited, + MessageAutoDeleteTimerChanged, ) from telegram.ext import Defaults from tests.conftest import check_shortcut_signature, check_shortcut_call, check_defaults_handling @@ -115,6 +119,7 @@ def message(bot): {'group_chat_created': True}, {'supergroup_chat_created': True}, {'channel_chat_created': True}, + {'message_auto_delete_timer_changed': MessageAutoDeleteTimerChanged(42)}, {'migrate_to_chat_id': -12345}, {'migrate_from_chat_id': -54321}, {'pinned_message': Message(7, None, None, None)}, @@ -166,6 +171,13 @@ def message(bot): User(1, 'John', False), User(2, 'Doe', False), 42 ) }, + {'voice_chat_started': VoiceChatStarted()}, + {'voice_chat_ended': VoiceChatEnded(100)}, + { + 'voice_chat_participants_invited': VoiceChatParticipantsInvited( + [User(1, 'Rem', False), User(2, 'Emilia', False)] + ) + }, {'sender_chat': Chat(-123, 'discussion_channel')}, ], ids=[ @@ -195,6 +207,7 @@ def message(bot): 'group_created', 'supergroup_created', 'channel_created', + 'message_auto_delete_timer_changed', 'migrated_to', 'migrated_from', 'pinned', @@ -211,6 +224,9 @@ def message(bot): 'dice', 'via_bot', 'proximity_alert_triggered', + 'voice_chat_started', + 'voice_chat_ended', + 'voice_chat_participants_invited', 'sender_chat', ], ) diff --git a/tests/test_messageautodeletetimerchanged.py b/tests/test_messageautodeletetimerchanged.py new file mode 100644 index 000000000..22b44ba54 --- /dev/null +++ b/tests/test_messageautodeletetimerchanged.py @@ -0,0 +1,51 @@ +#!/usr/bin/env python +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-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 telegram import MessageAutoDeleteTimerChanged, VoiceChatEnded + + +class TestMessageAutoDeleteTimerChanged: + message_auto_delete_time = 100 + + def test_de_json(self): + json_dict = {'message_auto_delete_time': self.message_auto_delete_time} + madtc = MessageAutoDeleteTimerChanged.de_json(json_dict, None) + + assert madtc.message_auto_delete_time == self.message_auto_delete_time + + def test_to_dict(self): + madtc = MessageAutoDeleteTimerChanged(self.message_auto_delete_time) + madtc_dict = madtc.to_dict() + + assert isinstance(madtc_dict, dict) + assert madtc_dict["message_auto_delete_time"] == self.message_auto_delete_time + + def test_equality(self): + a = MessageAutoDeleteTimerChanged(100) + b = MessageAutoDeleteTimerChanged(100) + c = MessageAutoDeleteTimerChanged(50) + d = VoiceChatEnded(25) + + assert a == b + assert hash(a) == hash(b) + + assert a != c + assert hash(a) != hash(c) + + assert a != d + assert hash(a) != hash(d) diff --git a/tests/test_update.py b/tests/test_update.py index 4a929ddfc..103263969 100644 --- a/tests/test_update.py +++ b/tests/test_update.py @@ -16,6 +16,7 @@ # # 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 time import pytest @@ -31,10 +32,20 @@ from telegram import ( PreCheckoutQuery, Poll, PollOption, + ChatMemberUpdated, + ChatMember, ) from telegram.poll import PollAnswer +from telegram.utils.helpers import from_timestamp message = Message(1, None, Chat(1, ''), from_user=User(1, '', False), text='Text') +chat_member_updated = ChatMemberUpdated( + Chat(1, 'chat'), + User(1, '', False), + from_timestamp(int(time.time())), + ChatMember(User(1, '', False), ChatMember.CREATOR), + ChatMember(User(1, '', False), ChatMember.CREATOR), +) params = [ {'message': message}, @@ -49,6 +60,8 @@ params = [ {'callback_query': CallbackQuery(1, User(1, '', False), 'chat')}, {'poll': Poll('id', '?', [PollOption('.', 1)], False, False, False, Poll.REGULAR, True)}, {'poll_answer': PollAnswer("id", User(1, '', False), [1])}, + {'my_chat_member': chat_member_updated}, + {'chat_member': chat_member_updated}, ] all_types = ( @@ -63,6 +76,8 @@ all_types = ( 'pre_checkout_query', 'poll', 'poll_answer', + 'my_chat_member', + 'chat_member', ) ids = all_types + ('callback_query_without_message',) @@ -146,6 +161,8 @@ class TestUpdate: or update.pre_checkout_query is not None or update.poll is not None or update.poll_answer is not None + or update.my_chat_member is not None + or update.chat_member is not None ): assert eff_message.message_id == message.message_id else: diff --git a/tests/test_voicechat.py b/tests/test_voicechat.py new file mode 100644 index 000000000..5f9d87652 --- /dev/null +++ b/tests/test_voicechat.py @@ -0,0 +1,114 @@ +#!/usr/bin/env python +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-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/]. + +import pytest + +from telegram import VoiceChatStarted, VoiceChatEnded, VoiceChatParticipantsInvited, User + + +@pytest.fixture(scope='class') +def user1(): + return User(first_name='Misses Test', id=123, is_bot=False) + + +@pytest.fixture(scope='class') +def user2(): + return User(first_name='Mister Test', id=124, is_bot=False) + + +class TestVoiceChatStarted: + def test_de_json(self): + voice_chat_started = VoiceChatStarted.de_json({}, None) + assert isinstance(voice_chat_started, VoiceChatStarted) + + def test_to_dict(self): + voice_chat_started = VoiceChatStarted() + voice_chat_dict = voice_chat_started.to_dict() + assert voice_chat_dict == {} + + +class TestVoiceChatEnded: + duration = 100 + + def test_de_json(self): + json_dict = {'duration': self.duration} + voice_chat_ended = VoiceChatEnded.de_json(json_dict, None) + + assert voice_chat_ended.duration == self.duration + + def test_to_dict(self): + voice_chat_ended = VoiceChatEnded(self.duration) + voice_chat_dict = voice_chat_ended.to_dict() + + assert isinstance(voice_chat_dict, dict) + assert voice_chat_dict["duration"] == self.duration + + def test_equality(self): + a = VoiceChatEnded(100) + b = VoiceChatEnded(100) + c = VoiceChatEnded(50) + d = VoiceChatStarted() + + assert a == b + assert hash(a) == hash(b) + + assert a != c + assert hash(a) != hash(c) + + assert a != d + assert hash(a) != hash(d) + + +class TestVoiceChatParticipantsInvited: + def test_de_json(self, user1, user2, bot): + json_data = {"users": [user1.to_dict(), user2.to_dict()]} + voice_chat_participants = VoiceChatParticipantsInvited.de_json(json_data, bot) + + assert isinstance(voice_chat_participants.users, list) + assert voice_chat_participants.users[0] == user1 + assert voice_chat_participants.users[1] == user2 + assert voice_chat_participants.users[0].id == user1.id + assert voice_chat_participants.users[1].id == user2.id + + def test_to_dict(self, user1, user2): + voice_chat_participants = VoiceChatParticipantsInvited([user1, user2]) + voice_chat_dict = voice_chat_participants.to_dict() + + assert isinstance(voice_chat_dict, dict) + assert voice_chat_dict["users"] == [user1.to_dict(), user2.to_dict()] + assert voice_chat_dict["users"][0]["id"] == user1.id + assert voice_chat_dict["users"][1]["id"] == user2.id + + def test_equality(self, user1, user2): + a = VoiceChatParticipantsInvited([user1]) + b = VoiceChatParticipantsInvited([user1]) + c = VoiceChatParticipantsInvited([user1, user2]) + d = VoiceChatParticipantsInvited([user2]) + e = VoiceChatStarted() + + 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)