Compare commits

..

12 Commits

Author SHA1 Message Date
Hinrich Mahler a9dd2b1746 Bump version to v13.9 2021-12-11 15:44:21 +01:00
Bibo-Joshi cb95868c4a Api 5.5 (#2809)
Co-authored-by: poolitzer <github@poolitzer.eu>
Co-authored-by: Harshil <37377066+harshil21@users.noreply.github.com>
2021-12-11 15:21:56 +01:00
Bibo-Joshi 2f6c4075c8 Adjust Automated Locking of Inactive Closed Threads (#2775) 2021-11-10 21:16:59 +01:00
Hinrich Mahler c53dc7dae4 Bump version to v13.8.1 2021-11-08 20:09:23 +01:00
Bibo-Joshi 8b3ea5ce56 Add ChatJoinRequest(Handler) to Docs (#2771) 2021-11-08 19:56:57 +01:00
Hinrich Mahler 51a4a6664c Bump version to v13.8 2021-11-08 19:12:10 +01:00
Bibo-Joshi e4dc80f41d API 5.4 (#2767)
Co-authored-by: poolitzer <25934244+Poolitzer@users.noreply.github.com>
2021-11-08 19:02:20 +01:00
Abshar Mohammed Aslam bc7c422a11 Create Issue Template Forms (#2689) 2021-10-03 20:08:04 +02:00
Yan c3e3bb77e5 Fix camelCase Functions in ExtBot (#2659)
Co-authored-by: Bibo-Joshi <22366557+Bibo-Joshi@users.noreply.github.com>
2021-09-15 17:07:11 +02:00
DonalDuck004 a25c76e6a3 Fix Empty Captions not Being Passed by Bot.copy_message (#2651) 2021-09-09 07:50:04 +02:00
Mehdi 0c5085022c Fix Setting Thumbs When Uploading A Single File (#2583)
* Update request.py

If the media has a thumb, we also need to attach it to the data.

* Add test

* Editing syntax

* Debug test

* update request.py

* Update test_inputmedia.py

* Update test_inputmedia.py

* Update test_inputmedia.py

Fix test.

* Update AUTHORS.rst

Adding my name!

* Update AUTHORS.rst
2021-08-11 08:34:47 +02:00
Bibo-Joshi 1fdaaac809 Fix Bug in BasePersistence.insert/replace_bot for Objects with __dict__ not in __slots__ (#2603)
* More special cases with slots

* Fix failing tests
2021-07-24 17:17:25 +02:00
47 changed files with 1938 additions and 167 deletions
+76
View File
@@ -0,0 +1,76 @@
name: Bug Report
description: Create a report to help us improve
title: "[BUG]"
labels: ["bug :bug:"]
body:
- type: markdown
attributes:
value: |
Thanks for reporting issues of python-telegram-bot!
Use this template to notify us if you found a bug.
To make it easier for us to help you please enter detailed information below.
Please note, we only support the latest version of python-telegram-bot and master branch. Please make sure to upgrade & recreate the issue on the latest version prior to opening an issue.
- type: textarea
id: steps-to-reproduce
attributes:
label: Steps to Reproduce
value: |
1.
2.
3.
validations:
required: true
- type: textarea
id: expected-behaviour
attributes:
label: Expected behaviour
description: Tell us what should happen
validations:
required: true
- type: textarea
id: actual-behaviour
attributes:
label: Actual behaviour
description: Tell us what happens instead
validations:
required: true
- type: markdown
attributes:
value: "### Configuration"
- type: input
id: operating-system
attributes:
label: Operating System
validations:
required: true
- type: textarea
id: versions
attributes:
label: Version of Python, python-telegram-bot & dependencies
description: Paste the output of `$ python -m telegram` here. This will be automatically formatted into code, so no need for backticks.
render: shell
validations:
required: true
- type: textarea
id: logs
attributes:
label: Relevant log output
description: Insert logs here (if necessary). This will be automatically formatted into code, so no need for backticks.
render: python
- type: textarea
id: additional-context
attributes:
label: Additional Context
description: You may provide any other additional context to the bug here.
-43
View File
@@ -1,43 +0,0 @@
---
name: Bug report
about: Create a report to help us improve
title: "[BUG]"
labels: 'bug :bug:'
assignees: ''
---
<!--
Thanks for reporting issues of python-telegram-bot!
Use this template to notify us if you found a bug.
To make it easier for us to help you please enter detailed information below.
Please note, we only support the latest version of python-telegram-bot and
master branch. Please make sure to upgrade & recreate the issue on the latest
version prior to opening an issue.
-->
### Steps to reproduce
1.
2.
3.
### Expected behaviour
Tell us what should happen
### Actual behaviour
Tell us what happens instead
### Configuration
**Operating System:**
**Version of Python, python-telegram-bot & dependencies:**
``$ python -m telegram``
### Logs
Insert logs here (if necessary)
@@ -0,0 +1,37 @@
name: Feature Request
description: Suggest an idea for this project
title: "[FEATURE]"
labels: ["enhancement"]
body:
- type: textarea
id: related-problem
attributes:
label: "What kind of feature are you missing? Where do you notice a shortcoming of PTB?"
description: "A clear and concise description of what the problem is."
placeholder: "Example: I want to do X, but there is no way to do it."
validations:
required: true
- type: textarea
id: solution
attributes:
label: "Describe the solution you'd like"
description: "A clear and concise description of what you want to happen."
placeholder: "Example: I think it would be nice if you would add feature Y so I can do X."
validations:
required: true
- type: textarea
id: alternatives
attributes:
label: "Describe alternatives you've considered"
description: "A clear and concise description of any alternative solutions or features you've considered."
placeholder: "Example: I considered Z to be able to do X, but that didn't work because..."
- type: textarea
id: additional-context
attributes:
label: "Additional context"
description: "Add any other context or screenshots about the feature request here."
placeholder: "Example: Here's a photo of my cat!"
-24
View File
@@ -1,24 +0,0 @@
---
name: Feature request
about: Suggest an idea for this project
title: "[FEATURE]"
labels: enhancement
assignees: ''
---
#### Is your feature request related to a problem? Please describe.
A clear and concise description of what the problem is.
Ex. *I want to do X, but there is no way to do it.*
#### Describe the solution you'd like
A clear and concise description of what you want to happen.
Ex. *I think it would be nice if you would add feature Y so it will make it easier.*
#### Describe alternatives you've considered
A clear and concise description of any alternative solutions or features you've considered.
Ex. *I considered Z, but that didn't work because...*
#### Additional context
Add any other context or screenshots about the feature request here.
Ex. *Here's a photo of my cat!*
-29
View File
@@ -1,29 +0,0 @@
---
name: Question
about: Get help with errors or general questions
title: "[QUESTION]"
labels: question
assignees: ''
---
<!--
Hey there, you have a question? We are happy to answer. Please make sure no similar question was opened already.
To make it easier for us to help you, please read this article https://git.io/JURJO and try to follow the template below as closely as possible.
Please mind that there is also a users' Telegram group at https://t.me/pythontelegrambotgroup for questions about the library. Questions asked there might be answered quicker than here. Moreover, GitHub Discussions at https://git.io/JG3rk offer a slightly better format to discuss usage questions.
-->
### Issue I am facing
Please describe the issue here in as much detail as possible
### Traceback to the issue
```
put it here
```
### Related part of your code
```python
put it here
```
+68
View File
@@ -0,0 +1,68 @@
name: Question
description: Get help with errors or general questions
title: "[QUESTION]"
labels: ["question"]
body:
- type: markdown
attributes:
value: |
Hey there, you have a question? We are happy to answer. Please make sure no similar question was opened already.
To make it easier for us to help you, please read this [article](https://git.io/JURJO).
Please mind that there is also a users' [Telegram group](https://t.me/pythontelegrambotgroup) for questions about the library. Questions asked there might be answered quicker than here. Moreover, [GitHub Discussions](https://git.io/JG3rk) offer a slightly better format to discuss usage questions.
- type: textarea
id: issue-faced
attributes:
label: "Issue I am facing"
description: "Please describe the issue here in as much detail as possible"
validations:
required: true
- type: textarea
id: traceback
attributes:
label: "Traceback to the issue"
description: "If you are facing a specific error message, please paste the traceback here. This will be automatically formatted into python code, so no need for backticks."
placeholder: |
Traceback (most recent call last):
File "/home/bot.py", line 1, in main
foo = bar()
...
telegram.error.BadRequest: Traceback not found
render: python
- type: textarea
id: related-code
attributes:
label: "Related part of your code"
description: "This will be automatically formatted into code (python), so no need for backticks."
placeholder: |
logging.basicConfig(
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", level=logging.INFO
)
logger = logging.getLogger(__name__)
render: python
- type: markdown
attributes:
value: "### Configuration"
- type: input
id: operating-system
attributes:
label: Operating System
validations:
required: true
- type: textarea
id: versions
attributes:
label: Version of Python, python-telegram-bot & dependencies
description: Paste the output of `$ python -m telegram` here. This will be automatically formatted into code, so no need for backticks.
render: shell
validations:
required: true
+1
View File
@@ -7,6 +7,7 @@ Hey! You're PRing? Cool! Please have a look at the below checklist. It's here to
- [ ] Added `.. versionadded:: version`, `.. versionchanged:: version` or `.. deprecated:: version` to the docstrings for user facing changes (for methods/class descriptions, arguments and attributes)
- [ ] Created new or adapted existing unit tests
- [ ] Added myself alphabetically to `AUTHORS.rst` (optional)
- [ ] Added new classes & modules to the docs
### If the PR contains API changes (otherwise, you can delete this passage)
+2 -2
View File
@@ -12,7 +12,7 @@ jobs:
- uses: dessant/lock-threads@v2.0.1
with:
github-token: ${{ github.token }}
issue-lock-inactive-days: '1'
issue-lock-inactive-days: '7'
issue-lock-reason: ''
pr-lock-inactive-days: '1'
pr-lock-inactive-days: '7'
pr-lock-reason: ''
+3
View File
@@ -26,6 +26,7 @@ Contributors
The following wonderful people contributed directly or indirectly to this project:
- `Abshar <https://github.com/abxhr>`_
- `Alateas <https://github.com/alateas>`_
- `Ales Dokshanin <https://github.com/alesdokshanin>`_
- `Ambro17 <https://github.com/Ambro17>`_
@@ -39,6 +40,7 @@ The following wonderful people contributed directly or indirectly to this projec
- `daimajia <https://github.com/daimajia>`_
- `Daniel Reed <https://github.com/nmlorg>`_
- `D David Livingston <https://github.com/daviddl9>`_
- `DonalDuck004 <https://github.com/DonalDuck004>`_
- `Eana Hufwe <https://github.com/blueset>`_
- `Ehsan Online <https://github.com/ehsanonline>`_
- `Eli Gao <https://github.com/eligao>`_
@@ -84,6 +86,7 @@ The following wonderful people contributed directly or indirectly to this projec
- `Oleg Sushchenko <https://github.com/feuillemorte>`_
- `Or Bin <https://github.com/OrBin>`_
- `overquota <https://github.com/overquota>`_
- `Paradox <https://github.com/paradox70>`_
- `Patrick Hofmann <https://github.com/PH89>`_
- `Paul Larsen <https://github.com/PaulSonOfLars>`_
- `Pieter Schutz <https://github.com/eldinnie>`_
+54
View File
@@ -2,6 +2,60 @@
Changelog
=========
Version 13.9
============
*Released 2021-12-11*
This is the technical changelog for version 13.9. More elaborate release notes can be found in the news channel `@pythontelegrambotchannel <https://t.me/pythontelegrambotchannel>`_.
**Major Changes:**
- Full Support for Api 5.5 (`#2809`_)
**Minor Changes**
- Adjust Automated Locking of Inactive Issues (`#2775`_)
.. _`#2809`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2809
.. _`#2775`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2775
Version 13.8.1
==============
*Released 2021-11-08*
This is the technical changelog for version 13.8.1. More elaborate release notes can be found in the news channel `@pythontelegrambotchannel <https://t.me/pythontelegrambotchannel>`_.
**Doc fixes:**
- Add ``ChatJoinRequest(Handler)`` to Docs (`#2771`_)
.. _`#2771`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2771
Version 13.8
============
*Released 2021-11-08*
This is the technical changelog for version 13.8. More elaborate release notes can be found in the news channel `@pythontelegrambotchannel <https://t.me/pythontelegrambotchannel>`_.
**Major Changes:**
- Full support for API 5.4 (`#2767`_)
**Minor changes, CI improvements, Doc fixes and Type hinting:**
- Create Issue Template Forms (`#2689`_)
- Fix ``camelCase`` Functions in ``ExtBot`` (`#2659`_)
- Fix Empty Captions not Being Passed by ``Bot.copy_message`` (`#2651`_)
- Fix Setting Thumbs When Uploading A Single File (`#2583`_)
- Fix Bug in ``BasePersistence.insert``/``replace_bot`` for Objects with ``__dict__`` not in ``__slots__`` (`#2603`_)
.. _`#2767`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2767
.. _`#2689`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2689
.. _`#2659`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2659
.. _`#2651`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2651
.. _`#2583`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2583
.. _`#2603`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2603
Version 13.7
============
*Released 2021-07-01*
+3 -3
View File
@@ -1,5 +1,5 @@
..
Make user to apply any changes to this file to README_RAW.rst as well!
Make sure to apply any changes to this file to README_RAW.rst as well!
.. image:: https://github.com/python-telegram-bot/logos/blob/master/logo-text/png/ptb-logo-text_768.png?raw=true
:align: center
@@ -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.5-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.5** are supported.
==========
Installing
+3 -3
View File
@@ -1,5 +1,5 @@
..
Make user to apply any changes to this file to README.rst as well!
Make sure to apply any changes to this file to README.rst as well!
.. image:: https://github.com/python-telegram-bot/logos/blob/master/logo-text/png/ptb-raw-logo-text_768.png?raw=true
:align: center
@@ -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.5-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.5** are supported.
==========
Installing
+2 -2
View File
@@ -60,9 +60,9 @@ author = u'Leandro Toledo'
# built documents.
#
# The short X.Y version.
version = '13.7' # telegram.__version__[:3]
version = '13.9' # telegram.__version__[:3]
# The full version, including alpha/beta/rc tags.
release = '13.7' # telegram.__version__
release = '13.9' # telegram.__version__
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
+8
View File
@@ -0,0 +1,8 @@
:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/chatjoinrequest.py
telegram.ChatJoinRequest
========================
.. autoclass:: telegram.ChatJoinRequest
:members:
:show-inheritance:
@@ -0,0 +1,8 @@
:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/ext/chatjoinrequesthandler.py
telegram.ext.ChatJoinRequestHandler
===================================
.. autoclass:: telegram.ext.ChatJoinRequestHandler
:members:
:show-inheritance:
+2 -1
View File
@@ -22,8 +22,9 @@ Handlers
telegram.ext.handler
telegram.ext.callbackqueryhandler
telegram.ext.choseninlineresulthandler
telegram.ext.chatjoinrequesthandler
telegram.ext.chatmemberhandler
telegram.ext.choseninlineresulthandler
telegram.ext.commandhandler
telegram.ext.conversationhandler
telegram.ext.inlinequeryhandler
+1
View File
@@ -19,6 +19,7 @@ telegram package
telegram.chat
telegram.chataction
telegram.chatinvitelink
telegram.chatjoinrequest
telegram.chatlocation
telegram.chatmember
telegram.chatmemberowner
+1 -1
View File
@@ -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
+2
View File
@@ -25,6 +25,7 @@ from .files.chatphoto import ChatPhoto
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 @@ __all__ = ( # Keep this alphabetically ordered
'Chat',
'ChatAction',
'ChatInviteLink',
'ChatJoinRequest',
'ChatLocation',
'ChatMember',
'ChatMemberOwner',
+219 -5
View File
@@ -579,6 +579,14 @@ class Bot(TelegramObject):
) -> Message:
"""Use this method to forward messages of any kind. Service messages can't be forwarded.
Note:
Since the release of Bot API 5.5 it can be impossible to forward messages from
some chats. Use the attributes :attr:`telegram.Message.has_protected_content` and
:attr:`telegram.Chat.has_protected_content` to check this.
As a workaround, it is still possible to use :meth:`copy_message`. However, this
behaviour is undocumented and might be changed by Telegram.
Args:
chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username
of the target channel (in the format ``@channelusername``).
@@ -2408,6 +2416,45 @@ class Bot(TelegramObject):
return result # type: ignore[return-value]
@log
def ban_chat_sender_chat(
self,
chat_id: Union[str, int],
sender_chat_id: int,
timeout: ODVInput[float] = DEFAULT_NONE,
api_kwargs: JSONDict = None,
) -> bool:
"""
Use this method to ban a channel chat in a supergroup or a channel. Until the chat is
unbanned, the owner of the banned chat won't be able to send messages on behalf of **any of
their channels**. The bot must be an administrator in the supergroup or channel for this
to work and must have the appropriate administrator rights.
.. versionadded:: 13.9
Args:
chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target group or username
of the target supergroup or channel (in the format ``@channelusername``).
sender_chat_id (:obj:`int`): Unique identifier of the target sender chat.
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, 'sender_chat_id': sender_chat_id}
result = self._post('banChatSenderChat', data, timeout=timeout, api_kwargs=api_kwargs)
return result # type: ignore[return-value]
@log
def unban_chat_member(
self,
@@ -2437,7 +2484,7 @@ class Bot(TelegramObject):
Telegram API.
Returns:
:obj:`bool` On success, :obj:`True` is returned.
:obj:`bool`: On success, :obj:`True` is returned.
Raises:
:class:`telegram.error.TelegramError`
@@ -2452,6 +2499,43 @@ class Bot(TelegramObject):
return result # type: ignore[return-value]
@log
def unban_chat_sender_chat(
self,
chat_id: Union[str, int],
sender_chat_id: int,
timeout: ODVInput[float] = DEFAULT_NONE,
api_kwargs: JSONDict = None,
) -> bool:
"""Use this method to unban a previously banned channel in a supergroup or channel.
The bot must be an administrator for this to work and must have the
appropriate administrator rights.
.. versionadded:: 13.9
Args:
chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username
of the target supergroup or channel (in the format ``@channelusername``).
sender_chat_id (:obj:`int`): Unique identifier of the target sender chat.
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, 'sender_chat_id': sender_chat_id}
result = self._post('unbanChatSenderChat', data, timeout=timeout, api_kwargs=api_kwargs)
return result # type: ignore[return-value]
@log
def answer_callback_query(
self,
@@ -3985,6 +4069,8 @@ class Bot(TelegramObject):
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 +4093,14 @@ class Bot(TelegramObject):
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`
@@ -4015,6 +4109,11 @@ class Bot(TelegramObject):
: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,
}
@@ -4029,6 +4128,12 @@ class Bot(TelegramObject):
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,11 +4147,19 @@ class Bot(TelegramObject):
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
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:
@@ -4064,6 +4177,14 @@ class Bot(TelegramObject):
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`
@@ -4072,6 +4193,11 @@ class Bot(TelegramObject):
: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:
@@ -4084,6 +4210,12 @@ class Bot(TelegramObject):
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 +4258,80 @@ class Bot(TelegramObject):
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
def set_chat_photo(
self,
@@ -5296,7 +5502,7 @@ class Bot(TelegramObject):
'disable_notification': disable_notification,
'allow_sending_without_reply': allow_sending_without_reply,
}
if caption:
if caption is not None:
data['caption'] = caption
if caption_entities:
data['caption_entities'] = caption_entities
@@ -5377,10 +5583,14 @@ class Bot(TelegramObject):
"""Alias for :meth:`get_file`"""
banChatMember = ban_chat_member
"""Alias for :meth:`ban_chat_member`"""
banChatSenderChat = ban_chat_sender_chat
"""Alias for :meth:`ban_chat_sender_chat`"""
kickChatMember = kick_chat_member
"""Alias for :meth:`kick_chat_member`"""
unbanChatMember = unban_chat_member
"""Alias for :meth:`unban_chat_member`"""
unbanChatSenderChat = unban_chat_sender_chat
"""Alias for :meth:`unban_chat_sender_chat`"""
answerCallbackQuery = answer_callback_query
"""Alias for :meth:`answer_callback_query`"""
editMessageText = edit_message_text
@@ -5436,11 +5646,15 @@ class Bot(TelegramObject):
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 :meth:`approve_chat_join_request`"""
declineChatJoinRequest = 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
+177
View File
@@ -81,6 +81,11 @@ class Chat(TelegramObject):
Returned only in :meth:`telegram.Bot.get_chat`.
bio (:obj:`str`, optional): Bio of the other party in a private chat. Returned only in
:meth:`telegram.Bot.get_chat`.
has_private_forwards (:obj:`bool`, optional): :obj:`True`, if privacy settings of the other
party in the private chat allows to use ``tg://user?id=<user_id>`` links only in chats
with the user. Returned only in :meth:`telegram.Bot.get_chat`.
.. versionadded:: 13.9
description (:obj:`str`, optional): Description, for groups, supergroups and channel chats.
Returned only in :meth:`telegram.Bot.get_chat`.
invite_link (:obj:`str`, optional): Primary invite link, for groups, supergroups and
@@ -97,6 +102,10 @@ class Chat(TelegramObject):
:meth:`telegram.Bot.get_chat`.
.. versionadded:: 13.4
has_protected_content (:obj:`bool`, optional): :obj:`True`, if messages from the chat can't
be forwarded to other chats. Returned only in :meth:`telegram.Bot.get_chat`.
.. versionadded:: 13.9
bot (:class:`telegram.Bot`, optional): The Bot to use for instance methods.
sticker_set_name (:obj:`str`, optional): For supergroups, name of group sticker set.
Returned only in :meth:`telegram.Bot.get_chat`.
@@ -119,6 +128,11 @@ class Chat(TelegramObject):
photo (:class:`telegram.ChatPhoto`): Optional. Chat photo.
bio (:obj:`str`): Optional. Bio of the other party in a private chat. Returned only in
:meth:`telegram.Bot.get_chat`.
has_private_forwards (:obj:`bool`): Optional. :obj:`True`, if privacy settings of the other
party in the private chat allows to use ``tg://user?id=<user_id>`` links only in chats
with the user.
.. versionadded:: 13.9
description (:obj:`str`): Optional. Description, for groups, supergroups and channel chats.
invite_link (:obj:`str`): Optional. Primary invite link, for groups, supergroups and
channel. Returned only in :meth:`telegram.Bot.get_chat`.
@@ -134,6 +148,10 @@ class Chat(TelegramObject):
:meth:`telegram.Bot.get_chat`.
.. versionadded:: 13.4
has_protected_content (:obj:`bool`): Optional. :obj:`True`, if messages from the chat can't
be forwarded to other chats.
.. versionadded:: 13.9
sticker_set_name (:obj:`str`): Optional. For supergroups, name of Group sticker set.
can_set_sticker_set (:obj:`bool`): Optional. :obj:`True`, if the bot can change group the
sticker set.
@@ -166,6 +184,8 @@ class Chat(TelegramObject):
'linked_chat_id',
'all_members_are_administrators',
'message_auto_delete_time',
'has_protected_content',
'has_private_forwards',
'_id_attrs',
)
@@ -204,6 +224,8 @@ class Chat(TelegramObject):
linked_chat_id: int = None,
location: ChatLocation = None,
message_auto_delete_time: int = None,
has_private_forwards: bool = None,
has_protected_content: bool = None,
**_kwargs: Any,
):
# Required
@@ -218,6 +240,7 @@ class Chat(TelegramObject):
self.all_members_are_administrators = _kwargs.get('all_members_are_administrators')
self.photo = photo
self.bio = bio
self.has_private_forwards = has_private_forwards
self.description = description
self.invite_link = invite_link
self.pinned_message = pinned_message
@@ -226,6 +249,7 @@ class Chat(TelegramObject):
self.message_auto_delete_time = (
int(message_auto_delete_time) if message_auto_delete_time is not None else None
)
self.has_protected_content = has_protected_content
self.sticker_set_name = sticker_set_name
self.can_set_sticker_set = can_set_sticker_set
self.linked_chat_id = linked_chat_id
@@ -433,6 +457,98 @@ class Chat(TelegramObject):
revoke_messages=revoke_messages,
)
def ban_sender_chat(
self,
sender_chat_id: int,
timeout: ODVInput[float] = DEFAULT_NONE,
api_kwargs: JSONDict = None,
) -> bool:
"""Shortcut for::
bot.ban_chat_sender_chat(chat_id=update.effective_chat.id, *args, **kwargs)
For the documentation of the arguments, please see
:meth:`telegram.Bot.ban_chat_sender_chat`.
.. versionadded:: 13.9
Returns:
:obj:`bool`: On success, :obj:`True` is returned.
"""
return self.bot.ban_chat_sender_chat(
chat_id=self.id, sender_chat_id=sender_chat_id, timeout=timeout, api_kwargs=api_kwargs
)
def ban_chat(
self,
chat_id: Union[str, int],
timeout: ODVInput[float] = DEFAULT_NONE,
api_kwargs: JSONDict = None,
) -> bool:
"""Shortcut for::
bot.ban_chat_sender_chat(sender_chat_id=update.effective_chat.id, *args, **kwargs)
For the documentation of the arguments, please see
:meth:`telegram.Bot.ban_chat_sender_chat`.
.. versionadded:: 13.9
Returns:
:obj:`bool`: On success, :obj:`True` is returned.
"""
return self.bot.ban_chat_sender_chat(
chat_id=chat_id, sender_chat_id=self.id, timeout=timeout, api_kwargs=api_kwargs
)
def unban_sender_chat(
self,
sender_chat_id: int,
timeout: ODVInput[float] = DEFAULT_NONE,
api_kwargs: JSONDict = None,
) -> bool:
"""Shortcut for::
bot.unban_chat_sender_chat(chat_id=update.effective_chat.id, *args, **kwargs)
For the documentation of the arguments, please see
:meth:`telegram.Bot.unban_chat_sender_chat`.
.. versionadded:: 13.9
Returns:
:obj:`bool`: On success, :obj:`True` is returned.
"""
return self.bot.unban_chat_sender_chat(
chat_id=self.id, sender_chat_id=sender_chat_id, timeout=timeout, api_kwargs=api_kwargs
)
def unban_chat(
self,
chat_id: Union[str, int],
timeout: ODVInput[float] = DEFAULT_NONE,
api_kwargs: JSONDict = None,
) -> bool:
"""Shortcut for::
bot.unban_chat_sender_chat(sender_chat_id=update.effective_chat.id, *args, **kwargs)
For the documentation of the arguments, please see
:meth:`telegram.Bot.unban_chat_sender_chat`.
.. versionadded:: 13.9
Returns:
:obj:`bool`: On success, :obj:`True` is returned.
"""
return self.bot.unban_chat_sender_chat(
chat_id=chat_id, sender_chat_id=self.id, timeout=timeout, api_kwargs=api_kwargs
)
def unban_member(
self,
user_id: Union[str, int],
@@ -1524,6 +1640,8 @@ class Chat(TelegramObject):
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 +1652,10 @@ class Chat(TelegramObject):
.. 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 +1666,8 @@ class Chat(TelegramObject):
member_limit=member_limit,
timeout=timeout,
api_kwargs=api_kwargs,
name=name,
creates_join_request=creates_join_request,
)
def edit_invite_link(
@@ -1553,6 +1677,8 @@ class Chat(TelegramObject):
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 +1689,9 @@ class Chat(TelegramObject):
.. 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 +1703,8 @@ class Chat(TelegramObject):
member_limit=member_limit,
timeout=timeout,
api_kwargs=api_kwargs,
name=name,
creates_join_request=creates_join_request,
)
def revoke_invite_link(
@@ -1598,3 +1729,49 @@ class Chat(TelegramObject):
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
)
+4
View File
@@ -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
+33 -1
View File
@@ -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 @@ class ChatInviteLink(TelegramObject):
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 @@ class ChatInviteLink(TelegramObject):
# 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
+159
View File
@@ -0,0 +1,159 @@
#!/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.
Note:
Since Bot API 5.5, bots are allowed to contact users who sent a join request to a chat
where the bot is an administrator with the
:attr:`~telegram.ChatMemberAdministrator.can_invite_users` administrator right even if
the user never interacted with the bot before.
.. 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
)
+17 -2
View File
@@ -21,7 +21,7 @@ The following constants were extracted from the
`Telegram Bots API <https://core.telegram.org/bots/api>`_.
Attributes:
BOT_API_VERSION (:obj:`str`): `5.3`. Telegram Bot API version supported by this
BOT_API_VERSION (:obj:`str`): `5.5`. Telegram Bot API version supported by this
version of `python-telegram-bot`. Also available as ``telegram.bot_api_version``.
.. versionadded:: 13.4
@@ -48,6 +48,10 @@ Attributes:
ANONYMOUS_ADMIN_ID (:obj:`int`): ``1087968824`` (User id in groups for anonymous admin)
SERVICE_CHAT_ID (:obj:`int`): ``777000`` (Telegram service chat, that also acts as sender of
channel posts forwarded to discussion groups)
FAKE_CHANNEL_ID (:obj:`int`): ``136817688`` (User id in groups when message is sent on behalf
of a channel).
.. versionadded:: 13.9
The following constants are related to specific classes and are also available
as attributes of those classes:
@@ -86,6 +90,9 @@ Attributes:
.. 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 +208,13 @@ Attributes:
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`:
@@ -233,11 +244,12 @@ Attributes:
"""
from typing import List
BOT_API_VERSION: str = '5.3'
BOT_API_VERSION: str = '5.5'
MAX_MESSAGE_LENGTH: int = 4096
MAX_CAPTION_LENGTH: int = 1024
ANONYMOUS_ADMIN_ID: int = 1087968824
SERVICE_CHAT_ID: int = 777000
FAKE_CHANNEL_ID: int = 136817688
# constants above this line are tested
@@ -267,6 +279,7 @@ CHATACTION_TYPING: str = 'typing'
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 +366,7 @@ UPDATE_POLL = 'poll'
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 +381,7 @@ UPDATE_ALL_TYPES = [
UPDATE_POLL_ANSWER,
UPDATE_MY_CHAT_MEMBER,
UPDATE_CHAT_MEMBER,
UPDATE_CHAT_JOIN_REQUEST,
]
BOT_COMMAND_SCOPE_DEFAULT = 'default'
+2
View File
@@ -59,6 +59,7 @@ from .messagequeue import DelayQueue
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 @@ __all__ = (
'CallbackContext',
'CallbackDataCache',
'CallbackQueryHandler',
'ChatJoinRequestHandler',
'ChatMemberHandler',
'ChosenInlineResultHandler',
'CommandHandler',
+12 -12
View File
@@ -277,8 +277,6 @@ class BasePersistence(Generic[UD, CD, BD], ABC):
new_obj[cls._replace_bot(k, memo)] = cls._replace_bot(val, memo)
memo[obj_id] = new_obj
return new_obj
# if '__dict__' in obj.__slots__, we already cover this here, that's why the
# __dict__ case comes below
try:
if hasattr(obj, '__slots__'):
for attr_name in new_obj.__slots__:
@@ -289,8 +287,11 @@ class BasePersistence(Generic[UD, CD, BD], ABC):
cls._replace_bot(getattr(new_obj, attr_name), memo), memo
),
)
memo[obj_id] = new_obj
return new_obj
if '__dict__' in obj.__slots__:
# In this case, we have already covered the case that obj has __dict__
# Note that obj may have a __dict__ even if it's not in __slots__!
memo[obj_id] = new_obj
return new_obj
if hasattr(obj, '__dict__'):
for attr_name, attr in new_obj.__dict__.items():
setattr(new_obj, attr_name, cls._replace_bot(attr, memo))
@@ -302,9 +303,8 @@ class BasePersistence(Generic[UD, CD, BD], ABC):
f'See the docs of BasePersistence.replace_bot for more information.',
RuntimeWarning,
)
memo[obj_id] = obj
return obj
memo[obj_id] = obj
return obj
def insert_bot(self, obj: object) -> object:
@@ -379,8 +379,6 @@ class BasePersistence(Generic[UD, CD, BD], ABC):
new_obj[self._insert_bot(k, memo)] = self._insert_bot(val, memo)
memo[obj_id] = new_obj
return new_obj
# if '__dict__' in obj.__slots__, we already cover this here, that's why the
# __dict__ case comes below
try:
if hasattr(obj, '__slots__'):
for attr_name in obj.__slots__:
@@ -391,8 +389,11 @@ class BasePersistence(Generic[UD, CD, BD], ABC):
self._insert_bot(getattr(new_obj, attr_name), memo), memo
),
)
memo[obj_id] = new_obj
return new_obj
if '__dict__' in obj.__slots__:
# In this case, we have already covered the case that obj has __dict__
# Note that obj may have a __dict__ even if it's not in __slots__!
memo[obj_id] = new_obj
return new_obj
if hasattr(obj, '__dict__'):
for attr_name, attr in new_obj.__dict__.items():
setattr(new_obj, attr_name, self._insert_bot(attr, memo))
@@ -404,9 +405,8 @@ class BasePersistence(Generic[UD, CD, BD], ABC):
f'See the docs of BasePersistence.insert_bot for more information.',
RuntimeWarning,
)
memo[obj_id] = obj
return obj
memo[obj_id] = obj
return obj
@abstractmethod
+100
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)
+10
View File
@@ -325,3 +325,13 @@ class ExtBot(telegram.bot.Bot):
# We override this method to call self._insert_callback_data
result = super().get_chat(chat_id=chat_id, timeout=timeout, api_kwargs=api_kwargs)
return self._insert_callback_data(result)
# updated camelCase aliases
getChat = get_chat
"""Alias for :meth:`get_chat`"""
copyMessage = copy_message
"""Alias for :meth:`copy_message`"""
getUpdates = get_updates
"""Alias for :meth:`get_updates`"""
stopPoll = stop_poll
"""Alias for :meth:`stop_poll`"""
+33 -4
View File
@@ -1965,16 +1965,16 @@ officedocument.wordprocessingml.document")``.
class sender_chat(_ChatUserBaseFilter):
# pylint: disable=W0235
"""Filters messages to allow only those which are from a specified sender chats chat ID or
"""Filters messages to allow only those which are from a specified sender chat's chat ID or
username.
Examples:
* To filter for messages forwarded to a discussion group from a channel with ID
* To filter for messages sent to a group by a channel with ID
``-1234``, use ``MessageHandler(Filters.sender_chat(-1234), callback_method)``.
* To filter for messages of anonymous admins in a super group with username
``@anonymous``, use
``MessageHandler(Filters.sender_chat(username='anonymous'), callback_method)``.
* To filter for messages forwarded to a discussion group from *any* channel, use
* To filter for messages sent to a group by *any* channel, use
``MessageHandler(Filters.sender_chat.channel, callback_method)``.
* To filter for messages of anonymous admins in *any* super group, use
``MessageHandler(Filters.sender_chat.super_group, callback_method)``.
@@ -1983,7 +1983,10 @@ officedocument.wordprocessingml.document")``.
Remember, ``sender_chat`` is also set for messages in a channel as the channel itself,
so when your bot is an admin in a channel and the linked discussion group, you would
receive the message twice (once from inside the channel, once inside the discussion
group).
group). Since v13.9, the field :attr:`telegram.Message.is_automatic_forward` will be
:obj:`True` for the discussion group message.
.. seealso:: :attr:`Filters.is_automatic_forward`
Warning:
:attr:`chat_ids` will return a *copy* of the saved chat ids as :class:`frozenset`. This
@@ -2089,6 +2092,32 @@ officedocument.wordprocessingml.document")``.
super_group = _SuperGroup()
channel = _Channel()
class _IsAutomaticForward(MessageFilter):
__slots__ = ()
name = 'Filters.is_automatic_forward'
def filter(self, message: Message) -> bool:
return bool(message.is_automatic_forward)
is_automatic_forward = _IsAutomaticForward()
"""Messages that contain :attr:`telegram.Message.is_automatic_forward`.
.. versionadded:: 13.9
"""
class _HasProtectedContent(MessageFilter):
__slots__ = ()
name = 'Filters.has_protected_content'
def filter(self, message: Message) -> bool:
return bool(message.has_protected_content)
has_protected_content = _HasProtectedContent()
"""Messages that contain :attr:`telegram.Message.has_protected_content`.
.. versionadded:: 13.9
"""
class _Invoice(MessageFilter):
__slots__ = ()
name = 'Filters.invoice'
+19 -4
View File
@@ -45,16 +45,25 @@ class InlineKeyboardButton(TelegramObject):
.. versionadded:: 13.6
* Since Bot API 5.5, it's now allowed to mention users by their ID in inline keyboards.
This will only work in Telegram versions released after December 7, 2021.
Older clients will display *unsupported message*.
Warning:
If your bot allows your arbitrary callback data, buttons whose callback data is a
non-hashable object will be come unhashable. Trying to evaluate ``hash(button)`` will
non-hashable object will become unhashable. Trying to evaluate ``hash(button)`` will
result in a :class:`TypeError`.
.. versionchanged:: 13.6
Args:
text (:obj:`str`): Label text on the button.
url (:obj:`str`, optional): HTTP or tg:// url to be opened when button is pressed.
url (:obj:`str`, optional): HTTP or tg:// url to be opened when the button is pressed.
Links ``tg://user?id=<user_id>`` can be used to mention a user by
their ID without using a username, if this is allowed by their privacy settings.
.. versionchanged:: 13.9
You can now mention a user using ``tg://user?id=<user_id>``.
login_url (:class:`telegram.LoginUrl`, optional): An HTTP URL used to automatically
authorize the user. Can be used as a replacement for the Telegram Login Widget.
callback_data (:obj:`str` | :obj:`Any`, optional): Data to be sent in a callback query to
@@ -76,12 +85,18 @@ class InlineKeyboardButton(TelegramObject):
be launched when the user presses the button. This type of button must always be
the ``first`` button in the first row.
pay (:obj:`bool`, optional): Specify :obj:`True`, to send a Pay button. This type of button
must always be the ``first`` button in the first row.
must always be the `first` button in the first row and can only be used in invoice
messages.
**kwargs (:obj:`dict`): Arbitrary keyword arguments.
Attributes:
text (:obj:`str`): Label text on the button.
url (:obj:`str`): Optional. HTTP or tg:// url to be opened when button is pressed.
url (:obj:`str`): Optional. HTTP or tg:// url to be opened when the button is pressed.
Links ``tg://user?id=<user_id>`` can be used to mention a user by
their ID without using a username, if this is allowed by their privacy settings.
.. versionchanged:: 13.9
You can now mention a user using ``tg://user?id=<user_id>``.
login_url (:class:`telegram.LoginUrl`): Optional. An HTTP URL used to automatically
authorize the user. Can be used as a replacement for the Telegram Login Widget.
callback_data (:obj:`str` | :obj:`object`): Optional. Data to be sent in a callback query
+43 -9
View File
@@ -90,12 +90,15 @@ class Message(TelegramObject):
Args:
message_id (:obj:`int`): Unique message identifier inside this chat.
from_user (:class:`telegram.User`, optional): Sender, empty for messages sent
to channels.
from_user (:class:`telegram.User`, optional): Sender of the message; empty for messages
sent to channels. For backward compatibility, this will contain a fake sender user in
non-channel chats, if the message was sent on behalf of a chat.
sender_chat (:class:`telegram.Chat`, optional): Sender of the message, sent on behalf of a
chat. The channel itself for channel messages. The supergroup itself for messages from
anonymous group administrators. The linked channel for messages automatically forwarded
to the discussion group.
chat. For example, the channel itself for channel posts, the supergroup itself for
messages from anonymous group administrators, the linked channel for messages
automatically forwarded to the discussion group. For backward compatibility,
:attr:`from_user` contains a fake sender user in non-channel chats, if the message was
sent on behalf of a chat.
date (:class:`datetime.datetime`): Date the message was sent in Unix time. Converted to
:class:`datetime.datetime`.
chat (:class:`telegram.Chat`): Conversation the message belongs to.
@@ -109,9 +112,17 @@ class Message(TelegramObject):
who disallow adding a link to their account in forwarded messages.
forward_date (:class:`datetime.datetime`, optional): For forwarded messages, date the
original message was sent in Unix time. Converted to :class:`datetime.datetime`.
is_automatic_forward (:obj:`bool`, optional): :obj:`True`, if the message is a channel post
that was automatically forwarded to the connected discussion group.
.. versionadded:: 13.9
reply_to_message (:class:`telegram.Message`, optional): For replies, the original message.
edit_date (:class:`datetime.datetime`, optional): Date the message was last edited in Unix
time. Converted to :class:`datetime.datetime`.
has_protected_content (:obj:`bool`, optional): :obj:`True`, if the message can't be
forwarded.
.. versionadded:: 13.9
media_group_id (:obj:`str`, optional): The unique identifier of a media message group this
message belongs to.
text (str, optional): For text messages, the actual UTF-8 text of the message, 0-4096
@@ -225,11 +236,12 @@ class Message(TelegramObject):
Attributes:
message_id (:obj:`int`): Unique message identifier inside this chat.
from_user (:class:`telegram.User`): Optional. Sender.
from_user (:class:`telegram.User`): Optional. Sender of the message; empty for messages
sent to channels. For backward compatibility, this will contain a fake sender user in
non-channel chats, if the message was sent on behalf of a chat.
sender_chat (:class:`telegram.Chat`): Optional. Sender of the message, sent on behalf of a
chat. The channel itself for channel messages. The supergroup itself for messages from
anonymous group administrators. The linked channel for messages automatically forwarded
to the discussion group.
chat. For backward compatibility, :attr:`from_user` contains a fake sender user in
non-channel chats, if the message was sent on behalf of a chat.
date (:class:`datetime.datetime`): Date the message was sent.
chat (:class:`telegram.Chat`): Conversation the message belongs to.
forward_from (:class:`telegram.User`): Optional. Sender of the original message.
@@ -238,10 +250,18 @@ class Message(TelegramObject):
forward_from_message_id (:obj:`int`): Optional. Identifier of the original message in the
channel.
forward_date (:class:`datetime.datetime`): Optional. Date the original message was sent.
is_automatic_forward (:obj:`bool`): Optional. :obj:`True`, if the message is a channel post
that was automatically forwarded to the connected discussion group.
.. versionadded:: 13.9
reply_to_message (:class:`telegram.Message`): Optional. For replies, the original message.
Note that the Message object in this field will not contain further
``reply_to_message`` fields even if it itself is a reply.
edit_date (:class:`datetime.datetime`): Optional. Date the message was last edited.
has_protected_content (:obj:`bool`): Optional. :obj:`True`, if the message can't be
forwarded.
.. versionadded:: 13.9
media_group_id (:obj:`str`): Optional. The unique identifier of a media message group this
message belongs to.
text (:obj:`str`): Optional. The actual UTF-8 text of the message.
@@ -390,6 +410,8 @@ class Message(TelegramObject):
'voice_chat_participants_invited',
'voice_chat_started',
'voice_chat_scheduled',
'is_automatic_forward',
'has_protected_content',
'_id_attrs',
)
@@ -492,6 +514,8 @@ class Message(TelegramObject):
voice_chat_participants_invited: VoiceChatParticipantsInvited = None,
message_auto_delete_timer_changed: MessageAutoDeleteTimerChanged = None,
voice_chat_scheduled: VoiceChatScheduled = None,
is_automatic_forward: bool = None,
has_protected_content: bool = None,
**_kwargs: Any,
):
# Required
@@ -504,8 +528,10 @@ class Message(TelegramObject):
self.forward_from = forward_from
self.forward_from_chat = forward_from_chat
self.forward_date = forward_date
self.is_automatic_forward = is_automatic_forward
self.reply_to_message = reply_to_message
self.edit_date = edit_date
self.has_protected_content = has_protected_content
self.text = text
self.entities = entities or []
self.caption_entities = caption_entities or []
@@ -1795,6 +1821,14 @@ class Message(TelegramObject):
For the documentation of the arguments, please see :meth:`telegram.Bot.forward_message`.
Note:
Since the release of Bot API 5.5 it can be impossible to forward messages from
some chats. Use the attributes :attr:`telegram.Message.has_protected_content` and
:attr:`telegram.Chat.has_protected_content` to check this.
As a workaround, it is still possible to use :meth:`copy`. However, this
behaviour is undocumented and might be changed by Telegram.
Returns:
:class:`telegram.Message`: On success, instance representing the message forwarded.
+29 -1
View File
@@ -31,6 +31,7 @@ from telegram import (
TelegramObject,
ChatMemberUpdated,
constants,
ChatJoinRequest,
)
from telegram.poll import PollAnswer
from telegram.utils.types import JSONDict
@@ -89,6 +90,12 @@ 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
:attr:`telegram.ChatPermissions.can_invite_users` administrator right in the chat to
receive these updates.
.. versionadded:: 13.8
**kwargs (:obj:`dict`): Arbitrary keyword arguments.
Attributes:
@@ -122,6 +129,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 +155,7 @@ class Update(TelegramObject):
'_effective_message',
'my_chat_member',
'chat_member',
'chat_join_request',
'_id_attrs',
)
@@ -198,6 +211,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 +236,7 @@ class Update(TelegramObject):
poll_answer: PollAnswer = None,
my_chat_member: ChatMemberUpdated = None,
chat_member: ChatMemberUpdated = None,
chat_join_request: ChatJoinRequest = None,
**_kwargs: Any,
):
# Required
@@ -237,6 +255,7 @@ class Update(TelegramObject):
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
@@ -286,6 +305,9 @@ class Update(TelegramObject):
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
@@ -325,6 +347,9 @@ class Update(TelegramObject):
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
@@ -335,7 +360,9 @@ class Update(TelegramObject):
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:
@@ -384,5 +411,6 @@ class Update(TelegramObject):
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)
+63
View File
@@ -22,6 +22,7 @@ from datetime import datetime
from typing import TYPE_CHECKING, Any, List, Optional, Union, Tuple
from telegram import TelegramObject, constants
from telegram.inline.inlinekeyboardbutton import InlineKeyboardButton
from telegram.utils.helpers import (
mention_html as util_mention_html,
DEFAULT_NONE,
@@ -233,6 +234,22 @@ class User(TelegramObject):
return util_mention_html(self.id, name)
return util_mention_html(self.id, self.full_name)
def mention_button(self, name: str = None) -> InlineKeyboardButton:
"""
Shortcut for::
InlineKeyboardButton(text=name, url=f"tg://user?id={update.effective_user.id}")
.. versionadded:: 13.9
Args:
name (:obj:`str`): The name used as a link for the user. Defaults to :attr:`full_name`.
Returns:
:class:`telegram.InlineKeyboardButton`: InlineButton with url set to the user mention
"""
return InlineKeyboardButton(text=name or self.full_name, url=f"tg://user?id={self.id}")
def pin_message(
self,
message_id: int,
@@ -1140,3 +1157,49 @@ class User(TelegramObject):
timeout=timeout,
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
)
+14 -9
View File
@@ -58,7 +58,7 @@ except ImportError: # pragma: no cover
raise
# pylint: disable=C0412
from telegram import InputFile, InputMedia, TelegramError
from telegram import InputFile, TelegramError
from telegram.error import (
BadRequest,
ChatMigrated,
@@ -325,13 +325,9 @@ class Request:
# Urllib3 doesn't like floats it seems
data[key] = str(val)
elif key == 'media':
# One media or multiple
if isinstance(val, InputMedia):
# Attach and set val to attached name
data[key] = val.to_json()
if isinstance(val.media, InputFile): # type: ignore
data[val.media.attach] = val.media.field_tuple # type: ignore
else:
files = True
# List of media
if isinstance(val, list):
# Attach and set val to attached name for all
media = []
for med in val:
@@ -343,7 +339,16 @@ class Request:
if "thumb" in media_dict:
data[med.thumb.attach] = med.thumb.field_tuple
data[key] = json.dumps(media)
files = True
# Single media
else:
# Attach and set val to attached name
media_dict = val.to_dict()
if isinstance(val.media, InputFile):
data[val.media.attach] = val.media.field_tuple
# if the file has a thumb, we also need to attach it to the data
if "thumb" in media_dict:
data[val.thumb.attach] = val.thumb.field_tuple
data[key] = json.dumps(media_dict)
elif isinstance(val, list):
# In case we're sending files, we need to json-dump lists manually
# As we can't know if that's the case, we just json-dump here
+1 -1
View File
@@ -20,5 +20,5 @@
from telegram import constants
__version__ = '13.7'
__version__ = '13.9'
bot_api_version = constants.BOT_API_VERSION # pylint: disable=C0103
+138 -5
View File
@@ -66,6 +66,14 @@ from tests.conftest import expect_bad_request, check_defaults_handling, GITHUB_A
from tests.bots import FALLBACKS
def to_camel_case(snake_str):
"""https://stackoverflow.com/a/19053800"""
components = snake_str.split('_')
# We capitalize the first letter of each component except the first one
# with the 'title' method and join them together.
return components[0] + ''.join(x.title() for x in components[1:])
class ExtBotSubClass(ExtBot):
# used for test_defaults_warning below
pass
@@ -710,6 +718,7 @@ class TestBot:
ChatAction.UPLOAD_VIDEO,
ChatAction.UPLOAD_VIDEO_NOTE,
ChatAction.UPLOAD_VOICE,
ChatAction.CHOOSE_STICKER,
],
)
def test_send_chat_action(self, bot, chat_id, chat_action):
@@ -1003,6 +1012,17 @@ class TestBot:
assert tz_bot.ban_chat_member(2, 32, until_date=until)
assert tz_bot.ban_chat_member(2, 32, until_date=until_timestamp)
def test_ban_chat_sender_chat(self, monkeypatch, bot):
# For now, we just test that we pass the correct data to TG
def make_assertion(url, data, *args, **kwargs):
chat_id = data['chat_id'] == 2
sender_chat_id = data['sender_chat_id'] == 32
return chat_id and sender_chat_id
monkeypatch.setattr(bot.request, 'post', make_assertion)
assert bot.ban_chat_sender_chat(2, 32)
monkeypatch.delattr(bot.request, 'post')
def test_kick_chat_member_warning(self, monkeypatch, bot, recwarn):
def test(url, data, *args, **kwargs):
chat_id = data['chat_id'] == 2
@@ -1028,6 +1048,15 @@ class TestBot:
assert bot.unban_chat_member(2, 32, only_if_banned=only_if_banned)
def test_unban_chat_sender_chat(self, monkeypatch, bot):
def make_assertion(url, data, *args, **kwargs):
chat_id = data['chat_id'] == 2
sender_chat_id = data['sender_chat_id'] == 32
return chat_id and sender_chat_id
monkeypatch.setattr(bot.request, 'post', make_assertion)
assert bot.unbanChatSenderChat(2, 32)
def test_set_chat_permissions(self, monkeypatch, bot, chat_permissions):
def test(url, data, *args, **kwargs):
chat_id = data['chat_id'] == 2
@@ -1688,6 +1717,37 @@ class TestBot:
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'])
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)
@pytest.mark.parametrize('datetime', argvalues=[True, False], ids=['datetime', 'integer'])
def test_advanced_chat_invite_links(self, bot, channel_id, datetime):
@@ -1712,12 +1772,29 @@ class TestBot:
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.name == 'NewName'
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)
assert revoked_invite_link.invite_link == invite_link.invite_link
assert revoked_invite_link.is_revoked is True
@@ -1742,16 +1819,49 @@ class TestBot:
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.name == 'NewName'
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)
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):
# 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)
def test_set_chat_photo(self, bot, channel_id):
def func():
@@ -2024,7 +2134,8 @@ class TestBot:
@flaky(3, 1)
@pytest.mark.parametrize('json_keyboard', [True, False])
def test_copy_message(self, monkeypatch, bot, chat_id, media_message, json_keyboard):
@pytest.mark.parametrize('caption', ["<b>Test</b>", '', None])
def test_copy_message(self, monkeypatch, bot, chat_id, media_message, json_keyboard, caption):
keyboard = InlineKeyboardMarkup(
[[InlineKeyboardButton(text="test", callback_data="test2")]]
)
@@ -2033,7 +2144,7 @@ class TestBot:
assert data["chat_id"] == chat_id
assert data["from_chat_id"] == chat_id
assert data["message_id"] == media_message.message_id
assert data["caption"] == "<b>Test</b>"
assert data.get("caption") == caption
assert data["parse_mode"] == ParseMode.HTML
assert data["reply_to_message_id"] == media_message.message_id
assert data["reply_markup"] == keyboard.to_json()
@@ -2046,7 +2157,7 @@ class TestBot:
chat_id,
from_chat_id=chat_id,
message_id=media_message.message_id,
caption="<b>Test</b>",
caption=caption,
caption_entities=[MessageEntity(MessageEntity.BOLD, 0, 4)],
parse_mode=ParseMode.HTML,
reply_to_message_id=media_message.message_id,
@@ -2454,3 +2565,25 @@ class TestBot:
assert 'Passing Defaults to telegram.Bot is deprecated.' in str(recwarn[-1].message)
else:
assert len(recwarn) == 0
def test_camel_case_redefinition_extbot(self):
invalid_camel_case_functions = []
for function_name, function in ExtBot.__dict__.items():
camel_case_function = getattr(ExtBot, to_camel_case(function_name), False)
if callable(function) and camel_case_function and camel_case_function is not function:
invalid_camel_case_functions.append(function_name)
assert invalid_camel_case_functions == []
def test_camel_case_bot(self):
not_available_camelcase_functions = []
for function_name, function in Bot.__dict__.items():
if (
function_name.startswith("_")
or not callable(function)
or function_name == "to_dict"
):
continue
camel_case_function = getattr(Bot, to_camel_case(function_name), False)
if not camel_case_function:
not_available_camelcase_functions.append(function_name)
assert not_available_camelcase_functions == []
+100
View File
@@ -41,6 +41,8 @@ def chat(bot):
bio=TestChat.bio,
linked_chat_id=TestChat.linked_chat_id,
location=TestChat.location,
has_private_forwards=True,
has_protected_content=True,
)
@@ -62,6 +64,8 @@ class TestChat:
bio = "I'm a Barbie Girl in a Barbie World"
linked_chat_id = 11880
location = ChatLocation(Location(123, 456), 'Barbie World')
has_protected_content = True
has_private_forwards = True
def test_slot_behaviour(self, chat, recwarn, mro_slots):
for attr in chat.__slots__:
@@ -84,6 +88,8 @@ class TestChat:
'slow_mode_delay': self.slow_mode_delay,
'message_auto_delete_time': self.message_auto_delete_time,
'bio': self.bio,
'has_protected_content': self.has_protected_content,
'has_private_forwards': self.has_private_forwards,
'linked_chat_id': self.linked_chat_id,
'location': self.location.to_dict(),
}
@@ -100,6 +106,8 @@ class TestChat:
assert chat.slow_mode_delay == self.slow_mode_delay
assert chat.message_auto_delete_time == self.message_auto_delete_time
assert chat.bio == self.bio
assert chat.has_protected_content == self.has_protected_content
assert chat.has_private_forwards == self.has_private_forwards
assert chat.linked_chat_id == self.linked_chat_id
assert chat.location.location == self.location.location
assert chat.location.address == self.location.address
@@ -117,6 +125,8 @@ class TestChat:
assert chat_dict['slow_mode_delay'] == chat.slow_mode_delay
assert chat_dict['message_auto_delete_time'] == chat.message_auto_delete_time
assert chat_dict['bio'] == chat.bio
assert chat_dict['has_private_forwards'] == chat.has_private_forwards
assert chat_dict['has_protected_content'] == chat.has_protected_content
assert chat_dict['linked_chat_id'] == chat.linked_chat_id
assert chat_dict['location'] == chat.location.to_dict()
@@ -225,6 +235,36 @@ class TestChat:
monkeypatch.setattr(chat.bot, 'ban_chat_member', make_assertion)
assert chat.ban_member(user_id=42, until_date=43)
def test_ban_sender_chat(self, monkeypatch, chat):
def make_assertion(*_, **kwargs):
chat_id = kwargs['chat_id'] == chat.id
sender_chat_id = kwargs['sender_chat_id'] == 42
return chat_id and sender_chat_id
assert check_shortcut_signature(
Chat.ban_sender_chat, Bot.ban_chat_sender_chat, ['chat_id'], []
)
assert check_shortcut_call(chat.ban_sender_chat, chat.bot, 'ban_chat_sender_chat')
assert check_defaults_handling(chat.ban_sender_chat, chat.bot)
monkeypatch.setattr(chat.bot, 'ban_chat_sender_chat', make_assertion)
assert chat.ban_sender_chat(42)
def test_ban_chat(self, monkeypatch, chat):
def make_assertion(*_, **kwargs):
chat_id = kwargs['chat_id'] == 42
sender_chat_id = kwargs['sender_chat_id'] == chat.id
return chat_id and sender_chat_id
assert check_shortcut_signature(
Chat.ban_chat, Bot.ban_chat_sender_chat, ['sender_chat_id'], []
)
assert check_shortcut_call(chat.ban_chat, chat.bot, 'ban_chat_sender_chat')
assert check_defaults_handling(chat.ban_chat, chat.bot)
monkeypatch.setattr(chat.bot, 'ban_chat_sender_chat', make_assertion)
assert chat.ban_chat(42)
def test_kick_member_warning(self, chat, monkeypatch, recwarn):
def make_assertion(*_, **kwargs):
chat_id = kwargs['chat_id'] == chat.id
@@ -252,6 +292,36 @@ class TestChat:
monkeypatch.setattr(chat.bot, 'unban_chat_member', make_assertion)
assert chat.unban_member(user_id=42, only_if_banned=only_if_banned)
def test_unban_sender_chat(self, monkeypatch, chat):
def make_assertion(*_, **kwargs):
chat_id = kwargs['chat_id'] == chat.id
sender_chat_id = kwargs['sender_chat_id'] == 42
return chat_id and sender_chat_id
assert check_shortcut_signature(
Chat.unban_sender_chat, Bot.unban_chat_sender_chat, ['chat_id'], []
)
assert check_shortcut_call(chat.unban_sender_chat, chat.bot, 'unban_chat_sender_chat')
assert check_defaults_handling(chat.unban_sender_chat, chat.bot)
monkeypatch.setattr(chat.bot, 'unban_chat_sender_chat', make_assertion)
assert chat.unban_sender_chat(42)
def test_unban_chat(self, monkeypatch, chat):
def make_assertion(*_, **kwargs):
chat_id = kwargs['chat_id'] == 42
sender_chat_id = kwargs['sender_chat_id'] == chat.id
return chat_id and sender_chat_id
assert check_shortcut_signature(
Chat.unban_chat, Bot.ban_chat_sender_chat, ['sender_chat_id'], []
)
assert check_shortcut_call(chat.unban_chat, chat.bot, 'unban_chat_sender_chat')
assert check_defaults_handling(chat.unban_chat, chat.bot)
monkeypatch.setattr(chat.bot, 'unban_chat_sender_chat', make_assertion)
assert chat.unban_chat(42)
@pytest.mark.parametrize('is_anonymous', [True, False, None])
def test_promote_member(self, monkeypatch, chat, is_anonymous):
def make_assertion(*_, **kwargs):
@@ -636,6 +706,36 @@ class TestChat:
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_)
+11 -1
View File
@@ -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 @@ class TestChatInviteLink:
'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 @@ class TestChatInviteLink:
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 @@ class TestChatInviteLink:
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)
+158
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()
+219
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
+10
View File
@@ -1718,6 +1718,16 @@ class TestFilters:
update.message.sender_chat = None
assert not Filters.sender_chat.channel(update)
def test_filters_is_automatic_forward(self, update):
assert not Filters.is_automatic_forward(update)
update.message.is_automatic_forward = True
assert Filters.is_automatic_forward(update)
def test_filters_has_protected_content(self, update):
assert not Filters.has_protected_content(update)
update.message.has_protected_content = True
assert Filters.has_protected_content(update)
def test_filters_invoice(self, update):
assert not Filters.invoice(update)
update.message.invoice = 'test'
+15
View File
@@ -591,6 +591,21 @@ class TestSendMediaGroup:
)
assert isinstance(new_message, Message)
def test_edit_message_media_with_thumb(
self, bot, chat_id, video_file, photo_file, monkeypatch # noqa: F811
):
def test(*args, **kwargs):
data = kwargs['fields']
video_check = data[input_video.media.attach] == input_video.media.field_tuple
thumb_check = data[input_video.thumb.attach] == input_video.thumb.field_tuple
result = video_check and thumb_check
raise Exception(f"Test was {'successful' if result else 'failing'}")
monkeypatch.setattr('telegram.utils.request.Request._request_wrapper', test)
input_video = InputMediaVideo(video_file, thumb=photo_file)
with pytest.raises(Exception, match='Test was successful'):
bot.edit_message_media(chat_id=chat_id, message_id=123, media=input_video)
@flaky(3, 1)
@pytest.mark.parametrize(
'default_bot', [{'parse_mode': ParseMode.HTML}], indirect=True, ids=['HTML-Bot']
+4
View File
@@ -180,6 +180,8 @@ def message(bot):
)
},
{'sender_chat': Chat(-123, 'discussion_channel')},
{'is_automatic_forward': True},
{'has_protected_content': True},
],
ids=[
'forwarded_user',
@@ -229,6 +231,8 @@ def message(bot):
'voice_chat_ended',
'voice_chat_participants_invited',
'sender_chat',
'is_automatic_forward',
'has_protected_content',
],
)
def message_params(bot, request):
+21 -3
View File
@@ -32,6 +32,7 @@ import logging
import os
import pickle
from collections import defaultdict
from collections.abc import Container
from time import sleep
from sys import version_info as py_ver
@@ -566,17 +567,32 @@ class TestBasePersistence:
def __init__(self):
self.bot = bot
self.not_in_dict = bot
self.not_in_slots = bot
def __eq__(self, other):
if isinstance(other, CustomSlottedClass):
return self.bot is other.bot and self.not_in_dict is other.not_in_dict
return self.bot is other.bot and self.not_in_slots is other.not_in_slots
return False
class DictNotInSlots(Container):
"""This classes parent has slots, but __dict__ is not in those slots."""
def __init__(self):
self.bot = bot
def __contains__(self, item):
return True
def __eq__(self, other):
if isinstance(other, DictNotInSlots):
return self.bot is other.bot
return False
class CustomClass:
def __init__(self):
self.bot = bot
self.slotted_object = CustomSlottedClass()
self.dict_not_in_slots_object = DictNotInSlots()
self.list_ = [1, 2, bot]
self.tuple_ = tuple(self.list_)
self.set_ = set(self.list_)
@@ -589,7 +605,8 @@ class TestBasePersistence:
cc = CustomClass()
cc.bot = BasePersistence.REPLACED_BOT
cc.slotted_object.bot = BasePersistence.REPLACED_BOT
cc.slotted_object.not_in_dict = BasePersistence.REPLACED_BOT
cc.slotted_object.not_in_slots = BasePersistence.REPLACED_BOT
cc.dict_not_in_slots_object.bot = BasePersistence.REPLACED_BOT
cc.list_ = [1, 2, BasePersistence.REPLACED_BOT]
cc.tuple_ = tuple(cc.list_)
cc.set_ = set(cc.list_)
@@ -603,6 +620,7 @@ class TestBasePersistence:
return (
self.bot is other.bot
and self.slotted_object == other.slotted_object
and self.dict_not_in_slots_object == other.dict_not_in_slots_object
and self.list_ == other.list_
and self.tuple_ == other.tuple_
and self.set_ == other.set_
+14 -1
View File
@@ -34,6 +34,7 @@ from telegram import (
PollOption,
ChatMemberUpdated,
ChatMember,
ChatJoinRequest,
)
from telegram.poll import PollAnswer
from telegram.utils.helpers import from_timestamp
@@ -47,6 +48,14 @@ chat_member_updated = ChatMemberUpdated(
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 @@ params = [
{'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 @@ all_types = (
'poll_answer',
'my_chat_member',
'chat_member',
'chat_join_request',
)
ids = all_types + ('callback_query_without_message',)
@@ -171,6 +183,7 @@ class TestUpdate:
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:
+42 -1
View File
@@ -18,7 +18,7 @@
# along with this program. If not, see [http://www.gnu.org/licenses/].
import pytest
from telegram import Update, User, Bot
from telegram import Update, User, Bot, InlineKeyboardButton
from telegram.utils.helpers import escape_markdown
from tests.conftest import check_shortcut_signature, check_shortcut_call, check_defaults_handling
@@ -430,6 +430,40 @@ class TestUser:
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 = '<a href="tg://user?id={}">{}</a>'
@@ -439,6 +473,13 @@ class TestUser:
)
assert user.mention_html(user.username) == expected.format(user.id, user.username)
def test_mention_button(self, user):
expected_name = InlineKeyboardButton(text="Bob", url=f"tg://user?id={user.id}")
expected_full = InlineKeyboardButton(text=user.full_name, url=f"tg://user?id={user.id}")
assert user.mention_button("Bob") == expected_name
assert user.mention_button() == expected_full
def test_mention_markdown(self, user):
expected = '[{}](tg://user?id={})'