Co-authored-by: poolitzer <25934244+Poolitzer@users.noreply.github.com>
This commit is contained in:
Bibo-Joshi 2021-11-08 19:02:20 +01:00 committed by GitHub
parent bc7c422a11
commit e4dc80f41d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 1125 additions and 15 deletions

View file

@ -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/ :target: https://pypi.org/project/python-telegram-bot/
:alt: Supported Python versions :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 :target: https://core.telegram.org/bots/api-changelog
:alt: Supported Bot API versions :alt: Supported Bot API versions
@ -111,7 +111,7 @@ Installing both ``python-telegram-bot`` and ``python-telegram-bot-raw`` in conju
Telegram API support 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 Installing

View file

@ -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/ :target: https://pypi.org/project/python-telegram-bot-raw/
:alt: Supported Python versions :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 :target: https://core.telegram.org/bots/api-changelog
:alt: Supported Bot API versions :alt: Supported Bot API versions
@ -105,7 +105,7 @@ Installing both ``python-telegram-bot`` and ``python-telegram-bot-raw`` in conju
Telegram API support 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 Installing

View file

@ -60,7 +60,7 @@ ignore_errors = True
# Disable strict optional for telegram objects with class methods # 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()' # 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 strict_optional = False
# type hinting for asyncio in webhookhandler is a bit tricky because it depends on the OS # type hinting for asyncio in webhookhandler is a bit tricky because it depends on the OS

View file

@ -25,6 +25,7 @@ from .files.chatphoto import ChatPhoto
from .chat import Chat from .chat import Chat
from .chatlocation import ChatLocation from .chatlocation import ChatLocation
from .chatinvitelink import ChatInviteLink from .chatinvitelink import ChatInviteLink
from .chatjoinrequest import ChatJoinRequest
from .chatmember import ( from .chatmember import (
ChatMember, ChatMember,
ChatMemberOwner, ChatMemberOwner,
@ -194,6 +195,7 @@ __all__ = ( # Keep this alphabetically ordered
'Chat', 'Chat',
'ChatAction', 'ChatAction',
'ChatInviteLink', 'ChatInviteLink',
'ChatJoinRequest',
'ChatLocation', 'ChatLocation',
'ChatMember', 'ChatMember',
'ChatMemberOwner', 'ChatMemberOwner',

View file

@ -3985,6 +3985,8 @@ class Bot(TelegramObject):
member_limit: int = None, member_limit: int = None,
timeout: ODVInput[float] = DEFAULT_NONE, timeout: ODVInput[float] = DEFAULT_NONE,
api_kwargs: JSONDict = None, api_kwargs: JSONDict = None,
name: str = None,
creates_join_request: bool = None,
) -> ChatInviteLink: ) -> ChatInviteLink:
""" """
Use this method to create an additional invite link for a chat. The bot must be an Use this method to create an additional invite link for a chat. The bot must be an
@ -4007,6 +4009,14 @@ class Bot(TelegramObject):
the connection pool). the connection pool).
api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the
Telegram API. 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: Returns:
:class:`telegram.ChatInviteLink` :class:`telegram.ChatInviteLink`
@ -4015,6 +4025,11 @@ class Bot(TelegramObject):
:class:`telegram.error.TelegramError` :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 = { data: JSONDict = {
'chat_id': chat_id, 'chat_id': chat_id,
} }
@ -4029,6 +4044,12 @@ class Bot(TelegramObject):
if member_limit is not None: if member_limit is not None:
data['member_limit'] = member_limit 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) result = self._post('createChatInviteLink', data, timeout=timeout, api_kwargs=api_kwargs)
return ChatInviteLink.de_json(result, self) # type: ignore[return-value, arg-type] return ChatInviteLink.de_json(result, self) # type: ignore[return-value, arg-type]
@ -4042,11 +4063,19 @@ class Bot(TelegramObject):
member_limit: int = None, member_limit: int = None,
timeout: ODVInput[float] = DEFAULT_NONE, timeout: ODVInput[float] = DEFAULT_NONE,
api_kwargs: JSONDict = None, api_kwargs: JSONDict = None,
name: str = None,
creates_join_request: bool = None,
) -> ChatInviteLink: ) -> ChatInviteLink:
""" """
Use this method to edit a non-primary invite link created by the bot. The bot must be an 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. 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 .. versionadded:: 13.4
Args: Args:
@ -4064,6 +4093,14 @@ class Bot(TelegramObject):
the connection pool). the connection pool).
api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the
Telegram API. 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: Returns:
:class:`telegram.ChatInviteLink` :class:`telegram.ChatInviteLink`
@ -4072,6 +4109,11 @@ class Bot(TelegramObject):
:class:`telegram.error.TelegramError` :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} data: JSONDict = {'chat_id': chat_id, 'invite_link': invite_link}
if expire_date is not None: if expire_date is not None:
@ -4084,6 +4126,12 @@ class Bot(TelegramObject):
if member_limit is not None: if member_limit is not None:
data['member_limit'] = member_limit 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) result = self._post('editChatInviteLink', data, timeout=timeout, api_kwargs=api_kwargs)
return ChatInviteLink.de_json(result, self) # type: ignore[return-value, arg-type] return ChatInviteLink.de_json(result, self) # type: ignore[return-value, arg-type]
@ -4126,6 +4174,80 @@ class Bot(TelegramObject):
return ChatInviteLink.de_json(result, self) # type: ignore[return-value, arg-type] 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
: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``).
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
: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``).
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 @log
def set_chat_photo( def set_chat_photo(
self, self,
@ -5436,11 +5558,15 @@ class Bot(TelegramObject):
exportChatInviteLink = export_chat_invite_link exportChatInviteLink = export_chat_invite_link
"""Alias for :meth:`export_chat_invite_link`""" """Alias for :meth:`export_chat_invite_link`"""
createChatInviteLink = create_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 editChatInviteLink = edit_chat_invite_link
"""Alias for :attr:`edit_chat_invite_link`""" """Alias for :meth:`edit_chat_invite_link`"""
revokeChatInviteLink = revoke_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 :meth:`approve_chat_join_request`"""
declineChatJoinRequest = decline_chat_join_request
"""Alias for :meth:`decline_chat_join_request`"""
setChatPhoto = set_chat_photo setChatPhoto = set_chat_photo
"""Alias for :meth:`set_chat_photo`""" """Alias for :meth:`set_chat_photo`"""
deleteChatPhoto = delete_chat_photo deleteChatPhoto = delete_chat_photo

View file

@ -1524,6 +1524,8 @@ class Chat(TelegramObject):
member_limit: int = None, member_limit: int = None,
timeout: ODVInput[float] = DEFAULT_NONE, timeout: ODVInput[float] = DEFAULT_NONE,
api_kwargs: JSONDict = None, api_kwargs: JSONDict = None,
name: str = None,
creates_join_request: bool = None,
) -> 'ChatInviteLink': ) -> 'ChatInviteLink':
"""Shortcut for:: """Shortcut for::
@ -1534,6 +1536,10 @@ class Chat(TelegramObject):
.. versionadded:: 13.4 .. versionadded:: 13.4
.. versionchanged:: 13.8
Edited signature according to the changes of
:meth:`telegram.Bot.create_chat_invite_link`.
Returns: Returns:
:class:`telegram.ChatInviteLink` :class:`telegram.ChatInviteLink`
@ -1544,6 +1550,8 @@ class Chat(TelegramObject):
member_limit=member_limit, member_limit=member_limit,
timeout=timeout, timeout=timeout,
api_kwargs=api_kwargs, api_kwargs=api_kwargs,
name=name,
creates_join_request=creates_join_request,
) )
def edit_invite_link( def edit_invite_link(
@ -1553,6 +1561,8 @@ class Chat(TelegramObject):
member_limit: int = None, member_limit: int = None,
timeout: ODVInput[float] = DEFAULT_NONE, timeout: ODVInput[float] = DEFAULT_NONE,
api_kwargs: JSONDict = None, api_kwargs: JSONDict = None,
name: str = None,
creates_join_request: bool = None,
) -> 'ChatInviteLink': ) -> 'ChatInviteLink':
"""Shortcut for:: """Shortcut for::
@ -1563,6 +1573,9 @@ class Chat(TelegramObject):
.. versionadded:: 13.4 .. versionadded:: 13.4
.. versionchanged:: 13.8
Edited signature according to the changes of :meth:`telegram.Bot.edit_chat_invite_link`.
Returns: Returns:
:class:`telegram.ChatInviteLink` :class:`telegram.ChatInviteLink`
@ -1574,6 +1587,8 @@ class Chat(TelegramObject):
member_limit=member_limit, member_limit=member_limit,
timeout=timeout, timeout=timeout,
api_kwargs=api_kwargs, api_kwargs=api_kwargs,
name=name,
creates_join_request=creates_join_request,
) )
def revoke_invite_link( def revoke_invite_link(
@ -1598,3 +1613,49 @@ class Chat(TelegramObject):
return self.bot.revoke_chat_invite_link( return self.bot.revoke_chat_invite_link(
chat_id=self.id, invite_link=invite_link, timeout=timeout, api_kwargs=api_kwargs 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
)

View file

@ -59,6 +59,10 @@ class ChatAction:
""" """
UPLOAD_DOCUMENT: ClassVar[str] = constants.CHATACTION_UPLOAD_DOCUMENT UPLOAD_DOCUMENT: ClassVar[str] = constants.CHATACTION_UPLOAD_DOCUMENT
""":const:`telegram.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 UPLOAD_PHOTO: ClassVar[str] = constants.CHATACTION_UPLOAD_PHOTO
""":const:`telegram.constants.CHATACTION_UPLOAD_PHOTO`""" """:const:`telegram.constants.CHATACTION_UPLOAD_PHOTO`"""
UPLOAD_VIDEO: ClassVar[str] = constants.CHATACTION_UPLOAD_VIDEO UPLOAD_VIDEO: ClassVar[str] = constants.CHATACTION_UPLOAD_VIDEO

View file

@ -46,6 +46,17 @@ class ChatInviteLink(TelegramObject):
has been expired. has been expired.
member_limit (:obj:`int`, optional): Maximum number of users that can be members of the 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. 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: Attributes:
invite_link (:obj:`str`): The invite link. If the link was created by another chat 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. has been expired.
member_limit (:obj:`int`): Optional. Maximum number of users that can be members 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. 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', 'is_revoked',
'expire_date', 'expire_date',
'member_limit', 'member_limit',
'name',
'creates_join_request',
'pending_join_request_count',
'_id_attrs', '_id_attrs',
) )
@ -78,6 +103,9 @@ class ChatInviteLink(TelegramObject):
is_revoked: bool, is_revoked: bool,
expire_date: datetime.datetime = None, expire_date: datetime.datetime = None,
member_limit: int = None, member_limit: int = None,
name: str = None,
creates_join_request: bool = None,
pending_join_request_count: int = None,
**_kwargs: Any, **_kwargs: Any,
): ):
# Required # Required
@ -89,7 +117,11 @@ class ChatInviteLink(TelegramObject):
# Optionals # Optionals
self.expire_date = expire_date self.expire_date = expire_date
self.member_limit = int(member_limit) if member_limit is not None else None 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) self._id_attrs = (self.invite_link, self.creator, self.is_primary, self.is_revoked)
@classmethod @classmethod

153
telegram/chatjoinrequest.py Normal file
View file

@ -0,0 +1,153 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
# Copyright (C) 2021
# Leandro Toledo de Souza <devs@python-telegram-bot.org>
#
# 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, DEFAULT_NONE
from telegram.utils.types import JSONDict, ODVInput
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.
bot (:class:`telegram.Bot`, optional): The Bot to use for instance methods.
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',
'bot',
'_id_attrs',
)
def __init__(
self,
chat: Chat,
from_user: User,
date: datetime.datetime,
bio: str = None,
invite_link: ChatInviteLink = None,
bot: 'Bot' = 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.bot = bot
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(bot=bot, **data)
def to_dict(self) -> JSONDict:
"""See :meth:`telegram.TelegramObject.to_dict`."""
data = super().to_dict()
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
)

View file

@ -86,6 +86,9 @@ Attributes:
.. versionadded:: 13.5 .. versionadded:: 13.5
CHATACTION_UPLOAD_DOCUMENT (:obj:`str`): ``'upload_document'`` 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_PHOTO (:obj:`str`): ``'upload_photo'``
CHATACTION_UPLOAD_VIDEO (:obj:`str`): ``'upload_video'`` CHATACTION_UPLOAD_VIDEO (:obj:`str`): ``'upload_video'``
CHATACTION_UPLOAD_VIDEO_NOTE (:obj:`str`): ``'upload_video_note'`` CHATACTION_UPLOAD_VIDEO_NOTE (:obj:`str`): ``'upload_video_note'``
@ -201,9 +204,13 @@ Attributes:
UPDATE_CHAT_MEMBER (:obj:`str`): ``'chat_member'`` UPDATE_CHAT_MEMBER (:obj:`str`): ``'chat_member'``
.. versionadded:: 13.5 .. 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. UPDATE_ALL_TYPES (List[:obj:`str`]): List of all update types.
.. versionadded:: 13.5 .. versionadded:: 13.5
.. versionchanged:: 13.8
:class:`telegram.BotCommandScope`: :class:`telegram.BotCommandScope`:
@ -233,7 +240,7 @@ Attributes:
""" """
from typing import List from typing import List
BOT_API_VERSION: str = '5.3' BOT_API_VERSION: str = '5.4'
MAX_MESSAGE_LENGTH: int = 4096 MAX_MESSAGE_LENGTH: int = 4096
MAX_CAPTION_LENGTH: int = 1024 MAX_CAPTION_LENGTH: int = 1024
ANONYMOUS_ADMIN_ID: int = 1087968824 ANONYMOUS_ADMIN_ID: int = 1087968824
@ -267,6 +274,7 @@ CHATACTION_TYPING: str = 'typing'
CHATACTION_UPLOAD_AUDIO: str = 'upload_audio' CHATACTION_UPLOAD_AUDIO: str = 'upload_audio'
CHATACTION_UPLOAD_VOICE: str = 'upload_voice' CHATACTION_UPLOAD_VOICE: str = 'upload_voice'
CHATACTION_UPLOAD_DOCUMENT: str = 'upload_document' CHATACTION_UPLOAD_DOCUMENT: str = 'upload_document'
CHATACTION_CHOOSE_STICKER: str = 'choose_sticker'
CHATACTION_UPLOAD_PHOTO: str = 'upload_photo' CHATACTION_UPLOAD_PHOTO: str = 'upload_photo'
CHATACTION_UPLOAD_VIDEO: str = 'upload_video' CHATACTION_UPLOAD_VIDEO: str = 'upload_video'
CHATACTION_UPLOAD_VIDEO_NOTE: str = 'upload_video_note' CHATACTION_UPLOAD_VIDEO_NOTE: str = 'upload_video_note'
@ -353,6 +361,7 @@ UPDATE_POLL = 'poll'
UPDATE_POLL_ANSWER = 'poll_answer' UPDATE_POLL_ANSWER = 'poll_answer'
UPDATE_MY_CHAT_MEMBER = 'my_chat_member' UPDATE_MY_CHAT_MEMBER = 'my_chat_member'
UPDATE_CHAT_MEMBER = 'chat_member' UPDATE_CHAT_MEMBER = 'chat_member'
UPDATE_CHAT_JOIN_REQUEST = 'chat_join_request'
UPDATE_ALL_TYPES = [ UPDATE_ALL_TYPES = [
UPDATE_MESSAGE, UPDATE_MESSAGE,
UPDATE_EDITED_MESSAGE, UPDATE_EDITED_MESSAGE,
@ -367,6 +376,7 @@ UPDATE_ALL_TYPES = [
UPDATE_POLL_ANSWER, UPDATE_POLL_ANSWER,
UPDATE_MY_CHAT_MEMBER, UPDATE_MY_CHAT_MEMBER,
UPDATE_CHAT_MEMBER, UPDATE_CHAT_MEMBER,
UPDATE_CHAT_JOIN_REQUEST,
] ]
BOT_COMMAND_SCOPE_DEFAULT = 'default' BOT_COMMAND_SCOPE_DEFAULT = 'default'

View file

@ -59,6 +59,7 @@ from .messagequeue import DelayQueue
from .pollanswerhandler import PollAnswerHandler from .pollanswerhandler import PollAnswerHandler
from .pollhandler import PollHandler from .pollhandler import PollHandler
from .chatmemberhandler import ChatMemberHandler from .chatmemberhandler import ChatMemberHandler
from .chatjoinrequesthandler import ChatJoinRequestHandler
from .defaults import Defaults from .defaults import Defaults
from .callbackdatacache import CallbackDataCache, InvalidCallbackData from .callbackdatacache import CallbackDataCache, InvalidCallbackData
@ -68,6 +69,7 @@ __all__ = (
'CallbackContext', 'CallbackContext',
'CallbackDataCache', 'CallbackDataCache',
'CallbackQueryHandler', 'CallbackQueryHandler',
'ChatJoinRequestHandler',
'ChatMemberHandler', 'ChatMemberHandler',
'ChosenInlineResultHandler', 'ChosenInlineResultHandler',
'CommandHandler', 'CommandHandler',

View file

@ -0,0 +1,100 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
# Copyright (C) 2021
# Leandro Toledo de Souza <devs@python-telegram-bot.org>
#
# 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 ChatJoinRequestHandler class."""
from telegram import Update
from .handler import Handler
from .utils.types import CCT
class ChatJoinRequestHandler(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.
.. 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.
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)

View file

@ -31,6 +31,7 @@ from telegram import (
TelegramObject, TelegramObject,
ChatMemberUpdated, ChatMemberUpdated,
constants, constants,
ChatJoinRequest,
) )
from telegram.poll import PollAnswer from telegram.poll import PollAnswer
from telegram.utils.types import JSONDict from telegram.utils.types import JSONDict
@ -89,6 +90,12 @@ class Update(TelegramObject):
:meth:`telegram.ext.Updater.start_webhook`). :meth:`telegram.ext.Updater.start_webhook`).
.. versionadded:: 13.4 .. versionadded:: 13.4
chat_join_request (:class:`telegram.ChatJoinRequest`, optional): A request to join the
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. **kwargs (:obj:`dict`): Arbitrary keyword arguments.
Attributes: Attributes:
@ -122,6 +129,11 @@ class Update(TelegramObject):
:meth:`telegram.ext.Updater.start_webhook`). :meth:`telegram.ext.Updater.start_webhook`).
.. versionadded:: 13.4 .. 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 +155,7 @@ class Update(TelegramObject):
'_effective_message', '_effective_message',
'my_chat_member', 'my_chat_member',
'chat_member', 'chat_member',
'chat_join_request',
'_id_attrs', '_id_attrs',
) )
@ -198,6 +211,10 @@ class Update(TelegramObject):
""":const:`telegram.constants.UPDATE_CHAT_MEMBER` """:const:`telegram.constants.UPDATE_CHAT_MEMBER`
.. versionadded:: 13.5""" .. 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 ALL_TYPES = constants.UPDATE_ALL_TYPES
""":const:`telegram.constants.UPDATE_ALL_TYPES` """:const:`telegram.constants.UPDATE_ALL_TYPES`
@ -219,6 +236,7 @@ class Update(TelegramObject):
poll_answer: PollAnswer = None, poll_answer: PollAnswer = None,
my_chat_member: ChatMemberUpdated = None, my_chat_member: ChatMemberUpdated = None,
chat_member: ChatMemberUpdated = None, chat_member: ChatMemberUpdated = None,
chat_join_request: ChatJoinRequest = None,
**_kwargs: Any, **_kwargs: Any,
): ):
# Required # Required
@ -237,6 +255,7 @@ class Update(TelegramObject):
self.poll_answer = poll_answer self.poll_answer = poll_answer
self.my_chat_member = my_chat_member self.my_chat_member = my_chat_member
self.chat_member = chat_member self.chat_member = chat_member
self.chat_join_request = chat_join_request
self._effective_user: Optional['User'] = None self._effective_user: Optional['User'] = None
self._effective_chat: Optional['Chat'] = None self._effective_chat: Optional['Chat'] = None
@ -286,6 +305,9 @@ class Update(TelegramObject):
elif self.chat_member: elif self.chat_member:
user = self.chat_member.from_user user = self.chat_member.from_user
elif self.chat_join_request:
user = self.chat_join_request.from_user
self._effective_user = user self._effective_user = user
return user return user
@ -325,6 +347,9 @@ class Update(TelegramObject):
elif self.chat_member: elif self.chat_member:
chat = self.chat_member.chat chat = self.chat_member.chat
elif self.chat_join_request:
chat = self.chat_join_request.chat
self._effective_chat = chat self._effective_chat = chat
return chat return chat
@ -335,7 +360,9 @@ class Update(TelegramObject):
update this is. Will be :obj:`None` for :attr:`inline_query`, update this is. Will be :obj:`None` for :attr:`inline_query`,
:attr:`chosen_inline_result`, :attr:`callback_query` from inline messages, :attr:`chosen_inline_result`, :attr:`callback_query` from inline messages,
:attr:`shipping_query`, :attr:`pre_checkout_query`, :attr:`poll`, :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: if self._effective_message:
@ -384,5 +411,6 @@ class Update(TelegramObject):
data['poll_answer'] = PollAnswer.de_json(data.get('poll_answer'), 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['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_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) return cls(**data)

View file

@ -1140,3 +1140,49 @@ class User(TelegramObject):
timeout=timeout, timeout=timeout,
api_kwargs=api_kwargs, api_kwargs=api_kwargs,
) )
def approve_join_request(
self,
chat_id: Union[int, str],
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: Union[int, str],
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
)

View file

@ -718,6 +718,7 @@ class TestBot:
ChatAction.UPLOAD_VIDEO, ChatAction.UPLOAD_VIDEO,
ChatAction.UPLOAD_VIDEO_NOTE, ChatAction.UPLOAD_VIDEO_NOTE,
ChatAction.UPLOAD_VOICE, ChatAction.UPLOAD_VOICE,
ChatAction.CHOOSE_STICKER,
], ],
) )
def test_send_chat_action(self, bot, chat_id, chat_action): def test_send_chat_action(self, bot, chat_id, chat_action):
@ -1696,6 +1697,37 @@ class TestBot:
assert isinstance(invite_link, str) assert isinstance(invite_link, str)
assert invite_link != '' 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'])
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
@flaky(3, 1) @flaky(3, 1)
@pytest.mark.parametrize('datetime', argvalues=[True, False], ids=['datetime', 'integer']) @pytest.mark.parametrize('datetime', argvalues=[True, False], ids=['datetime', 'integer'])
def test_advanced_chat_invite_links(self, bot, channel_id, datetime): def test_advanced_chat_invite_links(self, bot, channel_id, datetime):
@ -1720,12 +1752,29 @@ class TestBot:
aware_time_in_future = pytz.UTC.localize(time_in_future) aware_time_in_future = pytz.UTC.localize(time_in_future)
edited_invite_link = bot.edit_chat_invite_link( 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 edited_invite_link.invite_link == invite_link.invite_link
assert pytest.approx(edited_invite_link.expire_date == aware_time_in_future) assert pytest.approx(edited_invite_link.expire_date == aware_time_in_future)
assert edited_invite_link.name == 'NewName'
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)
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) 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.invite_link == invite_link.invite_link
assert revoked_invite_link.is_revoked is True assert revoked_invite_link.is_revoked is True
@ -1750,16 +1799,49 @@ class TestBot:
time_in_future = aware_expire_date.replace(tzinfo=None) time_in_future = aware_expire_date.replace(tzinfo=None)
edited_invite_link = tz_bot.edit_chat_invite_link( 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 edited_invite_link.invite_link == invite_link.invite_link
assert pytest.approx(edited_invite_link.expire_date == aware_expire_date) assert pytest.approx(edited_invite_link.expire_date == aware_expire_date)
assert edited_invite_link.name == 'NewName'
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)
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) 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.invite_link == invite_link.invite_link
assert revoked_invite_link.is_revoked is True assert revoked_invite_link.is_revoked is True
@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'):
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):
# 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'):
bot.decline_chat_join_request(chat_id=channel_id, user_id=chat_id)
@flaky(3, 1) @flaky(3, 1)
def test_set_chat_photo(self, bot, channel_id): def test_set_chat_photo(self, bot, channel_id):
def func(): def func():

View file

@ -636,6 +636,36 @@ class TestChat:
monkeypatch.setattr(chat.bot, 'revoke_chat_invite_link', make_assertion) monkeypatch.setattr(chat.bot, 'revoke_chat_invite_link', make_assertion)
assert chat.revoke_invite_link(invite_link=link) 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): def test_equality(self):
a = Chat(self.id_, self.title, self.type_) a = Chat(self.id_, self.title, self.type_)
b = Chat(self.id_, self.title, self.type_) b = Chat(self.id_, self.title, self.type_)

View file

@ -38,6 +38,8 @@ def invite_link(creator):
TestChatInviteLink.revoked, TestChatInviteLink.revoked,
expire_date=TestChatInviteLink.expire_date, expire_date=TestChatInviteLink.expire_date,
member_limit=TestChatInviteLink.member_limit, 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 revoked = False
expire_date = datetime.datetime.utcnow() expire_date = datetime.datetime.utcnow()
member_limit = 42 member_limit = 42
name = 'LinkName'
pending_join_request_count = 42
def test_slot_behaviour(self, recwarn, mro_slots, invite_link): def test_slot_behaviour(self, recwarn, mro_slots, invite_link):
for attr in invite_link.__slots__: for attr in invite_link.__slots__:
@ -79,7 +83,9 @@ class TestChatInviteLink:
'is_primary': self.primary, 'is_primary': self.primary,
'is_revoked': self.revoked, 'is_revoked': self.revoked,
'expire_date': to_timestamp(self.expire_date), '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) invite_link = ChatInviteLink.de_json(json_dict, bot)
@ -91,6 +97,8 @@ class TestChatInviteLink:
assert pytest.approx(invite_link.expire_date == self.expire_date) assert pytest.approx(invite_link.expire_date == self.expire_date)
assert to_timestamp(invite_link.expire_date) == to_timestamp(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.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): def test_to_dict(self, invite_link):
invite_link_dict = invite_link.to_dict() invite_link_dict = invite_link.to_dict()
@ -101,6 +109,8 @@ class TestChatInviteLink:
assert invite_link_dict['is_revoked'] == self.revoked assert invite_link_dict['is_revoked'] == self.revoked
assert invite_link_dict['expire_date'] == to_timestamp(self.expire_date) assert invite_link_dict['expire_date'] == to_timestamp(self.expire_date)
assert invite_link_dict['member_limit'] == self.member_limit 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): def test_equality(self):
a = ChatInviteLink("link", User(1, '', False), True, True) a = ChatInviteLink("link", User(1, '', False), True, True)

View file

@ -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 <devs@python-telegram-bot.org>
#
# 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()

View file

@ -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 <devs@python-telegram-bot.org>
#
# 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

View file

@ -34,6 +34,7 @@ from telegram import (
PollOption, PollOption,
ChatMemberUpdated, ChatMemberUpdated,
ChatMember, ChatMember,
ChatJoinRequest,
) )
from telegram.poll import PollAnswer from telegram.poll import PollAnswer
from telegram.utils.helpers import from_timestamp from telegram.utils.helpers import from_timestamp
@ -47,6 +48,14 @@ chat_member_updated = ChatMemberUpdated(
ChatMember(User(1, '', False), ChatMember.CREATOR), 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 = [ params = [
{'message': message}, {'message': message},
{'edited_message': message}, {'edited_message': message},
@ -57,11 +66,13 @@ params = [
{'chosen_inline_result': ChosenInlineResult('id', User(1, '', False), '')}, {'chosen_inline_result': ChosenInlineResult('id', User(1, '', False), '')},
{'shipping_query': ShippingQuery('id', User(1, '', False), '', None)}, {'shipping_query': ShippingQuery('id', User(1, '', False), '', None)},
{'pre_checkout_query': PreCheckoutQuery('id', User(1, '', False), '', 0, '')}, {'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': Poll('id', '?', [PollOption('.', 1)], False, False, False, Poll.REGULAR, True)},
{'poll_answer': PollAnswer("id", User(1, '', False), [1])}, {'poll_answer': PollAnswer("id", User(1, '', False), [1])},
{'my_chat_member': chat_member_updated}, {'my_chat_member': chat_member_updated},
{'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 = ( all_types = (
@ -78,6 +89,7 @@ all_types = (
'poll_answer', 'poll_answer',
'my_chat_member', 'my_chat_member',
'chat_member', 'chat_member',
'chat_join_request',
) )
ids = all_types + ('callback_query_without_message',) ids = all_types + ('callback_query_without_message',)
@ -171,6 +183,7 @@ class TestUpdate:
or update.poll_answer is not None or update.poll_answer is not None
or update.my_chat_member is not None or update.my_chat_member is not None
or update.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 assert eff_message.message_id == message.message_id
else: else:

View file

@ -430,6 +430,40 @@ class TestUser:
monkeypatch.setattr(user.bot, 'copy_message', make_assertion) monkeypatch.setattr(user.bot, 'copy_message', make_assertion)
assert user.copy_message(chat_id='chat_id', message_id='message_id') 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): def test_mention_html(self, user):
expected = '<a href="tg://user?id={}">{}</a>' expected = '<a href="tg://user?id={}">{}</a>'