From 02f74d3f688d86eb6d84f497c0e3dbd5f626adb2 Mon Sep 17 00:00:00 2001 From: poolitzer <25934244+Poolitzer@users.noreply.github.com> Date: Fri, 5 Nov 2021 17:33:07 +0100 Subject: [PATCH 01/11] Feat: API 5.4 support --- telegram/__init__.py | 2 + telegram/bot.py | 106 ++++++++++++++++++++++++++++++++++++ telegram/chataction.py | 4 ++ telegram/chatinvitelink.py | 34 +++++++++++- telegram/chatjoinrequest.py | 106 ++++++++++++++++++++++++++++++++++++ telegram/constants.py | 10 ++++ telegram/update.py | 19 +++++++ 7 files changed, 280 insertions(+), 1 deletion(-) create mode 100644 telegram/chatjoinrequest.py diff --git a/telegram/__init__.py b/telegram/__init__.py index 59179e8ae3e..4a9198f9ff4 100644 --- a/telegram/__init__.py +++ b/telegram/__init__.py @@ -25,6 +25,7 @@ from .chat import Chat from .chatlocation import ChatLocation from .chatinvitelink import ChatInviteLink +from .chatjoinrequest import ChatJoinRequest from .chatmember import ( ChatMember, ChatMemberOwner, @@ -194,6 +195,7 @@ 'Chat', 'ChatAction', 'ChatInviteLink', + 'ChatJoinRequest', 'ChatLocation', 'ChatMember', 'ChatMemberOwner', diff --git a/telegram/bot.py b/telegram/bot.py index 63fbd7556d3..63b57088bce 100644 --- a/telegram/bot.py +++ b/telegram/bot.py @@ -3985,6 +3985,8 @@ def create_chat_invite_link( member_limit: int = None, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, + name: str = None, + creates_join_request: bool = None, ) -> ChatInviteLink: """ Use this method to create an additional invite link for a chat. The bot must be an @@ -4007,6 +4009,14 @@ def create_chat_invite_link( the connection pool). api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the Telegram API. + name (:obj:`str`, optional): Invite link name; 0-32 characters. + + .. versionadded:: 13.8 + creates_join_request (:obj:`bool`, optional): :obj:`True`, if users joining the chat + via the link need to be approved by chat administrators. + If :obj:`True`, `member_limit` can't be specified. + + .. versionadded:: 13.8 Returns: :class:`telegram.ChatInviteLink` @@ -4029,6 +4039,12 @@ def create_chat_invite_link( if member_limit is not None: data['member_limit'] = member_limit + if name is not None: + data['name'] = name + + if creates_join_request is not None: + data['creates_join_request'] = creates_join_request + result = self._post('createChatInviteLink', data, timeout=timeout, api_kwargs=api_kwargs) return ChatInviteLink.de_json(result, self) # type: ignore[return-value, arg-type] @@ -4042,6 +4058,8 @@ def edit_chat_invite_link( member_limit: int = None, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, + name: str = None, + creates_join_request: bool = None, ) -> ChatInviteLink: """ Use this method to edit a non-primary invite link created by the bot. The bot must be an @@ -4064,6 +4082,14 @@ def edit_chat_invite_link( the connection pool). api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the Telegram API. + name (:obj:`str`, optional): Invite link name; 0-32 characters. + + .. versionadded:: 13.8 + creates_join_request (:obj:`bool`, optional): :obj:`True`, if users joining the chat + via the link need to be approved by chat administrators. + If :obj:`True`, `member_limit` can't be specified. + + .. versionadded:: 13.8 Returns: :class:`telegram.ChatInviteLink` @@ -4084,6 +4110,12 @@ def edit_chat_invite_link( if member_limit is not None: data['member_limit'] = member_limit + if name is not None: + data['name'] = name + + if creates_join_request is not None: + data['creates_join_request'] = creates_join_request + result = self._post('editChatInviteLink', data, timeout=timeout, api_kwargs=api_kwargs) return ChatInviteLink.de_json(result, self) # type: ignore[return-value, arg-type] @@ -4126,6 +4158,76 @@ def revoke_chat_invite_link( return ChatInviteLink.de_json(result, self) # type: ignore[return-value, arg-type] + @log + def approve_chat_join_request( + self, + chat_id: Union[str, int], + user_id: int, + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> bool: + """Use this method to approve a chat join request. + + The bot must be an administrator in the chat for this to work and must have the + `can_invite_users` administrator right. + + Args: + chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username + of the target 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). + api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the + Telegram API. + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + Raises: + :class:`telegram.error.TelegramError` + """ + data: JSONDict = {'chat_id': chat_id, 'user_id': user_id} + + result = self._post('approveChatJoinRequest', data, timeout=timeout, api_kwargs=api_kwargs) + + return result # type: ignore[return-value] + + @log + def decline_chat_join_request( + self, + chat_id: Union[str, int], + user_id: int, + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> bool: + """Use this method to decline a chat join request. + + The bot must be an administrator in the chat for this to work and must have the + `can_invite_users` administrator right. + + Args: + chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username + of the target 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). + api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the + Telegram API. + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + Raises: + :class:`telegram.error.TelegramError` + """ + data: JSONDict = {'chat_id': chat_id, 'user_id': user_id} + + result = self._post('declineChatJoinRequest', data, timeout=timeout, api_kwargs=api_kwargs) + + return result # type: ignore[return-value] + @log def set_chat_photo( self, @@ -5441,6 +5543,10 @@ def __hash__(self) -> int: """Alias for :attr:`edit_chat_invite_link`""" revokeChatInviteLink = revoke_chat_invite_link """Alias for :attr:`revoke_chat_invite_link`""" + approveChatJoinRequest = approve_chat_join_request + """Alias for :attr:`approve_chat_join_request`""" + declineChatJoinRequest = decline_chat_join_request + """Alias for :attr:`decline_chat_join_request`""" setChatPhoto = set_chat_photo """Alias for :meth:`set_chat_photo`""" deleteChatPhoto = delete_chat_photo diff --git a/telegram/chataction.py b/telegram/chataction.py index c737b810fbc..0e30ce3f086 100644 --- a/telegram/chataction.py +++ b/telegram/chataction.py @@ -59,6 +59,10 @@ class ChatAction: """ UPLOAD_DOCUMENT: ClassVar[str] = constants.CHATACTION_UPLOAD_DOCUMENT """:const:`telegram.constants.CHATACTION_UPLOAD_DOCUMENT`""" + CHOOSE_STICKER: ClassVar[str] = constants.CHATACTION_CHOOSE_STICKER + """:const:`telegram.constants.CHOOSE_STICKER` + + .. versionadded:: 13.8""" UPLOAD_PHOTO: ClassVar[str] = constants.CHATACTION_UPLOAD_PHOTO """:const:`telegram.constants.CHATACTION_UPLOAD_PHOTO`""" UPLOAD_VIDEO: ClassVar[str] = constants.CHATACTION_UPLOAD_VIDEO diff --git a/telegram/chatinvitelink.py b/telegram/chatinvitelink.py index 0755853b007..b25f0876fd7 100644 --- a/telegram/chatinvitelink.py +++ b/telegram/chatinvitelink.py @@ -46,6 +46,17 @@ class ChatInviteLink(TelegramObject): 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. + name (:obj:`str`, optional): Invite link name. + + .. versionadded:: 13.8 + creates_join_request (:obj:`bool`, optional): :obj:`True`, if users joining the chat via + the link need to be approved by chat administrators. + + .. versionadded:: 13.8 + pending_join_request_count (:obj:`int`, optional): Number of pending join requests + created using this link. + + .. versionadded:: 13.8 Attributes: invite_link (:obj:`str`): The invite link. If the link was created by another chat @@ -57,6 +68,17 @@ class ChatInviteLink(TelegramObject): 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. + name (:obj:`str`): Optional. Invite link name. + + .. versionadded:: 13.8 + creates_join_request (:obj:`bool`): Optional. :obj:`True`, if users joining the chat via + the link need to be approved by chat administrators. + + .. versionadded:: 13.8 + pending_join_request_count (:obj:`int`): Optional. Number of pending join requests + created using this link. + + .. versionadded:: 13.8 """ @@ -67,6 +89,9 @@ class ChatInviteLink(TelegramObject): 'is_revoked', 'expire_date', 'member_limit', + 'name', + 'creates_join_request', + 'pending_join_request_count', '_id_attrs', ) @@ -78,6 +103,9 @@ def __init__( is_revoked: bool, expire_date: datetime.datetime = None, member_limit: int = None, + name: str = None, + creates_join_request: bool = None, + pending_join_request_count: int = None, **_kwargs: Any, ): # Required @@ -89,7 +117,11 @@ def __init__( # Optionals self.expire_date = expire_date self.member_limit = int(member_limit) if member_limit is not None else None - + self.name = name + self.creates_join_request = creates_join_request + self.pending_join_request_count = ( + int(pending_join_request_count) if pending_join_request_count is not None else None + ) self._id_attrs = (self.invite_link, self.creator, self.is_primary, self.is_revoked) @classmethod diff --git a/telegram/chatjoinrequest.py b/telegram/chatjoinrequest.py new file mode 100644 index 00000000000..2400ffe02a0 --- /dev/null +++ b/telegram/chatjoinrequest.py @@ -0,0 +1,106 @@ +#!/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 a Telegram ChatJoinRequest.""" +import datetime +from typing import TYPE_CHECKING, Any, Optional + +from telegram import TelegramObject, User, Chat, ChatInviteLink +from telegram.utils.helpers import from_timestamp, to_timestamp +from telegram.utils.types import JSONDict + +if TYPE_CHECKING: + from telegram import Bot + + +class ChatJoinRequest(TelegramObject): + """This object represents a join request sent to a chat. + + 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` and :attr:`date` are equal. + + .. versionadded:: 13.8 + + Args: + chat (:class:`telegram.Chat`): Chat to which the request was sent. + from_user (:class:`telegram.User`): User that sent the join request. + date (:class:`datetime.datetime`): Date the request was sent. + bio (:obj:`str`, optional): Bio of the user. + invite_link (:class:`telegram.ChatInviteLink`, optional): Chat invite link that was used + by the user to send the join request. + + Attributes: + chat (:class:`telegram.Chat`): Chat to which the request was sent. + from_user (:class:`telegram.User`): User that sent the join request. + date (:class:`datetime.datetime`): Date the request was sent. + bio (:obj:`str`): Optional. Bio of the user. + invite_link (:class:`telegram.ChatInviteLink`): Optional. Chat invite link that was used + by the user to send the join request. + + """ + + __slots__ = ( + 'chat', + 'from_user', + 'date', + 'bio', + 'invite_link', + '_id_attrs', + ) + + def __init__( + self, + chat: Chat, + from_user: User, + date: datetime.datetime, + bio: str = None, + invite_link: ChatInviteLink = None, + **_kwargs: Any, + ): + # Required + self.chat = chat + self.from_user = from_user + self.date = date + + # Optionals + self.bio = bio + self.invite_link = invite_link + self._id_attrs = (self.chat, self.from_user, self.date) + + @classmethod + def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['ChatJoinRequest']: + """See :meth:`telegram.TelegramObject.de_json`.""" + 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', None)) + data['invite_link'] = ChatInviteLink.de_json(data.get('invite_link'), bot) + + return cls(**data) + + def to_dict(self) -> JSONDict: + """See :meth:`telegram.TelegramObject.to_dict`.""" + data = super().to_dict() + + data['date'] = to_timestamp(self.date) + + return data diff --git a/telegram/constants.py b/telegram/constants.py index 795f37203c1..ca67b3e9d1f 100644 --- a/telegram/constants.py +++ b/telegram/constants.py @@ -86,6 +86,9 @@ .. versionadded:: 13.5 CHATACTION_UPLOAD_DOCUMENT (:obj:`str`): ``'upload_document'`` + CHATACTION_CHOOSE_STICKER (:obj:`str`): ``'choose_sticker'`` + + .. versionadded:: 13.8 CHATACTION_UPLOAD_PHOTO (:obj:`str`): ``'upload_photo'`` CHATACTION_UPLOAD_VIDEO (:obj:`str`): ``'upload_video'`` CHATACTION_UPLOAD_VIDEO_NOTE (:obj:`str`): ``'upload_video_note'`` @@ -201,9 +204,13 @@ UPDATE_CHAT_MEMBER (:obj:`str`): ``'chat_member'`` .. versionadded:: 13.5 + UPDATE_CHAT_JOIN_REQUEST (:obj:`str`): ``'chat_join_request'`` + + .. versionadded:: 13.8 UPDATE_ALL_TYPES (List[:obj:`str`]): List of all update types. .. versionadded:: 13.5 + .. versionchanged:: 13.8 :class:`telegram.BotCommandScope`: @@ -267,6 +274,7 @@ CHATACTION_UPLOAD_AUDIO: str = 'upload_audio' CHATACTION_UPLOAD_VOICE: str = 'upload_voice' CHATACTION_UPLOAD_DOCUMENT: str = 'upload_document' +CHATACTION_CHOOSE_STICKER: str = 'choose_sticker' CHATACTION_UPLOAD_PHOTO: str = 'upload_photo' CHATACTION_UPLOAD_VIDEO: str = 'upload_video' CHATACTION_UPLOAD_VIDEO_NOTE: str = 'upload_video_note' @@ -353,6 +361,7 @@ UPDATE_POLL_ANSWER = 'poll_answer' UPDATE_MY_CHAT_MEMBER = 'my_chat_member' UPDATE_CHAT_MEMBER = 'chat_member' +UPDATE_CHAT_JOIN_REQUEST = 'chat_join_request' UPDATE_ALL_TYPES = [ UPDATE_MESSAGE, UPDATE_EDITED_MESSAGE, @@ -367,6 +376,7 @@ UPDATE_POLL_ANSWER, UPDATE_MY_CHAT_MEMBER, UPDATE_CHAT_MEMBER, + UPDATE_CHAT_JOIN_REQUEST, ] BOT_COMMAND_SCOPE_DEFAULT = 'default' diff --git a/telegram/update.py b/telegram/update.py index 8497ee213a5..7d0dae69239 100644 --- a/telegram/update.py +++ b/telegram/update.py @@ -31,6 +31,7 @@ TelegramObject, ChatMemberUpdated, constants, + ChatJoinRequest, ) from telegram.poll import PollAnswer from telegram.utils.types import JSONDict @@ -89,6 +90,11 @@ class Update(TelegramObject): :meth:`telegram.ext.Updater.start_webhook`). .. versionadded:: 13.4 + chat_join_request (:class:`telegram.ChatJoinRequest`, optional): A request to join the + chat has been sent. The bot must have the can_invite_users administrator right in + the chat to receive these updates. + + .. versionadded:: 13.8 **kwargs (:obj:`dict`): Arbitrary keyword arguments. Attributes: @@ -122,6 +128,11 @@ class Update(TelegramObject): :meth:`telegram.ext.Updater.start_webhook`). .. versionadded:: 13.4 + chat_join_request (:class:`telegram.ChatJoinRequest`): Optional. A request to join the + chat has been sent. The bot must have the ``'can_invite_users'`` administrator + right in the chat to receive these updates. + + .. versionadded:: 13.8 """ @@ -143,6 +154,7 @@ class Update(TelegramObject): '_effective_message', 'my_chat_member', 'chat_member', + 'chat_join_request', '_id_attrs', ) @@ -198,6 +210,10 @@ class Update(TelegramObject): """:const:`telegram.constants.UPDATE_CHAT_MEMBER` .. versionadded:: 13.5""" + CHAT_JOIN_REQUEST = constants.UPDATE_CHAT_JOIN_REQUEST + """:const:`telegram.constants.UPDATE_CHAT_JOIN_REQUEST` + + .. versionadded:: 13.8""" ALL_TYPES = constants.UPDATE_ALL_TYPES """:const:`telegram.constants.UPDATE_ALL_TYPES` @@ -219,6 +235,7 @@ def __init__( poll_answer: PollAnswer = None, my_chat_member: ChatMemberUpdated = None, chat_member: ChatMemberUpdated = None, + chat_join_request: ChatJoinRequest = None, **_kwargs: Any, ): # Required @@ -237,6 +254,7 @@ def __init__( self.poll_answer = poll_answer self.my_chat_member = my_chat_member self.chat_member = chat_member + self.chat_join_request = chat_join_request self._effective_user: Optional['User'] = None self._effective_chat: Optional['Chat'] = None @@ -384,5 +402,6 @@ def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['Update']: 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) + data['chat_join_request'] = ChatJoinRequest.de_json(data.get('chat_join_request'), bot) return cls(**data) From aa423afc4b72041af17a039c9d7dd76dc414ca9e Mon Sep 17 00:00:00 2001 From: poolitzer <25934244+Poolitzer@users.noreply.github.com> Date: Fri, 5 Nov 2021 17:40:35 +0100 Subject: [PATCH 02/11] Feat: Add shortcuts --- telegram/chat.py | 46 ++++++++++++++++++++++++++++++++++++++++++++++ telegram/user.py | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 92 insertions(+) diff --git a/telegram/chat.py b/telegram/chat.py index 4b5b6c844ff..b29aec4ee78 100644 --- a/telegram/chat.py +++ b/telegram/chat.py @@ -1598,3 +1598,49 @@ def revoke_invite_link( return self.bot.revoke_chat_invite_link( chat_id=self.id, invite_link=invite_link, timeout=timeout, api_kwargs=api_kwargs ) + + def approve_join_request( + self, + user_id: int, + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> bool: + """Shortcut for:: + + bot.approve_chat_join_request(chat_id=update.effective_chat.id, *args, **kwargs) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.approve_chat_join_request`. + + .. versionadded:: 13.8 + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + """ + return self.bot.approve_chat_join_request( + chat_id=self.id, user_id=user_id, timeout=timeout, api_kwargs=api_kwargs + ) + + def decline_join_request( + self, + user_id: int, + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> bool: + """Shortcut for:: + + bot.decline_chat_join_request(chat_id=update.effective_chat.id, *args, **kwargs) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.decline_chat_join_request`. + + .. versionadded:: 13.8 + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + """ + return self.bot.decline_chat_join_request( + chat_id=self.id, user_id=user_id, timeout=timeout, api_kwargs=api_kwargs + ) diff --git a/telegram/user.py b/telegram/user.py index 7949e249e2d..8d160d5d8ec 100644 --- a/telegram/user.py +++ b/telegram/user.py @@ -1140,3 +1140,49 @@ def copy_message( timeout=timeout, api_kwargs=api_kwargs, ) + + def approve_join_request( + self, + chat_id: int, + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> bool: + """Shortcut for:: + + bot.approve_chat_join_request(user_id=update.effective_user.id, *args, **kwargs) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.approve_chat_join_request`. + + .. versionadded:: 13.8 + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + """ + return self.bot.approve_chat_join_request( + user_id=self.id, chat_id=chat_id, timeout=timeout, api_kwargs=api_kwargs + ) + + def decline_join_request( + self, + chat_id: int, + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> bool: + """Shortcut for:: + + bot.decline_chat_join_request(user_id=update.effective_user.id, *args, **kwargs) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.decline_chat_join_request`. + + .. versionadded:: 13.8 + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + """ + return self.bot.decline_chat_join_request( + user_id=self.id, chat_id=chat_id, timeout=timeout, api_kwargs=api_kwargs + ) From e7570a68770ce9b4733394b80e58b78fb6d8a847 Mon Sep 17 00:00:00 2001 From: poolitzer <25934244+Poolitzer@users.noreply.github.com> Date: Fri, 5 Nov 2021 17:46:59 +0100 Subject: [PATCH 03/11] Feat: Add rest of .ext/project stuff --- README.rst | 4 +- README_RAW.rst | 4 +- telegram/chatjoinrequest.py | 2 +- telegram/ext/chatjoinrequest.py | 98 +++++++++++++++++++++++++++++++++ 4 files changed, 103 insertions(+), 5 deletions(-) create mode 100644 telegram/ext/chatjoinrequest.py diff --git a/README.rst b/README.rst index 41ce1c86d94..ee8ebc941ee 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.3-blue?logo=telegram +.. image:: https://img.shields.io/badge/Bot%20API-5.4-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.3** are supported. +All types and methods of the Telegram Bot API **5.4** are supported. ========== Installing diff --git a/README_RAW.rst b/README_RAW.rst index 7a8c8fd5e6d..a76584e8341 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.3-blue?logo=telegram +.. image:: https://img.shields.io/badge/Bot%20API-5.4-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.3** are supported. +All types and methods of the Telegram Bot API **5.4** are supported. ========== Installing diff --git a/telegram/chatjoinrequest.py b/telegram/chatjoinrequest.py index 2400ffe02a0..4c04ce43e5e 100644 --- a/telegram/chatjoinrequest.py +++ b/telegram/chatjoinrequest.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # # A library that provides a Python interface to the Telegram Bot API -# Copyright (C) 2015-2021 +# Copyright (C) 2021 # Leandro Toledo de Souza # # This program is free software: you can redistribute it and/or modify diff --git a/telegram/ext/chatjoinrequest.py b/telegram/ext/chatjoinrequest.py new file mode 100644 index 00000000000..d1e04537367 --- /dev/null +++ b/telegram/ext/chatjoinrequest.py @@ -0,0 +1,98 @@ +#!/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 the ChatJoinRequest class.""" + + +from telegram import Update + +from .handler import Handler +from .utils.types import CCT + + +class ChatJoinRequest(Handler[Update, CCT]): + """Handler class to handle Telegram updates that contain a chat join request. + + 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`. + 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. + 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. + + """ + + __slots__ = () + + 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` + + """ + return isinstance(update, Update) and bool(update.chat_join_request) From e072f26c7612533e82bb88f08c02e0d6b90cd698 Mon Sep 17 00:00:00 2001 From: poolitzer <25934244+Poolitzer@users.noreply.github.com> Date: Fri, 5 Nov 2021 19:59:48 +0100 Subject: [PATCH 04/11] Feat: Add shortcut to chatjoin class Fix: Forgot to add the class to the effective_+ part of Update --- setup.cfg | 2 +- telegram/chatjoinrequest.py | 53 ++++++++++++++++++++++++++++++++++--- telegram/update.py | 6 +++++ 3 files changed, 57 insertions(+), 4 deletions(-) diff --git a/setup.cfg b/setup.cfg index f013075113f..ecfc17fad34 100644 --- a/setup.cfg +++ b/setup.cfg @@ -60,7 +60,7 @@ ignore_errors = True # Disable strict optional for telegram objects with class methods # We don't want to clutter the code with 'if self.bot is None: raise RuntimeError()' -[mypy-telegram.callbackquery,telegram.chat,telegram.message,telegram.user,telegram.files.*,telegram.inline.inlinequery,telegram.payment.precheckoutquery,telegram.payment.shippingquery,telegram.passport.passportdata,telegram.passport.credentials,telegram.passport.passportfile,telegram.ext.filters] +[mypy-telegram.callbackquery,telegram.chat,telegram.message,telegram.user,telegram.files.*,telegram.inline.inlinequery,telegram.payment.precheckoutquery,telegram.payment.shippingquery,telegram.passport.passportdata,telegram.passport.credentials,telegram.passport.passportfile,telegram.ext.filters,telegram.chatjoinrequest] strict_optional = False # type hinting for asyncio in webhookhandler is a bit tricky because it depends on the OS diff --git a/telegram/chatjoinrequest.py b/telegram/chatjoinrequest.py index 4c04ce43e5e..81210c784a9 100644 --- a/telegram/chatjoinrequest.py +++ b/telegram/chatjoinrequest.py @@ -21,8 +21,8 @@ from typing import TYPE_CHECKING, Any, Optional from telegram import TelegramObject, User, Chat, ChatInviteLink -from telegram.utils.helpers import from_timestamp, to_timestamp -from telegram.utils.types import JSONDict +from telegram.utils.helpers import from_timestamp, to_timestamp, DEFAULT_NONE +from telegram.utils.types import JSONDict, ODVInput if TYPE_CHECKING: from telegram import Bot @@ -43,6 +43,7 @@ class ChatJoinRequest(TelegramObject): bio (:obj:`str`, optional): Bio of the user. invite_link (:class:`telegram.ChatInviteLink`, optional): Chat invite link that was used by the user to send the join request. + bot (:class:`telegram.Bot`, optional): The Bot to use for instance methods. Attributes: chat (:class:`telegram.Chat`): Chat to which the request was sent. @@ -60,6 +61,7 @@ class ChatJoinRequest(TelegramObject): 'date', 'bio', 'invite_link', + 'bot', '_id_attrs', ) @@ -70,6 +72,7 @@ def __init__( date: datetime.datetime, bio: str = None, invite_link: ChatInviteLink = None, + bot: 'Bot' = None, **_kwargs: Any, ): # Required @@ -80,6 +83,8 @@ def __init__( # Optionals self.bio = bio self.invite_link = invite_link + + self.bot = bot self._id_attrs = (self.chat, self.from_user, self.date) @classmethod @@ -95,7 +100,7 @@ def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['ChatJoinRequ data['date'] = from_timestamp(data.get('date', None)) data['invite_link'] = ChatInviteLink.de_json(data.get('invite_link'), bot) - return cls(**data) + return cls(bot=bot, **data) def to_dict(self) -> JSONDict: """See :meth:`telegram.TelegramObject.to_dict`.""" @@ -104,3 +109,45 @@ def to_dict(self) -> JSONDict: data['date'] = to_timestamp(self.date) return data + + def approve( + self, + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> bool: + """Shortcut for:: + + bot.approve_chat_join_request(chat_id=update.effective_chat.id, + user_id=update.effective_user.id, *args, **kwargs) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.approve_chat_join_request`. + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + """ + return self.bot.approve_chat_join_request( + chat_id=self.chat.id, user_id=self.from_user.id, timeout=timeout, api_kwargs=api_kwargs + ) + + def decline( + self, + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> bool: + """Shortcut for:: + + bot.decline_chat_join_request(chat_id=update.effective_chat.id, + user_id=update.effective_user.id, *args, **kwargs) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.decline_chat_join_request`. + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + """ + return self.bot.decline_chat_join_request( + chat_id=self.chat.id, user_id=self.from_user.id, timeout=timeout, api_kwargs=api_kwargs + ) diff --git a/telegram/update.py b/telegram/update.py index 7d0dae69239..6b853551f00 100644 --- a/telegram/update.py +++ b/telegram/update.py @@ -304,6 +304,9 @@ def effective_user(self) -> Optional['User']: elif self.chat_member: user = self.chat_member.from_user + elif self.chat_join_request: + user = self.chat_join_request.from_user + self._effective_user = user return user @@ -343,6 +346,9 @@ def effective_chat(self) -> Optional['Chat']: elif self.chat_member: chat = self.chat_member.chat + elif self.chat_join_request: + chat = self.chat_join_request.chat + self._effective_chat = chat return chat From 910470b1977dbb7a33fd4e44fda67dd32fca13b8 Mon Sep 17 00:00:00 2001 From: Bibo-Joshi <22366557+Bibo-Joshi@users.noreply.github.com> Date: Sat, 6 Nov 2021 11:47:58 +0100 Subject: [PATCH 05/11] Apply suggestions from code review --- telegram/bot.py | 4 ++-- telegram/ext/chatjoinrequest.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/telegram/bot.py b/telegram/bot.py index 63b57088bce..5401535967a 100644 --- a/telegram/bot.py +++ b/telegram/bot.py @@ -4169,7 +4169,7 @@ def approve_chat_join_request( """Use this method to approve a chat join request. The bot must be an administrator in the chat for this to work and must have the - `can_invite_users` administrator right. + ``can_invite_users`` administrator right. Args: chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username @@ -4204,7 +4204,7 @@ def decline_chat_join_request( """Use this method to decline a chat join request. The bot must be an administrator in the chat for this to work and must have the - `can_invite_users` administrator right. + ``can_invite_users`` administrator right. Args: chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username diff --git a/telegram/ext/chatjoinrequest.py b/telegram/ext/chatjoinrequest.py index d1e04537367..998037ac156 100644 --- a/telegram/ext/chatjoinrequest.py +++ b/telegram/ext/chatjoinrequest.py @@ -25,7 +25,7 @@ from .utils.types import CCT -class ChatJoinRequest(Handler[Update, CCT]): +class ChatJoinRequestHandler(Handler[Update, CCT]): """Handler class to handle Telegram updates that contain a chat join request. Note: From 5bcfb89a58c0da2f8a3b87bb9e045bbcca2ec6b2 Mon Sep 17 00:00:00 2001 From: Hinrich Mahler <22366557+Bibo-Joshi@users.noreply.github.com> Date: Sat, 6 Nov 2021 15:00:50 +0100 Subject: [PATCH 06/11] Update shortcuts to create/edit_invite link --- telegram/chat.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/telegram/chat.py b/telegram/chat.py index b29aec4ee78..33c6a03dd70 100644 --- a/telegram/chat.py +++ b/telegram/chat.py @@ -1524,6 +1524,8 @@ def create_invite_link( member_limit: int = None, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, + name: str = None, + creates_join_request: bool = None, ) -> 'ChatInviteLink': """Shortcut for:: @@ -1534,6 +1536,10 @@ def create_invite_link( .. versionadded:: 13.4 + .. versionchanged:: 13.8 + Edited signature according to the changes of + :meth:`telegram.Bot.create_chat_invite_link`. + Returns: :class:`telegram.ChatInviteLink` @@ -1544,6 +1550,8 @@ def create_invite_link( member_limit=member_limit, timeout=timeout, api_kwargs=api_kwargs, + name=name, + creates_join_request=creates_join_request, ) def edit_invite_link( @@ -1553,6 +1561,8 @@ def edit_invite_link( member_limit: int = None, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, + name: str = None, + creates_join_request: bool = None, ) -> 'ChatInviteLink': """Shortcut for:: @@ -1563,6 +1573,9 @@ def edit_invite_link( .. versionadded:: 13.4 + .. versionchanged:: 13.8 + Edited signature according to the changes of :meth:`telegram.Bot.edit_chat_invite_link`. + Returns: :class:`telegram.ChatInviteLink` @@ -1574,6 +1587,8 @@ def edit_invite_link( member_limit=member_limit, timeout=timeout, api_kwargs=api_kwargs, + name=name, + creates_join_request=creates_join_request, ) def revoke_invite_link( From 10f9240ca39d4621ffd1c9498f8e3ea2ca4e1866 Mon Sep 17 00:00:00 2001 From: Hinrich Mahler <22366557+Bibo-Joshi@users.noreply.github.com> Date: Sat, 6 Nov 2021 18:09:31 +0100 Subject: [PATCH 07/11] Update shortcuts to create/edit_invite link --- telegram/bot.py | 4 +- telegram/ext/__init__.py | 2 + ...inrequest.py => chatjoinrequesthandler.py} | 0 telegram/user.py | 4 +- tests/test_bot.py | 80 ++++++- tests/test_chat.py | 30 +++ tests/test_chatinvitelink.py | 12 +- tests/test_chatjoinrequest.py | 158 +++++++++++++ tests/test_chatjoinrequesthandler.py | 219 ++++++++++++++++++ tests/test_update.py | 22 +- tests/test_user.py | 34 +++ 11 files changed, 555 insertions(+), 10 deletions(-) rename telegram/ext/{chatjoinrequest.py => chatjoinrequesthandler.py} (100%) create mode 100644 tests/test_chatjoinrequest.py create mode 100644 tests/test_chatjoinrequesthandler.py diff --git a/telegram/bot.py b/telegram/bot.py index 5401535967a..dca00c33c9c 100644 --- a/telegram/bot.py +++ b/telegram/bot.py @@ -4014,7 +4014,7 @@ def create_chat_invite_link( .. versionadded:: 13.8 creates_join_request (:obj:`bool`, optional): :obj:`True`, if users joining the chat via the link need to be approved by chat administrators. - If :obj:`True`, `member_limit` can't be specified. + If :obj:`True`, ``member_limit`` can't be specified. .. versionadded:: 13.8 @@ -4087,7 +4087,7 @@ def edit_chat_invite_link( .. versionadded:: 13.8 creates_join_request (:obj:`bool`, optional): :obj:`True`, if users joining the chat via the link need to be approved by chat administrators. - If :obj:`True`, `member_limit` can't be specified. + If :obj:`True`, ``member_limit`` can't be specified. .. versionadded:: 13.8 diff --git a/telegram/ext/__init__.py b/telegram/ext/__init__.py index 731ad2c9e49..da3c4894178 100644 --- a/telegram/ext/__init__.py +++ b/telegram/ext/__init__.py @@ -59,6 +59,7 @@ from .pollanswerhandler import PollAnswerHandler from .pollhandler import PollHandler from .chatmemberhandler import ChatMemberHandler +from .chatjoinrequesthandler import ChatJoinRequestHandler from .defaults import Defaults from .callbackdatacache import CallbackDataCache, InvalidCallbackData @@ -68,6 +69,7 @@ 'CallbackContext', 'CallbackDataCache', 'CallbackQueryHandler', + 'ChatJoinRequestHandler', 'ChatMemberHandler', 'ChosenInlineResultHandler', 'CommandHandler', diff --git a/telegram/ext/chatjoinrequest.py b/telegram/ext/chatjoinrequesthandler.py similarity index 100% rename from telegram/ext/chatjoinrequest.py rename to telegram/ext/chatjoinrequesthandler.py diff --git a/telegram/user.py b/telegram/user.py index 8d160d5d8ec..2f7f962ac8c 100644 --- a/telegram/user.py +++ b/telegram/user.py @@ -1143,7 +1143,7 @@ def copy_message( def approve_join_request( self, - chat_id: int, + chat_id: Union[int, str], timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, ) -> bool: @@ -1166,7 +1166,7 @@ def approve_join_request( def decline_join_request( self, - chat_id: int, + chat_id: Union[int, str], timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, ) -> bool: diff --git a/tests/test_bot.py b/tests/test_bot.py index 002c49488ed..96dbd18d818 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -718,6 +718,7 @@ def test_send_dice_default_allow_sending_without_reply(self, default_bot, chat_i ChatAction.UPLOAD_VIDEO, ChatAction.UPLOAD_VIDEO_NOTE, ChatAction.UPLOAD_VOICE, + ChatAction.CHOOSE_STICKER, ], ) def test_send_chat_action(self, bot, chat_id, chat_action): @@ -1696,6 +1697,27 @@ def test_export_chat_invite_link(self, bot, channel_id): assert isinstance(invite_link, str) assert invite_link != '' + @flaky(3, 1) + @pytest.mark.parametrize('creates_join_request', [True, False]) + @pytest.mark.parametrize('name', [None, 'name']) + def test_create_chat_invite_link_basics(self, bot, creates_join_request, name, channel_id): + data = {} + if creates_join_request: + data['creates_join_request'] = True + if name: + data['name'] = name + invite_link = bot.create_chat_invite_link(chat_id=channel_id, **data) + + assert invite_link.member_limit is None + assert invite_link.expire_date is None + assert invite_link.creates_join_request == creates_join_request + assert invite_link.name == name + + revoked_link = bot.revoke_chat_invite_link( + chat_id=channel_id, invite_link=invite_link.invite_link + ) + assert revoked_link.is_revoked is True + @flaky(3, 1) @pytest.mark.parametrize('datetime', argvalues=[True, False], ids=['datetime', 'integer']) def test_advanced_chat_invite_links(self, bot, channel_id, datetime): @@ -1720,11 +1742,29 @@ def test_advanced_chat_invite_links(self, bot, channel_id, datetime): 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 + channel_id, + invite_link.invite_link, + expire_date=expire_time, + member_limit=20, + name='NewName', ) 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 + assert edited_invite_link.name == 'NewName' + # TODO: reactivate test after https://github.com/tdlib/telegram-bot-api/issues/196 is fixed + # assert edited_invite_link.member_limit == 20 + + edited_invite_link = bot.edit_chat_invite_link( + channel_id, + invite_link.invite_link, + creates_join_request=True, + ) + assert edited_invite_link.invite_link == invite_link.invite_link + assert pytest.approx(edited_invite_link.expire_date == aware_time_in_future) + # TODO: activate tests after https://github.com/tdlib/telegram-bot-api/issues/196 is fixed + # assert edited_invite_link.name == 'NewName' + # assert edited_invite_link.creates_join_request is True + # assert edited_invite_link.member_limit is None revoked_invite_link = bot.revoke_chat_invite_link(channel_id, invite_link.invite_link) assert revoked_invite_link.invite_link == invite_link.invite_link @@ -1750,16 +1790,48 @@ def test_advanced_chat_invite_links_default_tzinfo(self, tz_bot, channel_id): 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 + channel_id, + invite_link.invite_link, + expire_date=time_in_future, + member_limit=20, + name='NewName', ) 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 + assert edited_invite_link.name == 'NewName' + # TODO: reactivate test after https://github.com/tdlib/telegram-bot-api/issues/196 is fixed + # assert edited_invite_link.member_limit == 20 + + edited_invite_link = tz_bot.edit_chat_invite_link( + channel_id, + invite_link.invite_link, + creates_join_request=True, + ) + assert edited_invite_link.invite_link == invite_link.invite_link + assert pytest.approx(edited_invite_link.expire_date == aware_expire_date) + # TODO: activate tests after https://github.com/tdlib/telegram-bot-api/issues/196 is fixed + # assert edited_invite_link.name == 'NewName' + # assert edited_invite_link.creates_join_request is True + # assert edited_invite_link.member_limit is None 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) + def test_approve_chat_join_request(self, bot, chat_id, channel_id): + # Since we can't create join requests on the fly, we just tests the call to TG + # by checking that it complains about approving a user who is already in the chat + with pytest.raises(BadRequest, match='User_already_participant'): + bot.approve_chat_join_request(chat_id=channel_id, user_id=chat_id) + + @flaky(3, 1) + def test_decline_chat_join_request(self, bot, chat_id, channel_id): + # Since we can't create join requests on the fly, we just tests the call to TG + # by checking that it complains about declining a user who is already in the chat + with pytest.raises(BadRequest, match='User_already_participant'): + bot.decline_chat_join_request(chat_id=channel_id, user_id=chat_id) + @flaky(3, 1) def test_set_chat_photo(self, bot, channel_id): def func(): diff --git a/tests/test_chat.py b/tests/test_chat.py index a60956c485e..7ff7aa39286 100644 --- a/tests/test_chat.py +++ b/tests/test_chat.py @@ -636,6 +636,36 @@ def make_assertion(*_, **kwargs): monkeypatch.setattr(chat.bot, 'revoke_chat_invite_link', make_assertion) assert chat.revoke_invite_link(invite_link=link) + def test_approve_join_request(self, monkeypatch, chat): + def make_assertion(*_, **kwargs): + return kwargs['chat_id'] == chat.id and kwargs['user_id'] == 42 + + assert check_shortcut_signature( + Chat.approve_join_request, Bot.approve_chat_join_request, ['chat_id'], [] + ) + assert check_shortcut_call( + chat.approve_join_request, chat.bot, 'approve_chat_join_request' + ) + assert check_defaults_handling(chat.approve_join_request, chat.bot) + + monkeypatch.setattr(chat.bot, 'approve_chat_join_request', make_assertion) + assert chat.approve_join_request(user_id=42) + + def test_decline_join_request(self, monkeypatch, chat): + def make_assertion(*_, **kwargs): + return kwargs['chat_id'] == chat.id and kwargs['user_id'] == 42 + + assert check_shortcut_signature( + Chat.decline_join_request, Bot.decline_chat_join_request, ['chat_id'], [] + ) + assert check_shortcut_call( + chat.decline_join_request, chat.bot, 'decline_chat_join_request' + ) + assert check_defaults_handling(chat.decline_join_request, chat.bot) + + monkeypatch.setattr(chat.bot, 'decline_chat_join_request', make_assertion) + assert chat.decline_join_request(user_id=42) + 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 index 8b4fcadfd5a..6d18c2cb640 100644 --- a/tests/test_chatinvitelink.py +++ b/tests/test_chatinvitelink.py @@ -38,6 +38,8 @@ def invite_link(creator): TestChatInviteLink.revoked, expire_date=TestChatInviteLink.expire_date, member_limit=TestChatInviteLink.member_limit, + name=TestChatInviteLink.name, + pending_join_request_count=TestChatInviteLink.pending_join_request_count, ) @@ -48,6 +50,8 @@ class TestChatInviteLink: revoked = False expire_date = datetime.datetime.utcnow() member_limit = 42 + name = 'LinkName' + pending_join_request_count = 42 def test_slot_behaviour(self, recwarn, mro_slots, invite_link): for attr in invite_link.__slots__: @@ -79,7 +83,9 @@ def test_de_json_all_args(self, bot, creator): 'is_primary': self.primary, 'is_revoked': self.revoked, 'expire_date': to_timestamp(self.expire_date), - 'member_limit': self.member_limit, + 'member_limit': str(self.member_limit), + 'name': self.name, + 'pending_join_request_count': str(self.pending_join_request_count), } invite_link = ChatInviteLink.de_json(json_dict, bot) @@ -91,6 +97,8 @@ def test_de_json_all_args(self, bot, creator): 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 + assert invite_link.name == self.name + assert invite_link.pending_join_request_count == self.pending_join_request_count def test_to_dict(self, invite_link): invite_link_dict = invite_link.to_dict() @@ -101,6 +109,8 @@ def test_to_dict(self, invite_link): 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 + assert invite_link_dict['name'] == self.name + assert invite_link_dict['pending_join_request_count'] == self.pending_join_request_count def test_equality(self): a = ChatInviteLink("link", User(1, '', False), True, True) diff --git a/tests/test_chatjoinrequest.py b/tests/test_chatjoinrequest.py new file mode 100644 index 00000000000..a187b22efc2 --- /dev/null +++ b/tests/test_chatjoinrequest.py @@ -0,0 +1,158 @@ +#!/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/]. +import datetime + +import pytest +import pytz + +from telegram import ChatJoinRequest, User, Chat, ChatInviteLink, Bot +from telegram.utils.helpers import to_timestamp +from tests.conftest import check_shortcut_signature, check_shortcut_call, check_defaults_handling + + +@pytest.fixture(scope='class') +def time(): + return datetime.datetime.now(tz=pytz.utc) + + +@pytest.fixture(scope='class') +def chat_join_request(bot, time): + return ChatJoinRequest( + chat=TestChatJoinRequest.chat, + from_user=TestChatJoinRequest.from_user, + date=time, + bio=TestChatJoinRequest.bio, + invite_link=TestChatJoinRequest.invite_link, + bot=bot, + ) + + +class TestChatJoinRequest: + chat = Chat(1, Chat.SUPERGROUP) + from_user = User(2, 'first_name', False) + bio = 'bio' + invite_link = ChatInviteLink( + 'https://invite.link', + User(42, 'creator', False), + name='InviteLink', + is_revoked=False, + is_primary=False, + ) + + def test_slot_behaviour(self, chat_join_request, recwarn, mro_slots): + inst = chat_join_request + for attr in inst.__slots__: + assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert not inst.__dict__, f"got missing slot(s): {inst.__dict__}" + assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" + inst.custom, inst.bio = 'should give warning', self.bio + assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list + + def test_de_json(self, bot, time): + json_dict = { + 'chat': self.chat.to_dict(), + 'from': self.from_user.to_dict(), + 'date': to_timestamp(time), + } + chat_join_request = ChatJoinRequest.de_json(json_dict, bot) + + assert chat_join_request.chat == self.chat + assert chat_join_request.from_user == self.from_user + assert pytest.approx(chat_join_request.date == time) + assert to_timestamp(chat_join_request.date) == to_timestamp(time) + + json_dict.update({'bio': self.bio, 'invite_link': self.invite_link.to_dict()}) + chat_join_request = ChatJoinRequest.de_json(json_dict, bot) + + assert chat_join_request.chat == self.chat + assert chat_join_request.from_user == self.from_user + assert pytest.approx(chat_join_request.date == time) + assert to_timestamp(chat_join_request.date) == to_timestamp(time) + assert chat_join_request.bio == self.bio + assert chat_join_request.invite_link == self.invite_link + + def test_to_dict(self, chat_join_request, time): + chat_join_request_dict = chat_join_request.to_dict() + + assert isinstance(chat_join_request_dict, dict) + assert chat_join_request_dict['chat'] == chat_join_request.chat.to_dict() + assert chat_join_request_dict['from'] == chat_join_request.from_user.to_dict() + assert chat_join_request_dict['date'] == to_timestamp(chat_join_request.date) + assert chat_join_request_dict['bio'] == chat_join_request.bio + assert chat_join_request_dict['invite_link'] == chat_join_request.invite_link.to_dict() + + def test_equality(self, chat_join_request, time): + a = chat_join_request + b = ChatJoinRequest(self.chat, self.from_user, time) + c = ChatJoinRequest(self.chat, self.from_user, time, bio='bio') + d = ChatJoinRequest(self.chat, self.from_user, time + datetime.timedelta(1)) + e = ChatJoinRequest(self.chat, User(-1, 'last_name', True), time) + f = User(456, '', False) + + assert a == b + assert hash(a) == hash(b) + assert a is not b + + assert a == c + assert hash(a) == hash(c) + + assert a != d + assert hash(a) != hash(d) + + assert a != e + assert hash(a) != hash(e) + + assert a != f + assert hash(a) != hash(f) + + def test_approve(self, monkeypatch, chat_join_request): + def make_assertion(*_, **kwargs): + chat_id_test = kwargs['chat_id'] == chat_join_request.chat.id + user_id_test = kwargs['user_id'] == chat_join_request.from_user.id + + return chat_id_test and user_id_test + + assert check_shortcut_signature( + ChatJoinRequest.approve, Bot.approve_chat_join_request, ['chat_id', 'user_id'], [] + ) + assert check_shortcut_call( + chat_join_request.approve, chat_join_request.bot, 'approve_chat_join_request' + ) + assert check_defaults_handling(chat_join_request.approve, chat_join_request.bot) + + monkeypatch.setattr(chat_join_request.bot, 'approve_chat_join_request', make_assertion) + assert chat_join_request.approve() + + def test_decline(self, monkeypatch, chat_join_request): + def make_assertion(*_, **kwargs): + chat_id_test = kwargs['chat_id'] == chat_join_request.chat.id + user_id_test = kwargs['user_id'] == chat_join_request.from_user.id + + return chat_id_test and user_id_test + + assert check_shortcut_signature( + ChatJoinRequest.decline, Bot.decline_chat_join_request, ['chat_id', 'user_id'], [] + ) + assert check_shortcut_call( + chat_join_request.decline, chat_join_request.bot, 'decline_chat_join_request' + ) + assert check_defaults_handling(chat_join_request.decline, chat_join_request.bot) + + monkeypatch.setattr(chat_join_request.bot, 'decline_chat_join_request', make_assertion) + assert chat_join_request.decline() diff --git a/tests/test_chatjoinrequesthandler.py b/tests/test_chatjoinrequesthandler.py new file mode 100644 index 00000000000..dcd7a75743c --- /dev/null +++ b/tests/test_chatjoinrequesthandler.py @@ -0,0 +1,219 @@ +#!/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 +from queue import Queue + +import pytest +import pytz + +from telegram import ( + Update, + Bot, + Message, + User, + Chat, + CallbackQuery, + ChosenInlineResult, + ShippingQuery, + PreCheckoutQuery, + ChatJoinRequest, + ChatInviteLink, +) +from telegram.ext import CallbackContext, JobQueue, ChatJoinRequestHandler + + +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 time(): + return datetime.datetime.now(tz=pytz.utc) + + +@pytest.fixture(scope='class') +def chat_join_request(time, bot): + return ChatJoinRequest( + chat=Chat(1, Chat.SUPERGROUP), + from_user=User(2, 'first_name', False), + date=time, + bio='bio', + invite_link=ChatInviteLink( + 'https://invite.link', + User(42, 'creator', False), + name='InviteLink', + is_revoked=False, + is_primary=False, + ), + bot=bot, + ) + + +@pytest.fixture(scope='function') +def chat_join_request_update(bot, chat_join_request): + return Update(0, chat_join_request=chat_join_request) + + +class TestChatJoinRequestHandler: + test_flag = False + + def test_slot_behaviour(self, recwarn, mro_slots): + action = ChatJoinRequestHandler(self.callback_basic) + for attr in action.__slots__: + assert getattr(action, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert not action.__dict__, f"got missing slot(s): {action.__dict__}" + assert len(mro_slots(action)) == len(set(mro_slots(action))), "duplicate slot" + action.custom = 'should give warning' + assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list + + @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_join_request, + ChatJoinRequest, + ) + ) + + def test_basic(self, dp, chat_join_request_update): + handler = ChatJoinRequestHandler(self.callback_basic) + dp.add_handler(handler) + + assert handler.check_update(chat_join_request_update) + + dp.process_update(chat_join_request_update) + assert self.test_flag + + def test_pass_user_or_chat_data(self, dp, chat_join_request_update): + handler = ChatJoinRequestHandler(self.callback_data_1, pass_user_data=True) + dp.add_handler(handler) + + dp.process_update(chat_join_request_update) + assert self.test_flag + + dp.remove_handler(handler) + handler = ChatJoinRequestHandler(self.callback_data_1, pass_chat_data=True) + dp.add_handler(handler) + + self.test_flag = False + dp.process_update(chat_join_request_update) + assert self.test_flag + + dp.remove_handler(handler) + handler = ChatJoinRequestHandler( + self.callback_data_2, pass_chat_data=True, pass_user_data=True + ) + dp.add_handler(handler) + + self.test_flag = False + dp.process_update(chat_join_request_update) + assert self.test_flag + + def test_pass_job_or_update_queue(self, dp, chat_join_request_update): + handler = ChatJoinRequestHandler(self.callback_queue_1, pass_job_queue=True) + dp.add_handler(handler) + + dp.process_update(chat_join_request_update) + assert self.test_flag + + dp.remove_handler(handler) + handler = ChatJoinRequestHandler(self.callback_queue_1, pass_update_queue=True) + dp.add_handler(handler) + + self.test_flag = False + dp.process_update(chat_join_request_update) + assert self.test_flag + + dp.remove_handler(handler) + handler = ChatJoinRequestHandler( + self.callback_queue_2, pass_job_queue=True, pass_update_queue=True + ) + dp.add_handler(handler) + + self.test_flag = False + dp.process_update(chat_join_request_update) + assert self.test_flag + + def test_other_update_types(self, false_update): + handler = ChatJoinRequestHandler(self.callback_basic) + assert not handler.check_update(false_update) + assert not handler.check_update(True) + + def test_context(self, cdp, chat_join_request_update): + handler = ChatJoinRequestHandler(callback=self.callback_context) + cdp.add_handler(handler) + + cdp.process_update(chat_join_request_update) + assert self.test_flag diff --git a/tests/test_update.py b/tests/test_update.py index 2777ff00893..b19502fd2ea 100644 --- a/tests/test_update.py +++ b/tests/test_update.py @@ -34,6 +34,7 @@ PollOption, ChatMemberUpdated, ChatMember, + ChatJoinRequest, ) from telegram.poll import PollAnswer from telegram.utils.helpers import from_timestamp @@ -47,6 +48,14 @@ ChatMember(User(1, '', False), ChatMember.CREATOR), ) + +chat_join_request = ChatJoinRequest( + chat=Chat(1, Chat.SUPERGROUP), + from_user=User(1, 'first_name', False), + date=from_timestamp(int(time.time())), + bio='bio', +) + params = [ {'message': message}, {'edited_message': message}, @@ -57,11 +66,13 @@ {'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')}, {'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}, + {'chat_join_request': chat_join_request}, + # Must be last to conform with `ids` below! + {'callback_query': CallbackQuery(1, User(1, '', False), 'chat')}, ] all_types = ( @@ -78,6 +89,7 @@ 'poll_answer', 'my_chat_member', 'chat_member', + 'chat_join_request', ) ids = all_types + ('callback_query_without_message',) @@ -101,6 +113,9 @@ def test_slot_behaviour(self, update, recwarn, mro_slots): @pytest.mark.parametrize('paramdict', argvalues=params, ids=ids) def test_de_json(self, bot, paramdict): + if 'chat_join_request' not in paramdict: + return + json_dict = {'update_id': TestUpdate.update_id} # Convert the single update 'item' to a dict of that item and apply it to the json_dict json_dict.update({k: v.to_dict() for k, v in paramdict.items()}) @@ -113,6 +128,10 @@ def test_de_json(self, bot, paramdict): for _type in all_types: if getattr(update, _type) is not None: i += 1 + print() + print(update) + print(type(getattr(update, _type)), getattr(update, _type)) + print(type(paramdict[_type]), paramdict[_type]) assert getattr(update, _type) == paramdict[_type] assert i == 1 @@ -171,6 +190,7 @@ def test_effective_message(self, update): or update.poll_answer is not None or update.my_chat_member is not None or update.chat_member is not None + or update.chat_join_request is not None ): assert eff_message.message_id == message.message_id else: diff --git a/tests/test_user.py b/tests/test_user.py index 85f75bb8b59..dc801957158 100644 --- a/tests/test_user.py +++ b/tests/test_user.py @@ -430,6 +430,40 @@ def make_assertion(*_, **kwargs): monkeypatch.setattr(user.bot, 'copy_message', make_assertion) assert user.copy_message(chat_id='chat_id', message_id='message_id') + def test_instance_method_approve_join_request(self, monkeypatch, user): + def make_assertion(*_, **kwargs): + chat_id = kwargs['chat_id'] == 'chat_id' + user_id = kwargs['user_id'] == user.id + return chat_id and user_id + + assert check_shortcut_signature( + User.approve_join_request, Bot.approve_chat_join_request, ['user_id'], [] + ) + assert check_shortcut_call( + user.approve_join_request, user.bot, 'approve_chat_join_request' + ) + assert check_defaults_handling(user.approve_join_request, user.bot) + + monkeypatch.setattr(user.bot, 'approve_chat_join_request', make_assertion) + assert user.approve_join_request(chat_id='chat_id') + + def test_instance_method_decline_join_request(self, monkeypatch, user): + def make_assertion(*_, **kwargs): + chat_id = kwargs['chat_id'] == 'chat_id' + user_id = kwargs['user_id'] == user.id + return chat_id and user_id + + assert check_shortcut_signature( + User.decline_join_request, Bot.decline_chat_join_request, ['user_id'], [] + ) + assert check_shortcut_call( + user.decline_join_request, user.bot, 'decline_chat_join_request' + ) + assert check_defaults_handling(user.decline_join_request, user.bot) + + monkeypatch.setattr(user.bot, 'decline_chat_join_request', make_assertion) + assert user.decline_join_request(chat_id='chat_id') + def test_mention_html(self, user): expected = '{}' From 86b4948585948ea4f609bcc74d5859acb78f0a62 Mon Sep 17 00:00:00 2001 From: Hinrich Mahler <22366557+Bibo-Joshi@users.noreply.github.com> Date: Sat, 6 Nov 2021 18:14:39 +0100 Subject: [PATCH 08/11] bump api version --- telegram/constants.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/telegram/constants.py b/telegram/constants.py index ca67b3e9d1f..9179074821b 100644 --- a/telegram/constants.py +++ b/telegram/constants.py @@ -240,7 +240,7 @@ """ from typing import List -BOT_API_VERSION: str = '5.3' +BOT_API_VERSION: str = '5.4' MAX_MESSAGE_LENGTH: int = 4096 MAX_CAPTION_LENGTH: int = 1024 ANONYMOUS_ADMIN_ID: int = 1087968824 From eec551d74b36fe297ceefc42266dbd90a700f719 Mon Sep 17 00:00:00 2001 From: Hinrich Mahler <22366557+Bibo-Joshi@users.noreply.github.com> Date: Sun, 7 Nov 2021 10:03:24 +0100 Subject: [PATCH 09/11] Review and update tests --- telegram/bot.py | 26 +++++++++++++------ telegram/ext/chatjoinrequesthandler.py | 4 ++- telegram/update.py | 9 ++++--- tests/test_bot.py | 36 ++++++++++++++++---------- tests/test_update.py | 7 ----- 5 files changed, 50 insertions(+), 32 deletions(-) diff --git a/telegram/bot.py b/telegram/bot.py index dca00c33c9c..65dbdb8a5ec 100644 --- a/telegram/bot.py +++ b/telegram/bot.py @@ -4025,6 +4025,11 @@ def create_chat_invite_link( :class:`telegram.error.TelegramError` """ + if creates_join_request and member_limit: + raise ValueError( + "If `creates_join_request` is `True`, `member_limit` can't be specified." + ) + data: JSONDict = { 'chat_id': chat_id, } @@ -4098,6 +4103,11 @@ def edit_chat_invite_link( :class:`telegram.error.TelegramError` """ + if creates_join_request and member_limit: + raise ValueError( + "If `creates_join_request` is `True`, `member_limit` can't be specified." + ) + data: JSONDict = {'chat_id': chat_id, 'invite_link': invite_link} if expire_date is not None: @@ -4169,7 +4179,7 @@ def approve_chat_join_request( """Use this method to approve a chat join request. The bot must be an administrator in the chat for this to work and must have the - ``can_invite_users`` administrator right. + :attr:`telegram.ChatPermissions.can_invite_users` administrator right. Args: chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username @@ -4201,10 +4211,10 @@ def decline_chat_join_request( timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None, ) -> bool: - """Use this method to decline a chat join request. + """Use this method to decline a chat join request. The bot must be an administrator in the chat for this to work and must have the - ``can_invite_users`` administrator right. + :attr:`telegram.ChatPermissions.can_invite_users` administrator right. Args: chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username @@ -5538,15 +5548,15 @@ def __hash__(self) -> int: exportChatInviteLink = export_chat_invite_link """Alias for :meth:`export_chat_invite_link`""" createChatInviteLink = create_chat_invite_link - """Alias for :attr:`create_chat_invite_link`""" + """Alias for :meth:`create_chat_invite_link`""" editChatInviteLink = edit_chat_invite_link - """Alias for :attr:`edit_chat_invite_link`""" + """Alias for :meth:`edit_chat_invite_link`""" revokeChatInviteLink = revoke_chat_invite_link - """Alias for :attr:`revoke_chat_invite_link`""" + """Alias for :meth:`revoke_chat_invite_link`""" approveChatJoinRequest = approve_chat_join_request - """Alias for :attr:`approve_chat_join_request`""" + """Alias for :meth:`approve_chat_join_request`""" declineChatJoinRequest = decline_chat_join_request - """Alias for :attr:`decline_chat_join_request`""" + """Alias for :meth:`decline_chat_join_request`""" setChatPhoto = set_chat_photo """Alias for :meth:`set_chat_photo`""" deleteChatPhoto = delete_chat_photo diff --git a/telegram/ext/chatjoinrequesthandler.py b/telegram/ext/chatjoinrequesthandler.py index 998037ac156..693b73e47e3 100644 --- a/telegram/ext/chatjoinrequesthandler.py +++ b/telegram/ext/chatjoinrequesthandler.py @@ -16,7 +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/]. -"""This module contains the ChatJoinRequest class.""" +"""This module contains the ChatJoinRequestHandler class.""" from telegram import Update @@ -41,6 +41,8 @@ class ChatJoinRequestHandler(Handler[Update, CCT]): 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. + .. versionadded:: 13.8 + 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. diff --git a/telegram/update.py b/telegram/update.py index 6b853551f00..5518bc4aaa0 100644 --- a/telegram/update.py +++ b/telegram/update.py @@ -91,8 +91,9 @@ class Update(TelegramObject): .. versionadded:: 13.4 chat_join_request (:class:`telegram.ChatJoinRequest`, optional): A request to join the - chat has been sent. The bot must have the can_invite_users administrator right in - the chat to receive these updates. + chat has been sent. The bot must have the + :attr:`telegram.ChatPermissions.can_invite_users` administrator right in the chat to + receive these updates. .. versionadded:: 13.8 **kwargs (:obj:`dict`): Arbitrary keyword arguments. @@ -359,7 +360,9 @@ def effective_message(self) -> Optional[Message]: update this is. Will be :obj:`None` for :attr:`inline_query`, :attr:`chosen_inline_result`, :attr:`callback_query` from inline messages, :attr:`shipping_query`, :attr:`pre_checkout_query`, :attr:`poll`, - :attr:`poll_answer`, :attr:`my_chat_member` and :attr:`chat_member`. + :attr:`poll_answer`, :attr:`my_chat_member`, :attr:`chat_member` as well as + :attr:`chat_join_request` in case the bot is missing the + :attr:`telegram.ChatPermissions.can_invite_users` administrator right in the chat. """ if self._effective_message: diff --git a/tests/test_bot.py b/tests/test_bot.py index 96dbd18d818..11eb44e91df 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -1697,6 +1697,16 @@ def test_export_chat_invite_link(self, bot, channel_id): assert isinstance(invite_link, str) assert invite_link != '' + def test_create_edit_invite_link_mutually_exclusive_arguments(self, bot, channel_id): + data = {'chat_id': channel_id, 'member_limit': 17, 'creates_join_request': True} + + with pytest.raises(ValueError, match="`member_limit` can't be specified"): + bot.create_chat_invite_link(**data) + + data.update({'invite_link': 'https://invite.link'}) + with pytest.raises(ValueError, match="`member_limit` can't be specified"): + bot.edit_chat_invite_link(**data) + @flaky(3, 1) @pytest.mark.parametrize('creates_join_request', [True, False]) @pytest.mark.parametrize('name', [None, 'name']) @@ -1716,7 +1726,7 @@ def test_create_chat_invite_link_basics(self, bot, creates_join_request, name, c revoked_link = bot.revoke_chat_invite_link( chat_id=channel_id, invite_link=invite_link.invite_link ) - assert revoked_link.is_revoked is True + assert revoked_link.is_revoked @flaky(3, 1) @pytest.mark.parametrize('datetime', argvalues=[True, False], ids=['datetime', 'integer']) @@ -1751,20 +1761,19 @@ def test_advanced_chat_invite_links(self, bot, channel_id, datetime): 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.name == 'NewName' - # TODO: reactivate test after https://github.com/tdlib/telegram-bot-api/issues/196 is fixed - # assert edited_invite_link.member_limit == 20 + assert edited_invite_link.member_limit == 20 edited_invite_link = bot.edit_chat_invite_link( channel_id, invite_link.invite_link, + name='EvenNewerName', creates_join_request=True, ) assert edited_invite_link.invite_link == invite_link.invite_link assert pytest.approx(edited_invite_link.expire_date == aware_time_in_future) - # TODO: activate tests after https://github.com/tdlib/telegram-bot-api/issues/196 is fixed - # assert edited_invite_link.name == 'NewName' - # assert edited_invite_link.creates_join_request is True - # assert edited_invite_link.member_limit is None + assert edited_invite_link.name == 'EvenNewerName' + assert edited_invite_link.creates_join_request is True + assert edited_invite_link.member_limit is None revoked_invite_link = bot.revoke_chat_invite_link(channel_id, invite_link.invite_link) assert revoked_invite_link.invite_link == invite_link.invite_link @@ -1799,20 +1808,19 @@ def test_advanced_chat_invite_links_default_tzinfo(self, tz_bot, channel_id): 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.name == 'NewName' - # TODO: reactivate test after https://github.com/tdlib/telegram-bot-api/issues/196 is fixed - # assert edited_invite_link.member_limit == 20 + assert edited_invite_link.member_limit == 20 edited_invite_link = tz_bot.edit_chat_invite_link( channel_id, invite_link.invite_link, + name='EvenNewerName', creates_join_request=True, ) assert edited_invite_link.invite_link == invite_link.invite_link assert pytest.approx(edited_invite_link.expire_date == aware_expire_date) - # TODO: activate tests after https://github.com/tdlib/telegram-bot-api/issues/196 is fixed - # assert edited_invite_link.name == 'NewName' - # assert edited_invite_link.creates_join_request is True - # assert edited_invite_link.member_limit is None + assert edited_invite_link.name == 'EvenNewerName' + assert edited_invite_link.creates_join_request is True + assert edited_invite_link.member_limit is None 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 @@ -1820,6 +1828,7 @@ def test_advanced_chat_invite_links_default_tzinfo(self, tz_bot, channel_id): @flaky(3, 1) def test_approve_chat_join_request(self, bot, chat_id, channel_id): + # TODO: Need incoming join request to properly test # Since we can't create join requests on the fly, we just tests the call to TG # by checking that it complains about approving a user who is already in the chat with pytest.raises(BadRequest, match='User_already_participant'): @@ -1827,6 +1836,7 @@ def test_approve_chat_join_request(self, bot, chat_id, channel_id): @flaky(3, 1) def test_decline_chat_join_request(self, bot, chat_id, channel_id): + # TODO: Need incoming join request to properly test # Since we can't create join requests on the fly, we just tests the call to TG # by checking that it complains about declining a user who is already in the chat with pytest.raises(BadRequest, match='User_already_participant'): diff --git a/tests/test_update.py b/tests/test_update.py index b19502fd2ea..12d55662339 100644 --- a/tests/test_update.py +++ b/tests/test_update.py @@ -113,9 +113,6 @@ def test_slot_behaviour(self, update, recwarn, mro_slots): @pytest.mark.parametrize('paramdict', argvalues=params, ids=ids) def test_de_json(self, bot, paramdict): - if 'chat_join_request' not in paramdict: - return - json_dict = {'update_id': TestUpdate.update_id} # Convert the single update 'item' to a dict of that item and apply it to the json_dict json_dict.update({k: v.to_dict() for k, v in paramdict.items()}) @@ -128,10 +125,6 @@ def test_de_json(self, bot, paramdict): for _type in all_types: if getattr(update, _type) is not None: i += 1 - print() - print(update) - print(type(getattr(update, _type)), getattr(update, _type)) - print(type(paramdict[_type]), paramdict[_type]) assert getattr(update, _type) == paramdict[_type] assert i == 1 From cf525969d53e2ec2dc6e3515b9275b52b6dbef3b Mon Sep 17 00:00:00 2001 From: Hinrich Mahler <22366557+Bibo-Joshi@users.noreply.github.com> Date: Sun, 7 Nov 2021 12:05:30 +0100 Subject: [PATCH 10/11] More versioning directives --- telegram/bot.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/telegram/bot.py b/telegram/bot.py index 65dbdb8a5ec..c9a3eb4c2c1 100644 --- a/telegram/bot.py +++ b/telegram/bot.py @@ -4181,6 +4181,8 @@ def approve_chat_join_request( The bot must be an administrator in the chat for this to work and must have the :attr:`telegram.ChatPermissions.can_invite_users` administrator right. + .. versionadded:: 13.8 + Args: chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username of the target channel (in the format ``@channelusername``). @@ -4216,6 +4218,8 @@ def decline_chat_join_request( The bot must be an administrator in the chat for this to work and must have the :attr:`telegram.ChatPermissions.can_invite_users` administrator right. + .. versionadded:: 13.8 + Args: chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username of the target channel (in the format ``@channelusername``). From b1b353d817bd80fca7add0349c8162cab1006031 Mon Sep 17 00:00:00 2001 From: Hinrich Mahler <22366557+Bibo-Joshi@users.noreply.github.com> Date: Mon, 8 Nov 2021 18:45:37 +0100 Subject: [PATCH 11/11] Add a note on edit_chat_invite_link regarding optional args --- telegram/bot.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/telegram/bot.py b/telegram/bot.py index c9a3eb4c2c1..5051482a1f3 100644 --- a/telegram/bot.py +++ b/telegram/bot.py @@ -4070,6 +4070,12 @@ def edit_chat_invite_link( 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. + Note: + Though not stated explicitly in the official docs, Telegram changes not only the + optional parameters that are explicitly passed, but also replaces all other optional + parameters to the default values. However, since not documented, this behaviour may + change unbeknown to PTB. + .. versionadded:: 13.4 Args: pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy