Compare commits

..

18 Commits

Author SHA1 Message Date
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
Hinrich Mahler bcec6f03cb Bump version to v13.7 2021-07-01 18:03:38 +02:00
Bibo-Joshi ed147813ab API 5.3 (#2572)
* BotCommandScopes

* pre-commit

* typo

Co-authored-by: Harshil <37377066+harshil21@users.noreply.github.com>

* rename kickChatMember & getChatMembersCount method

Signed-off-by: starry69 <starry369126@outlook.com>

* add `language_code` and `scope` to `get/set_my_commands`

and add `delete_my_commands()`

* add `input_field_placeholder` to replykeyboardmarkup.py and forcereply.py

also improved/fixed docs along the way

* showcase `input_field_placeholder` in conversationbot.py

* review 1

'i will not go away' has gone away 😢

* deprecate `Bot.commands` and make sure its only used for default scope

* review 2 (use constants for scope)

* Review

Signed-off-by: starry69 <starry369126@outlook.com>

* doc updates

* New ChatMember classes

Signed-off-by: starry69 <starry369126@outlook.com>

* Address review

Signed-off-by: starry69 <starry369126@outlook.com>

* add versionadded tags again

Signed-off-by: starry69 <starry369126@outlook.com>

* Improve tests & add a deprecation note to ChatMember

* test_official

* Documentation tweaks

* Bump bot api version number

* but bot

* Rename chat shortcuts

Signed-off-by: starry69 <starry369126@outlook.com>

* deepsource

Signed-off-by: starry69 <starry369126@outlook.com>

* add missing slot in botcommandscope & missing slot tests

Co-authored-by: Harshil <37377066+harshil21@users.noreply.github.com>
Co-authored-by: starry69 <starry369126@outlook.com>
Co-authored-by: Harshil <ilovebhagwan@gmail.com>
2021-07-01 17:45:19 +02:00
Bibo-Joshi 4315225642 Type Hinting Fixes (#2552)
* Fixe overload signatures for ContextTypes

* more fixing for contexttypes
2021-07-01 17:43:59 +02:00
Bibo-Joshi a75dffd4a8 Doc Fixes (#2551)
* Document ExtBot.insert_callbakc_data

* fix duplicate object descriptions

Fixed by removing `:undoc-members:` in the affected classes.
Closes https://github.com/sphinx-doc/sphinx/issues/9294

* fix incorrect shortcut docstrings in user.py

* fix object type in forcereply.py

* fix discuss bot link in loginurl.py

* document that message is None for (my)_chat_member

in `effective_message`

* numerous persistence rendering fixes

* move docstring from property setter to property

* Revert "fix object type in forcereply.py"

This reverts commit 012663e0c3.

* Document comparison of jobs

* Update min python version to 3.6.8

* remove old note from chat.py + some return msg fixes

* fix colon placement

Co-authored-by: Harshil <ilovebhagwan@gmail.com>
Co-authored-by: Harshil <37377066+harshil21@users.noreply.github.com>
2021-07-01 17:34:23 +02:00
Harshil fce2993d21 Improve Deprecation Warning for __slots__ (#2574)
* add stacklevel to `set_new_attribute_deprecated`

* detail warning message and change stacklevel
2021-06-29 18:10:08 +02:00
Harshil 9aec8deec6 Stabilize CI (#2575)
* attempt 'surely this one' on fixing test_idle and test_depr_warnings

* remove unused filterwarnings
2021-06-26 22:19:59 +02:00
Bibo-Joshi ec3026673b Fix Coverage Configuration (#2571)
* remove possibly malicious line from config

* exclude overload signatures from coverage
2021-06-25 09:02:46 +02:00
zeroone2numeral2 105f1ccdb5 Better Exception-Handling for BasePersistence.replace/insert_bot (#2564)
* Catch exceptions raised while copying __dict__/__slots__ in BasePersistence.replace/insert_bot()

Also updated the docstrings to reflect the changes in behavior with unexpected errors

* Tests: added to CustomClass immutable object that would trigger a setattr() exception

* Tests: added new uuid_ property to own CustomClass methods

* Updated AUTHORS.rst

* Revert "Tests: added new uuid_ property to own CustomClass methods"

This reverts commit 9e67463cf7.

* Revert "Tests: added to CustomClass immutable object that would trigger a setattr() exception"

This reverts commit 1c258304

* Removed unneeded Exception cast to string

f-string will perform the string-ification on their own

* Removed another unneeded Exception cast to string

* Added test to parse unparsable objects in __dict__ or __slots__

* Applied black and pylint style suggestions

All lint tests passed

* Fix typo

Co-authored-by: Harshil <37377066+harshil21@users.noreply.github.com>

Co-authored-by: Bibo-Joshi <hinrich.mahler@freenet.de>
Co-authored-by: Harshil <37377066+harshil21@users.noreply.github.com>
2021-06-20 22:14:05 +02:00
Bibo-Joshi 52ce03929b Fix Bug in BasePersistence.insert/replace_bot for Objects with __dict__ in their slots (#2561)
* Handle objects with __dict__ in __slots__

* Rework
2021-06-13 15:07:40 +02:00
Bibo-Joshi ac4768155f Remove Incorrect Warning About Defaults and ExtBot (#2553)
* Don't throw warning when passing defaults to ExtBot

* Review
2021-06-10 12:03:44 +02:00
Harshil d08172b4b0 Remove Deprecated pass_args from Deeplinking Example (#2550) 2021-06-07 09:05:17 +02:00
75 changed files with 3479 additions and 381 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
+4
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>`_
@@ -106,6 +109,7 @@ The following wonderful people contributed directly or indirectly to this projec
- `Vorobjev Simon <https://github.com/simonvorobjev>`_
- `Wagner Macedo <https://github.com/wagnerluis1982>`_
- `wjt <https://github.com/wjt>`_
- `zeroone2numeral2 <https://github.com/zeroone2numeral2>`_
- `zeshuaro <https://github.com/zeshuaro>`_
Please add yourself here alphabetically when you submit your first pull request.
+61
View File
@@ -2,6 +2,67 @@
Changelog
=========
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*
This is the technical changelog for version 13.7. More elaborate release notes can be found in the news channel `@pythontelegrambotchannel <https://t.me/pythontelegrambotchannel>`_.
**Major Changes:**
- Full support for Bot API 5.3 (`#2572`_)
**Bug Fixes:**
- Fix Bug in ``BasePersistence.insert/replace_bot`` for Objects with ``__dict__`` in their slots (`#2561`_)
- Remove Incorrect Warning About ``Defaults`` and ``ExtBot`` (`#2553`_)
**Minor changes, CI improvements, Doc fixes and Type hinting:**
- Type Hinting Fixes (`#2552`_)
- Doc Fixes (`#2551`_)
- Improve Deprecation Warning for ``__slots__`` (`#2574`_)
- Stabilize CI (`#2575`_)
- Fix Coverage Configuration (`#2571`_)
- Better Exception-Handling for ``BasePersistence.replace/insert_bot`` (`#2564`_)
- Remove Deprecated ``pass_args`` from Deeplinking Example (`#2550`_)
.. _`#2572`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2572
.. _`#2561`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2561
.. _`#2553`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2553
.. _`#2552`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2552
.. _`#2551`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2551
.. _`#2574`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2574
.. _`#2575`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2575
.. _`#2571`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2571
.. _`#2564`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2564
.. _`#2550`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2550
Version 13.6
============
*Released 2021-06-06*
+3 -3
View File
@@ -20,7 +20,7 @@ We have a vibrant community of developers helping each other in our `Telegram gr
:target: https://pypi.org/project/python-telegram-bot/
:alt: Supported Python versions
.. image:: https://img.shields.io/badge/Bot%20API-5.2-blue?logo=telegram
.. image:: https://img.shields.io/badge/Bot%20API-5.4-blue?logo=telegram
:target: https://core.telegram.org/bots/api-changelog
:alt: Supported Bot API versions
@@ -93,7 +93,7 @@ Introduction
This library provides a pure Python interface for the
`Telegram Bot API <https://core.telegram.org/bots/api>`_.
It's compatible with Python versions 3.6.2+. PTB might also work on `PyPy <http://pypy.org/>`_, though there have been a lot of issues before. Hence, PyPy is not officially supported.
It's compatible with Python versions 3.6.8+. PTB might also work on `PyPy <http://pypy.org/>`_, though there have been a lot of issues before. Hence, PyPy is not officially supported.
In addition to the pure API implementation, this library features a number of high-level classes to
make the development of bots easy and straightforward. These classes are contained in the
@@ -111,7 +111,7 @@ Installing both ``python-telegram-bot`` and ``python-telegram-bot-raw`` in conju
Telegram API support
====================
All types and methods of the Telegram Bot API **5.2** are supported.
All types and methods of the Telegram Bot API **5.4** are supported.
==========
Installing
+3 -3
View File
@@ -20,7 +20,7 @@ We have a vibrant community of developers helping each other in our `Telegram gr
:target: https://pypi.org/project/python-telegram-bot-raw/
:alt: Supported Python versions
.. image:: https://img.shields.io/badge/Bot%20API-5.2-blue?logo=telegram
.. image:: https://img.shields.io/badge/Bot%20API-5.4-blue?logo=telegram
:target: https://core.telegram.org/bots/api-changelog
:alt: Supported Bot API versions
@@ -91,7 +91,7 @@ Introduction
This library provides a pure Python, lightweight interface for the
`Telegram Bot API <https://core.telegram.org/bots/api>`_.
It's compatible with Python versions 3.6.2+. PTB-Raw might also work on `PyPy <http://pypy.org/>`_, though there have been a lot of issues before. Hence, PyPy is not officially supported.
It's compatible with Python versions 3.6.8+. PTB-Raw might also work on `PyPy <http://pypy.org/>`_, though there have been a lot of issues before. Hence, PyPy is not officially supported.
``python-telegram-bot-raw`` is part of the `python-telegram-bot <https://python-telegram-bot.org>`_ ecosystem and provides the pure API functionality extracted from PTB. It therefore does *not* have independent release schedules, changelogs or documentation. Please consult the PTB resources.
@@ -105,7 +105,7 @@ Installing both ``python-telegram-bot`` and ``python-telegram-bot-raw`` in conju
Telegram API support
====================
All types and methods of the Telegram Bot API **5.2** are supported.
All types and methods of the Telegram Bot API **5.4** are supported.
==========
Installing
+2 -2
View File
@@ -60,9 +60,9 @@ author = u'Leandro Toledo'
# built documents.
#
# The short X.Y version.
version = '13.6' # telegram.__version__[:3]
version = '13.8' # telegram.__version__[:3]
# The full version, including alpha/beta/rc tags.
release = '13.6' # telegram.__version__
release = '13.8' # 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/botcommandscope.py
telegram.BotCommandScope
========================
.. autoclass:: telegram.BotCommandScope
:members:
:show-inheritance:
@@ -0,0 +1,8 @@
:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/botcommandscope.py
telegram.BotCommandScopeAllChatAdministrators
=============================================
.. autoclass:: telegram.BotCommandScopeAllChatAdministrators
:members:
:show-inheritance:
@@ -0,0 +1,8 @@
:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/botcommandscope.py
telegram.BotCommandScopeAllGroupChats
=======================================
.. autoclass:: telegram.BotCommandScopeAllGroupChats
:members:
:show-inheritance:
@@ -0,0 +1,8 @@
:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/botcommandscope.py
telegram.BotCommandScopeAllPrivateChats
=======================================
.. autoclass:: telegram.BotCommandScopeAllPrivateChats
:members:
:show-inheritance:
@@ -0,0 +1,8 @@
:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/botcommandscope.py
telegram.BotCommandScopeChat
============================
.. autoclass:: telegram.BotCommandScopeChat
:members:
:show-inheritance:
@@ -0,0 +1,8 @@
:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/botcommandscope.py
telegram.BotCommandScopeChatAdministrators
==========================================
.. autoclass:: telegram.BotCommandScopeChatAdministrators
:members:
:show-inheritance:
@@ -0,0 +1,8 @@
:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/botcommandscope.py
telegram.BotCommandScopeChatMember
==================================
.. autoclass:: telegram.BotCommandScopeChatMember
:members:
:show-inheritance:
@@ -0,0 +1,8 @@
:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/botcommandscope.py
telegram.BotCommandScopeDefault
===============================
.. autoclass:: telegram.BotCommandScopeDefault
:members:
:show-inheritance:
@@ -0,0 +1,8 @@
:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/chatmember.py
telegram.ChatMemberAdministrator
================================
.. autoclass:: telegram.ChatMemberAdministrator
:members:
:show-inheritance:
@@ -0,0 +1,8 @@
:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/chatmember.py
telegram.ChatMemberBanned
=========================
.. autoclass:: telegram.ChatMemberBanned
:members:
:show-inheritance:
+8
View File
@@ -0,0 +1,8 @@
:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/chatmember.py
telegram.ChatMemberLeft
=======================
.. autoclass:: telegram.ChatMemberLeft
:members:
:show-inheritance:
@@ -0,0 +1,8 @@
:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/chatmember.py
telegram.ChatMemberMember
=========================
.. autoclass:: telegram.ChatMemberMember
:members:
:show-inheritance:
+9
View File
@@ -0,0 +1,9 @@
:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/chatmember.py
telegram.ChatMemberOwner
========================
.. autoclass:: telegram.ChatMemberOwner
:members:
:show-inheritance:
@@ -0,0 +1,8 @@
:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/chatmember.py
telegram.ChatMemberRestricted
=============================
.. autoclass:: telegram.ChatMemberRestricted
:members:
:show-inheritance:
+2
View File
@@ -5,3 +5,5 @@ telegram.ext.ExtBot
.. autoclass:: telegram.ext.ExtBot
:show-inheritance:
.. autofunction:: telegram.ext.ExtBot.insert_callback_data
-1
View File
@@ -5,5 +5,4 @@ telegram.ext.Handler
.. autoclass:: telegram.ext.Handler
:members:
:undoc-members:
:show-inheritance:
+14
View File
@@ -7,12 +7,26 @@ telegram package
telegram.audio
telegram.bot
telegram.botcommand
telegram.botcommandscope
telegram.botcommandscopedefault
telegram.botcommandscopeallprivatechats
telegram.botcommandscopeallgroupchats
telegram.botcommandscopeallchatadministrators
telegram.botcommandscopechat
telegram.botcommandscopechatadministrators
telegram.botcommandscopechatmember
telegram.callbackquery
telegram.chat
telegram.chataction
telegram.chatinvitelink
telegram.chatlocation
telegram.chatmember
telegram.chatmemberowner
telegram.chatmemberadministrator
telegram.chatmembermember
telegram.chatmemberrestricted
telegram.chatmemberleft
telegram.chatmemberbanned
telegram.chatmemberupdated
telegram.chatpermissions
telegram.chatphoto
-1
View File
@@ -5,5 +5,4 @@ telegram.Update
.. autoclass:: telegram.Update
:members:
:undoc-members:
:show-inheritance:
-1
View File
@@ -5,5 +5,4 @@ telegram.User
.. autoclass:: telegram.User
:members:
:undoc-members:
:show-inheritance:
+3 -1
View File
@@ -44,7 +44,9 @@ def start(update: Update, context: CallbackContext) -> int:
'Hi! My name is Professor Bot. I will hold a conversation with you. '
'Send /cancel to stop talking to me.\n\n'
'Are you a boy or a girl?',
reply_markup=ReplyKeyboardMarkup(reply_keyboard, one_time_keyboard=True),
reply_markup=ReplyKeyboardMarkup(
reply_keyboard, one_time_keyboard=True, input_field_placeholder='Boy or Girl?'
),
)
return GENDER
+1 -1
View File
@@ -124,7 +124,7 @@ def main() -> None:
# We can also pass on the deep-linking payload
dispatcher.add_handler(
CommandHandler("start", deep_linked_level_3, Filters.regex(USING_ENTITIES), pass_args=True)
CommandHandler("start", deep_linked_level_3, Filters.regex(USING_ENTITIES))
)
# Possible with inline keyboard buttons as well
+3 -2
View File
@@ -43,8 +43,9 @@ omit =
[coverage:report]
exclude_lines =
pragma: no cover
@overload
if TYPE_CHECKING:
...
[mypy]
warn_unused_ignores = True
@@ -59,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
+35 -1
View File
@@ -25,7 +25,16 @@ from .files.chatphoto import ChatPhoto
from .chat import Chat
from .chatlocation import ChatLocation
from .chatinvitelink import ChatInviteLink
from .chatmember import ChatMember
from .chatjoinrequest import ChatJoinRequest
from .chatmember import (
ChatMember,
ChatMemberOwner,
ChatMemberAdministrator,
ChatMemberMember,
ChatMemberRestricted,
ChatMemberLeft,
ChatMemberBanned,
)
from .chatmemberupdated import ChatMemberUpdated
from .chatpermissions import ChatPermissions
from .files.photosize import PhotoSize
@@ -153,6 +162,16 @@ from .passport.credentials import (
FileCredentials,
TelegramDecryptionError,
)
from .botcommandscope import (
BotCommandScope,
BotCommandScopeDefault,
BotCommandScopeAllPrivateChats,
BotCommandScopeAllGroupChats,
BotCommandScopeAllChatAdministrators,
BotCommandScopeChat,
BotCommandScopeChatAdministrators,
BotCommandScopeChatMember,
)
from .bot import Bot
from .version import __version__, bot_api_version # noqa: F401
@@ -163,13 +182,28 @@ __all__ = ( # Keep this alphabetically ordered
'Audio',
'Bot',
'BotCommand',
'BotCommandScope',
'BotCommandScopeAllChatAdministrators',
'BotCommandScopeAllGroupChats',
'BotCommandScopeAllPrivateChats',
'BotCommandScopeChat',
'BotCommandScopeChatAdministrators',
'BotCommandScopeChatMember',
'BotCommandScopeDefault',
'CallbackGame',
'CallbackQuery',
'Chat',
'ChatAction',
'ChatInviteLink',
'ChatJoinRequest',
'ChatLocation',
'ChatMember',
'ChatMemberOwner',
'ChatMemberAdministrator',
'ChatMemberMember',
'ChatMemberRestricted',
'ChatMemberLeft',
'ChatMemberBanned',
'ChatMemberUpdated',
'ChatPermissions',
'ChatPhoto',
+315 -18
View File
@@ -57,6 +57,7 @@ from telegram import (
Animation,
Audio,
BotCommand,
BotCommandScope,
Chat,
ChatMember,
ChatPermissions,
@@ -400,7 +401,20 @@ class Bot(TelegramObject):
@property
def commands(self) -> List[BotCommand]:
"""List[:class:`BotCommand`]: Bot's commands."""
"""
List[:class:`BotCommand`]: Bot's commands as available in the default scope.
.. deprecated:: 13.7
This property has been deprecated since there can be different commands available for
different scopes.
"""
warnings.warn(
"Bot.commands has been deprecated since there can be different command "
"lists for different scopes.",
TelegramDeprecationWarning,
stacklevel=2,
)
if self._commands is None:
self._commands = self.get_my_commands()
return self._commands
@@ -2312,11 +2326,43 @@ class Bot(TelegramObject):
revoke_messages: bool = None,
) -> bool:
"""
Use this method to kick a user from a group, supergroup or a channel. In the case of
Deprecated, use :func:`~telegram.Bot.ban_chat_member` instead.
.. deprecated:: 13.7
"""
warnings.warn(
'`bot.kick_chat_member` is deprecated. Use `bot.ban_chat_member` instead.',
TelegramDeprecationWarning,
stacklevel=2,
)
return self.ban_chat_member(
chat_id=chat_id,
user_id=user_id,
timeout=timeout,
until_date=until_date,
api_kwargs=api_kwargs,
revoke_messages=revoke_messages,
)
@log
def ban_chat_member(
self,
chat_id: Union[str, int],
user_id: Union[str, int],
timeout: ODVInput[float] = DEFAULT_NONE,
until_date: Union[int, datetime] = None,
api_kwargs: JSONDict = None,
revoke_messages: bool = None,
) -> bool:
"""
Use this method to ban a user from a group, supergroup or a channel. In the case of
supergroups and channels, the user will not be able to return to the group on their own
using invite links, etc., unless unbanned first. The bot must be an administrator in the
chat for this to work and must have the appropriate admin rights.
.. versionadded:: 13.7
Args:
chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target group or username
of the target supergroup or channel (in the format ``@channelusername``).
@@ -2358,7 +2404,7 @@ class Bot(TelegramObject):
if revoke_messages is not None:
data['revoke_messages'] = revoke_messages
result = self._post('kickChatMember', data, timeout=timeout, api_kwargs=api_kwargs)
result = self._post('banChatMember', data, timeout=timeout, api_kwargs=api_kwargs)
return result # type: ignore[return-value]
@@ -3061,9 +3107,31 @@ class Bot(TelegramObject):
chat_id: Union[str, int],
timeout: ODVInput[float] = DEFAULT_NONE,
api_kwargs: JSONDict = None,
) -> int:
"""
Deprecated, use :func:`~telegram.Bot.get_chat_member_count` instead.
.. deprecated:: 13.7
"""
warnings.warn(
'`bot.get_chat_members_count` is deprecated. '
'Use `bot.get_chat_member_count` instead.',
TelegramDeprecationWarning,
stacklevel=2,
)
return self.get_chat_member_count(chat_id=chat_id, timeout=timeout, api_kwargs=api_kwargs)
@log
def get_chat_member_count(
self,
chat_id: Union[str, int],
timeout: ODVInput[float] = DEFAULT_NONE,
api_kwargs: JSONDict = None,
) -> int:
"""Use this method to get the number of members in a chat.
.. versionadded:: 13.7
Args:
chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username
of the target supergroup or channel (in the format ``@channelusername``).
@@ -3082,7 +3150,7 @@ class Bot(TelegramObject):
"""
data: JSONDict = {'chat_id': chat_id}
result = self._post('getChatMembersCount', data, timeout=timeout, api_kwargs=api_kwargs)
result = self._post('getChatMemberCount', data, timeout=timeout, api_kwargs=api_kwargs)
return result # type: ignore[return-value]
@@ -3917,6 +3985,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
@@ -3939,6 +4009,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`
@@ -3947,6 +4025,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,
}
@@ -3961,6 +4044,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]
@@ -3974,11 +4063,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:
@@ -3996,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`
@@ -4004,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, 'invite_link': invite_link}
if expire_date is not None:
@@ -4016,6 +4126,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]
@@ -4058,6 +4174,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,
@@ -4959,10 +5149,15 @@ class Bot(TelegramObject):
@log
def get_my_commands(
self, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None
self,
timeout: ODVInput[float] = DEFAULT_NONE,
api_kwargs: JSONDict = None,
scope: BotCommandScope = None,
language_code: str = None,
) -> List[BotCommand]:
"""
Use this method to get the current list of the bot's commands.
Use this method to get the current list of the bot's commands for the given scope and user
language.
Args:
timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as
@@ -4970,19 +5165,39 @@ class Bot(TelegramObject):
the connection pool).
api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the
Telegram API.
scope (:class:`telegram.BotCommandScope`, optional): A JSON-serialized object,
describing scope of users. Defaults to :class:`telegram.BotCommandScopeDefault`.
.. versionadded:: 13.7
language_code (:obj:`str`, optional): A two-letter ISO 639-1 language code or an empty
string.
.. versionadded:: 13.7
Returns:
List[:class:`telegram.BotCommand]`: On success, the commands set for the bot
List[:class:`telegram.BotCommand`]: On success, the commands set for the bot. An empty
list is returned if commands are not set.
Raises:
:class:`telegram.error.TelegramError`
"""
result = self._post('getMyCommands', timeout=timeout, api_kwargs=api_kwargs)
data: JSONDict = {}
self._commands = BotCommand.de_list(result, self) # type: ignore[assignment,arg-type]
if scope:
data['scope'] = scope.to_dict()
return self._commands # type: ignore[return-value]
if language_code:
data['language_code'] = language_code
result = self._post('getMyCommands', data, timeout=timeout, api_kwargs=api_kwargs)
if (scope is None or scope.type == scope.DEFAULT) and language_code is None:
self._commands = BotCommand.de_list(result, self) # type: ignore[assignment,arg-type]
return self._commands # type: ignore[return-value]
return BotCommand.de_list(result, self) # type: ignore[return-value,arg-type]
@log
def set_my_commands(
@@ -4990,9 +5205,13 @@ class Bot(TelegramObject):
commands: List[Union[BotCommand, Tuple[str, str]]],
timeout: ODVInput[float] = DEFAULT_NONE,
api_kwargs: JSONDict = None,
scope: BotCommandScope = None,
language_code: str = None,
) -> bool:
"""
Use this method to change the list of the bot's commands.
Use this method to change the list of the bot's commands. See the
`Telegram docs <https://core.telegram.org/bots#commands>`_ for more details about bot
commands.
Args:
commands (List[:class:`BotCommand` | (:obj:`str`, :obj:`str`)]): A JSON-serialized list
@@ -5003,9 +5222,20 @@ class Bot(TelegramObject):
the connection pool).
api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the
Telegram API.
scope (:class:`telegram.BotCommandScope`, optional): A JSON-serialized object,
describing scope of users for which the commands are relevant. Defaults to
:class:`telegram.BotCommandScopeDefault`.
.. versionadded:: 13.7
language_code (:obj:`str`, optional): A two-letter ISO 639-1 language code. If empty,
commands will be applied to all users from the given scope, for whose language
there are no dedicated commands.
.. versionadded:: 13.7
Returns:
:obj:`True`: On success
:obj:`bool`: On success, :obj:`True` is returned.
Raises:
:class:`telegram.error.TelegramError`
@@ -5015,11 +5245,68 @@ class Bot(TelegramObject):
data: JSONDict = {'commands': [c.to_dict() for c in cmds]}
if scope:
data['scope'] = scope.to_dict()
if language_code:
data['language_code'] = language_code
result = self._post('setMyCommands', data, timeout=timeout, api_kwargs=api_kwargs)
# Set commands. No need to check for outcome.
# Set commands only for default scope. No need to check for outcome.
# If request failed, we won't come this far
self._commands = cmds
if (scope is None or scope.type == scope.DEFAULT) and language_code is None:
self._commands = cmds
return result # type: ignore[return-value]
@log
def delete_my_commands(
self,
scope: BotCommandScope = None,
language_code: str = None,
api_kwargs: JSONDict = None,
timeout: ODVInput[float] = DEFAULT_NONE,
) -> bool:
"""
Use this method to delete the list of the bot's commands for the given scope and user
language. After deletion,
`higher level commands <https://core.telegram.org/bots/api#determining-list-of-commands>`_
will be shown to affected users.
.. versionadded:: 13.7
Args:
scope (:class:`telegram.BotCommandScope`, optional): A JSON-serialized object,
describing scope of users for which the commands are relevant. Defaults to
:class:`telegram.BotCommandScopeDefault`.
language_code (:obj:`str`, optional): A two-letter ISO 639-1 language code. If empty,
commands will be applied to all users from the given scope, for whose language
there are no dedicated commands.
timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as
the read timeout from the server (instead of the one specified during creation of
the connection pool).
api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the
Telegram API.
Returns:
:obj:`bool`: On success, :obj:`True` is returned.
Raises:
:class:`telegram.error.TelegramError`
"""
data: JSONDict = {}
if scope:
data['scope'] = scope.to_dict()
if language_code:
data['language_code'] = language_code
result = self._post('deleteMyCommands', data, timeout=timeout, api_kwargs=api_kwargs)
if (scope is None or scope.type == scope.DEFAULT) and language_code is None:
self._commands = []
return result # type: ignore[return-value]
@@ -5131,7 +5418,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
@@ -5210,6 +5497,8 @@ class Bot(TelegramObject):
"""Alias for :meth:`get_user_profile_photos`"""
getFile = get_file
"""Alias for :meth:`get_file`"""
banChatMember = ban_chat_member
"""Alias for :meth:`ban_chat_member`"""
kickChatMember = kick_chat_member
"""Alias for :meth:`kick_chat_member`"""
unbanChatMember = unban_chat_member
@@ -5242,6 +5531,8 @@ class Bot(TelegramObject):
"""Alias for :meth:`set_chat_sticker_set`"""
deleteChatStickerSet = delete_chat_sticker_set
"""Alias for :meth:`delete_chat_sticker_set`"""
getChatMemberCount = get_chat_member_count
"""Alias for :meth:`get_chat_member_count`"""
getChatMembersCount = get_chat_members_count
"""Alias for :meth:`get_chat_members_count`"""
getWebhookInfo = get_webhook_info
@@ -5267,11 +5558,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
@@ -5312,6 +5607,8 @@ class Bot(TelegramObject):
"""Alias for :meth:`get_my_commands`"""
setMyCommands = set_my_commands
"""Alias for :meth:`set_my_commands`"""
deleteMyCommands = delete_my_commands
"""Alias for :meth:`delete_my_commands`"""
logOut = log_out
"""Alias for :meth:`log_out`"""
copyMessage = copy_message
+263
View File
@@ -0,0 +1,263 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
# Copyright (C) 2021
# Leandro Toledo de Souza <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/].
# pylint: disable=W0622
"""This module contains objects representing Telegram bot command scopes."""
from typing import Any, Union, Optional, TYPE_CHECKING, Dict, Type
from telegram import TelegramObject, constants
from telegram.utils.types import JSONDict
if TYPE_CHECKING:
from telegram import Bot
class BotCommandScope(TelegramObject):
"""Base class for objects that represent the scope to which bot commands are applied.
Currently, the following 7 scopes are supported:
* :class:`telegram.BotCommandScopeDefault`
* :class:`telegram.BotCommandScopeAllPrivateChats`
* :class:`telegram.BotCommandScopeAllGroupChats`
* :class:`telegram.BotCommandScopeAllChatAdministrators`
* :class:`telegram.BotCommandScopeChat`
* :class:`telegram.BotCommandScopeChatAdministrators`
* :class:`telegram.BotCommandScopeChatMember`
Objects of this class are comparable in terms of equality. Two objects of this class are
considered equal, if their :attr:`type` is equal. For subclasses with additional attributes,
the notion of equality is overridden.
Note:
Please see the `official docs`_ on how Telegram determines which commands to display.
.. _`official docs`: https://core.telegram.org/bots/api#determining-list-of-commands
.. versionadded:: 13.7
Args:
type (:obj:`str`): Scope type.
Attributes:
type (:obj:`str`): Scope type.
"""
__slots__ = ('type', '_id_attrs')
DEFAULT = constants.BOT_COMMAND_SCOPE_DEFAULT
""":const:`telegram.constants.BOT_COMMAND_SCOPE_DEFAULT`"""
ALL_PRIVATE_CHATS = constants.BOT_COMMAND_SCOPE_ALL_PRIVATE_CHATS
""":const:`telegram.constants.BOT_COMMAND_SCOPE_ALL_PRIVATE_CHATS`"""
ALL_GROUP_CHATS = constants.BOT_COMMAND_SCOPE_ALL_GROUP_CHATS
""":const:`telegram.constants.BOT_COMMAND_SCOPE_ALL_GROUP_CHATS`"""
ALL_CHAT_ADMINISTRATORS = constants.BOT_COMMAND_SCOPE_ALL_CHAT_ADMINISTRATORS
""":const:`telegram.constants.BOT_COMMAND_SCOPE_ALL_CHAT_ADMINISTRATORS`"""
CHAT = constants.BOT_COMMAND_SCOPE_CHAT
""":const:`telegram.constants.BOT_COMMAND_SCOPE_CHAT`"""
CHAT_ADMINISTRATORS = constants.BOT_COMMAND_SCOPE_CHAT_ADMINISTRATORS
""":const:`telegram.constants.BOT_COMMAND_SCOPE_CHAT_ADMINISTRATORS`"""
CHAT_MEMBER = constants.BOT_COMMAND_SCOPE_CHAT_MEMBER
""":const:`telegram.constants.BOT_COMMAND_SCOPE_CHAT_MEMBER`"""
def __init__(self, type: str, **_kwargs: Any):
self.type = type
self._id_attrs = (self.type,)
@classmethod
def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['BotCommandScope']:
"""Converts JSON data to the appropriate :class:`BotCommandScope` object, i.e. takes
care of selecting the correct subclass.
Args:
data (Dict[:obj:`str`, ...]): The JSON data.
bot (:class:`telegram.Bot`): The bot associated with this object.
Returns:
The Telegram object.
"""
data = cls._parse_data(data)
if not data:
return None
_class_mapping: Dict[str, Type['BotCommandScope']] = {
cls.DEFAULT: BotCommandScopeDefault,
cls.ALL_PRIVATE_CHATS: BotCommandScopeAllPrivateChats,
cls.ALL_GROUP_CHATS: BotCommandScopeAllGroupChats,
cls.ALL_CHAT_ADMINISTRATORS: BotCommandScopeAllChatAdministrators,
cls.CHAT: BotCommandScopeChat,
cls.CHAT_ADMINISTRATORS: BotCommandScopeChatAdministrators,
cls.CHAT_MEMBER: BotCommandScopeChatMember,
}
if cls is BotCommandScope:
return _class_mapping.get(data['type'], cls)(**data, bot=bot)
return cls(**data)
class BotCommandScopeDefault(BotCommandScope):
"""Represents the default scope of bot commands. Default commands are used if no commands with
a `narrower scope`_ are specified for the user.
.. _`narrower scope`: https://core.telegram.org/bots/api#determining-list-of-commands
.. versionadded:: 13.7
Attributes:
type (:obj:`str`): Scope type :attr:`telegram.BotCommandScope.DEFAULT`.
"""
__slots__ = ()
def __init__(self, **_kwargs: Any):
super().__init__(type=BotCommandScope.DEFAULT)
class BotCommandScopeAllPrivateChats(BotCommandScope):
"""Represents the scope of bot commands, covering all private chats.
.. versionadded:: 13.7
Attributes:
type (:obj:`str`): Scope type :attr:`telegram.BotCommandScope.ALL_PRIVATE_CHATS`.
"""
__slots__ = ()
def __init__(self, **_kwargs: Any):
super().__init__(type=BotCommandScope.ALL_PRIVATE_CHATS)
class BotCommandScopeAllGroupChats(BotCommandScope):
"""Represents the scope of bot commands, covering all group and supergroup chats.
.. versionadded:: 13.7
Attributes:
type (:obj:`str`): Scope type :attr:`telegram.BotCommandScope.ALL_GROUP_CHATS`.
"""
__slots__ = ()
def __init__(self, **_kwargs: Any):
super().__init__(type=BotCommandScope.ALL_GROUP_CHATS)
class BotCommandScopeAllChatAdministrators(BotCommandScope):
"""Represents the scope of bot commands, covering all group and supergroup chat administrators.
.. versionadded:: 13.7
Attributes:
type (:obj:`str`): Scope type :attr:`telegram.BotCommandScope.ALL_CHAT_ADMINISTRATORS`.
"""
__slots__ = ()
def __init__(self, **_kwargs: Any):
super().__init__(type=BotCommandScope.ALL_CHAT_ADMINISTRATORS)
class BotCommandScopeChat(BotCommandScope):
"""Represents the scope of bot commands, covering a specific chat.
Objects of this class are comparable in terms of equality. Two objects of this class are
considered equal, if their :attr:`type` and :attr:`chat_id` are equal.
.. versionadded:: 13.7
Args:
chat_id (:obj:`str` | :obj:`int`): Unique identifier for the target chat or username of the
target supergroup (in the format ``@supergroupusername``)
Attributes:
type (:obj:`str`): Scope type :attr:`telegram.BotCommandScope.CHAT`.
chat_id (:obj:`str` | :obj:`int`): Unique identifier for the target chat or username of the
target supergroup (in the format ``@supergroupusername``)
"""
__slots__ = ('chat_id',)
def __init__(self, chat_id: Union[str, int], **_kwargs: Any):
super().__init__(type=BotCommandScope.CHAT)
self.chat_id = (
chat_id if isinstance(chat_id, str) and chat_id.startswith('@') else int(chat_id)
)
self._id_attrs = (self.type, self.chat_id)
class BotCommandScopeChatAdministrators(BotCommandScope):
"""Represents the scope of bot commands, covering all administrators of a specific group or
supergroup chat.
Objects of this class are comparable in terms of equality. Two objects of this class are
considered equal, if their :attr:`type` and :attr:`chat_id` are equal.
.. versionadded:: 13.7
Args:
chat_id (:obj:`str` | :obj:`int`): Unique identifier for the target chat or username of the
target supergroup (in the format ``@supergroupusername``)
Attributes:
type (:obj:`str`): Scope type :attr:`telegram.BotCommandScope.CHAT_ADMINISTRATORS`.
chat_id (:obj:`str` | :obj:`int`): Unique identifier for the target chat or username of the
target supergroup (in the format ``@supergroupusername``)
"""
__slots__ = ('chat_id',)
def __init__(self, chat_id: Union[str, int], **_kwargs: Any):
super().__init__(type=BotCommandScope.CHAT_ADMINISTRATORS)
self.chat_id = (
chat_id if isinstance(chat_id, str) and chat_id.startswith('@') else int(chat_id)
)
self._id_attrs = (self.type, self.chat_id)
class BotCommandScopeChatMember(BotCommandScope):
"""Represents the scope of bot commands, covering a specific member of a group or supergroup
chat.
Objects of this class are comparable in terms of equality. Two objects of this class are
considered equal, if their :attr:`type`, :attr:`chat_id` and :attr:`user_id` are equal.
.. versionadded:: 13.7
Args:
chat_id (:obj:`str` | :obj:`int`): Unique identifier for the target chat or username of the
target supergroup (in the format ``@supergroupusername``)
user_id (:obj:`int`): Unique identifier of the target user.
Attributes:
type (:obj:`str`): Scope type :attr:`telegram.BotCommandScope.CHAT_MEMBER`.
chat_id (:obj:`str` | :obj:`int`): Unique identifier for the target chat or username of the
target supergroup (in the format ``@supergroupusername``)
user_id (:obj:`int`): Unique identifier of the target user.
"""
__slots__ = ('chat_id', 'user_id')
def __init__(self, chat_id: Union[str, int], user_id: int, **_kwargs: Any):
super().__init__(type=BotCommandScope.CHAT_MEMBER)
self.chat_id = (
chat_id if isinstance(chat_id, str) and chat_id.startswith('@') else int(chat_id)
)
self.user_id = int(user_id)
self._id_attrs = (self.type, self.chat_id, self.user_id)
+123 -21
View File
@@ -18,11 +18,13 @@
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains an object that represents a Telegram Chat."""
import warnings
from datetime import datetime
from typing import TYPE_CHECKING, List, Optional, ClassVar, Union, Tuple, Any
from telegram import ChatPhoto, TelegramObject, constants
from telegram.utils.types import JSONDict, FileInput, ODVInput, DVInput
from telegram.utils.deprecate import TelegramDeprecationWarning
from .chatpermissions import ChatPermissions
from .chatlocation import ChatLocation
@@ -284,7 +286,7 @@ class Chat(TelegramObject):
For the documentation of the arguments, please see :meth:`telegram.Bot.leave_chat`.
Returns:
:obj:`bool` If the action was sent successfully.
:obj:`bool`: On success, :obj:`True` is returned.
"""
return self.bot.leave_chat(
@@ -318,19 +320,37 @@ class Chat(TelegramObject):
def get_members_count(
self, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None
) -> int:
"""
Deprecated, use :func:`~telegram.Chat.get_member_count` instead.
.. deprecated:: 13.7
"""
warnings.warn(
'`Chat.get_members_count` is deprecated. Use `Chat.get_member_count` instead.',
TelegramDeprecationWarning,
stacklevel=2,
)
return self.get_member_count(
timeout=timeout,
api_kwargs=api_kwargs,
)
def get_member_count(
self, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None
) -> int:
"""Shortcut for::
bot.get_chat_members_count(update.effective_chat.id, *args, **kwargs)
bot.get_chat_member_count(update.effective_chat.id, *args, **kwargs)
For the documentation of the arguments, please see
:meth:`telegram.Bot.get_chat_members_count`.
:meth:`telegram.Bot.get_chat_member_count`.
Returns:
:obj:`int`
"""
return self.bot.get_chat_members_count(
return self.bot.get_chat_member_count(
chat_id=self.id,
timeout=timeout,
api_kwargs=api_kwargs,
@@ -366,24 +386,45 @@ class Chat(TelegramObject):
until_date: Union[int, datetime] = None,
api_kwargs: JSONDict = None,
revoke_messages: bool = None,
) -> bool:
"""
Deprecated, use :func:`~telegram.Chat.ban_member` instead.
.. deprecated:: 13.7
"""
warnings.warn(
'`Chat.kick_member` is deprecated. Use `Chat.ban_member` instead.',
TelegramDeprecationWarning,
stacklevel=2,
)
return self.ban_member(
user_id=user_id,
timeout=timeout,
until_date=until_date,
api_kwargs=api_kwargs,
revoke_messages=revoke_messages,
)
def ban_member(
self,
user_id: Union[str, int],
timeout: ODVInput[float] = DEFAULT_NONE,
until_date: Union[int, datetime] = None,
api_kwargs: JSONDict = None,
revoke_messages: bool = None,
) -> bool:
"""Shortcut for::
bot.kick_chat_member(update.effective_chat.id, *args, **kwargs)
bot.ban_chat_member(update.effective_chat.id, *args, **kwargs)
For the documentation of the arguments, please see
:meth:`telegram.Bot.kick_chat_member`.
:meth:`telegram.Bot.ban_chat_member`.
Returns:
:obj:`bool`: If the action was sent successfully.
Note:
This method will only work if the `All Members Are Admins` setting is off in the
target group. Otherwise members may only be removed by the group's creator or by the
member that added them.
:obj:`bool`: On success, :obj:`True` is returned.
"""
return self.bot.kick_chat_member(
return self.bot.ban_chat_member(
chat_id=self.id,
user_id=user_id,
timeout=timeout,
@@ -406,7 +447,7 @@ class Chat(TelegramObject):
For the documentation of the arguments, please see :meth:`telegram.Bot.unban_chat_member`.
Returns:
:obj:`bool`: If the action was sent successfully.
:obj:`bool`: On success, :obj:`True` is returned.
"""
return self.bot.unban_chat_member(
@@ -444,7 +485,7 @@ class Chat(TelegramObject):
.. versionadded:: 13.2
Returns:
:obj:`bool`: If the action was sent successfully.
:obj:`bool`: On success, :obj:`True` is returned.
"""
return self.bot.promote_chat_member(
@@ -483,7 +524,7 @@ class Chat(TelegramObject):
.. versionadded:: 13.2
Returns:
:obj:`bool`: If the action was sent successfully.
:obj:`bool`: On success, :obj:`True` is returned.
"""
return self.bot.restrict_chat_member(
@@ -509,7 +550,7 @@ class Chat(TelegramObject):
:meth:`telegram.Bot.set_chat_permissions`.
Returns:
:obj:`bool`: If the action was sent successfully.
:obj:`bool`: On success, :obj:`True` is returned.
"""
return self.bot.set_chat_permissions(
@@ -534,7 +575,7 @@ class Chat(TelegramObject):
:meth:`telegram.Bot.set_chat_administrator_custom_title`.
Returns:
:obj:`bool`: If the action was sent successfully.
:obj:`bool`: On success, :obj:`True` is returned.
"""
return self.bot.set_chat_administrator_custom_title(
@@ -678,7 +719,7 @@ class Chat(TelegramObject):
For the documentation of the arguments, please see :meth:`telegram.Bot.send_media_group`.
Returns:
List[:class:`telegram.Message`:] On success, instance representing the message posted.
List[:class:`telegram.Message`]: On success, instance representing the message posted.
"""
return self.bot.send_media_group(
@@ -1483,6 +1524,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::
@@ -1493,6 +1536,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`
@@ -1503,6 +1550,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(
@@ -1512,6 +1561,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::
@@ -1522,6 +1573,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`
@@ -1533,6 +1587,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(
@@ -1557,3 +1613,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
+153
View File
@@ -0,0 +1,153 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
# Copyright (C) 2021
# Leandro Toledo de Souza <devs@python-telegram-bot.org>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser Public License for more details.
#
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains an object that represents a Telegram ChatJoinRequest."""
import datetime
from typing import TYPE_CHECKING, Any, Optional
from telegram import TelegramObject, User, Chat, ChatInviteLink
from telegram.utils.helpers import from_timestamp, to_timestamp, DEFAULT_NONE
from telegram.utils.types import JSONDict, ODVInput
if TYPE_CHECKING:
from telegram import Bot
class ChatJoinRequest(TelegramObject):
"""This object represents a join request sent to a chat.
Objects of this class are comparable in terms of equality. Two objects of this class are
considered equal, if their :attr:`chat`, :attr:`from_user` and :attr:`date` are equal.
.. versionadded:: 13.8
Args:
chat (:class:`telegram.Chat`): Chat to which the request was sent.
from_user (:class:`telegram.User`): User that sent the join request.
date (:class:`datetime.datetime`): Date the request was sent.
bio (:obj:`str`, optional): Bio of the user.
invite_link (:class:`telegram.ChatInviteLink`, optional): Chat invite link that was used
by the user to send the join request.
bot (:class:`telegram.Bot`, optional): The Bot to use for instance methods.
Attributes:
chat (:class:`telegram.Chat`): Chat to which the request was sent.
from_user (:class:`telegram.User`): User that sent the join request.
date (:class:`datetime.datetime`): Date the request was sent.
bio (:obj:`str`): Optional. Bio of the user.
invite_link (:class:`telegram.ChatInviteLink`): Optional. Chat invite link that was used
by the user to send the join request.
"""
__slots__ = (
'chat',
'from_user',
'date',
'bio',
'invite_link',
'bot',
'_id_attrs',
)
def __init__(
self,
chat: Chat,
from_user: User,
date: datetime.datetime,
bio: str = None,
invite_link: ChatInviteLink = None,
bot: 'Bot' = None,
**_kwargs: Any,
):
# Required
self.chat = chat
self.from_user = from_user
self.date = date
# Optionals
self.bio = bio
self.invite_link = invite_link
self.bot = bot
self._id_attrs = (self.chat, self.from_user, self.date)
@classmethod
def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['ChatJoinRequest']:
"""See :meth:`telegram.TelegramObject.de_json`."""
data = cls._parse_data(data)
if not data:
return None
data['chat'] = Chat.de_json(data.get('chat'), bot)
data['from_user'] = User.de_json(data.get('from'), bot)
data['date'] = from_timestamp(data.get('date', None))
data['invite_link'] = ChatInviteLink.de_json(data.get('invite_link'), bot)
return cls(bot=bot, **data)
def to_dict(self) -> JSONDict:
"""See :meth:`telegram.TelegramObject.to_dict`."""
data = super().to_dict()
data['date'] = to_timestamp(self.date)
return data
def approve(
self,
timeout: ODVInput[float] = DEFAULT_NONE,
api_kwargs: JSONDict = None,
) -> bool:
"""Shortcut for::
bot.approve_chat_join_request(chat_id=update.effective_chat.id,
user_id=update.effective_user.id, *args, **kwargs)
For the documentation of the arguments, please see
:meth:`telegram.Bot.approve_chat_join_request`.
Returns:
:obj:`bool`: On success, :obj:`True` is returned.
"""
return self.bot.approve_chat_join_request(
chat_id=self.chat.id, user_id=self.from_user.id, timeout=timeout, api_kwargs=api_kwargs
)
def decline(
self,
timeout: ODVInput[float] = DEFAULT_NONE,
api_kwargs: JSONDict = None,
) -> bool:
"""Shortcut for::
bot.decline_chat_join_request(chat_id=update.effective_chat.id,
user_id=update.effective_user.id, *args, **kwargs)
For the documentation of the arguments, please see
:meth:`telegram.Bot.decline_chat_join_request`.
Returns:
:obj:`bool`: On success, :obj:`True` is returned.
"""
return self.bot.decline_chat_join_request(
chat_id=self.chat.id, user_id=self.from_user.id, timeout=timeout, api_kwargs=api_kwargs
)
+466 -4
View File
@@ -18,7 +18,7 @@
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains an object that represents a Telegram ChatMember."""
import datetime
from typing import TYPE_CHECKING, Any, Optional, ClassVar
from typing import TYPE_CHECKING, Any, Optional, ClassVar, Dict, Type
from telegram import TelegramObject, User, constants
from telegram.utils.helpers import from_timestamp, to_timestamp
@@ -29,113 +29,239 @@ if TYPE_CHECKING:
class ChatMember(TelegramObject):
"""This object contains information about one member of a chat.
"""Base class for Telegram ChatMember Objects.
Currently, the following 6 types of chat members are supported:
* :class:`telegram.ChatMemberOwner`
* :class:`telegram.ChatMemberAdministrator`
* :class:`telegram.ChatMemberMember`
* :class:`telegram.ChatMemberRestricted`
* :class:`telegram.ChatMemberLeft`
* :class:`telegram.ChatMemberBanned`
Objects of this class are comparable in terms of equality. Two objects of this class are
considered equal, if their :attr:`user` and :attr:`status` are equal.
Note:
As of Bot API 5.3, :class:`ChatMember` is nothing but the base class for the subclasses
listed above and is no longer returned directly by :meth:`~telegram.Bot.get_chat`.
Therefore, most of the arguments and attributes were deprecated and you should no longer
use :class:`ChatMember` directly.
Args:
user (:class:`telegram.User`): Information about the user.
status (:obj:`str`): The member's status in the chat. Can be 'creator', 'administrator',
'member', 'restricted', 'left' or 'kicked'.
status (:obj:`str`): The member's status in the chat. Can be
:attr:`~telegram.ChatMember.ADMINISTRATOR`, :attr:`~telegram.ChatMember.CREATOR`,
:attr:`~telegram.ChatMember.KICKED`, :attr:`~telegram.ChatMember.LEFT`,
:attr:`~telegram.ChatMember.MEMBER` or :attr:`~telegram.ChatMember.RESTRICTED`.
custom_title (:obj:`str`, optional): Owner and administrators only.
Custom title for this user.
.. deprecated:: 13.7
is_anonymous (:obj:`bool`, optional): Owner and administrators only. :obj:`True`, if the
user's presence in the chat is hidden.
.. deprecated:: 13.7
until_date (:class:`datetime.datetime`, optional): Restricted and kicked only. Date when
restrictions will be lifted for this user.
.. deprecated:: 13.7
can_be_edited (:obj:`bool`, optional): Administrators only. :obj:`True`, if the bot is
allowed to edit administrator privileges of that user.
.. deprecated:: 13.7
can_manage_chat (:obj:`bool`, optional): Administrators only. :obj:`True`, if the
administrator can access the chat event log, chat statistics, message statistics in
channels, see channel members, see anonymous administrators in supergroups and ignore
slow mode. Implied by any other administrator privilege.
.. versionadded:: 13.4
.. deprecated:: 13.7
can_manage_voice_chats (:obj:`bool`, optional): Administrators only. :obj:`True`, if the
administrator can manage voice chats.
.. versionadded:: 13.4
.. deprecated:: 13.7
can_change_info (:obj:`bool`, optional): Administrators and restricted only. :obj:`True`,
if the user can change the chat title, photo and other settings.
.. deprecated:: 13.7
can_post_messages (:obj:`bool`, optional): Administrators only. :obj:`True`, if the
administrator can post in the channel, channels only.
.. deprecated:: 13.7
can_edit_messages (:obj:`bool`, optional): Administrators only. :obj:`True`, if the
administrator can edit messages of other users and can pin messages; channels only.
.. deprecated:: 13.7
can_delete_messages (:obj:`bool`, optional): Administrators only. :obj:`True`, if the
administrator can delete messages of other users.
.. deprecated:: 13.7
can_invite_users (:obj:`bool`, optional): Administrators and restricted only. :obj:`True`,
if the user can invite new users to the chat.
.. deprecated:: 13.7
can_restrict_members (:obj:`bool`, optional): Administrators only. :obj:`True`, if the
administrator can restrict, ban or unban chat members.
.. deprecated:: 13.7
can_pin_messages (:obj:`bool`, optional): Administrators and restricted only. :obj:`True`,
if the user can pin messages, groups and supergroups only.
.. deprecated:: 13.7
can_promote_members (:obj:`bool`, optional): Administrators only. :obj:`True`, if the
administrator can add new administrators with a subset of his own privileges or demote
administrators that he has promoted, directly or indirectly (promoted by administrators
that were appointed by the user).
.. deprecated:: 13.7
is_member (:obj:`bool`, optional): Restricted only. :obj:`True`, if the user is a member of
the chat at the moment of the request.
.. deprecated:: 13.7
can_send_messages (:obj:`bool`, optional): Restricted only. :obj:`True`, if the user can
send text messages, contacts, locations and venues.
.. deprecated:: 13.7
can_send_media_messages (:obj:`bool`, optional): Restricted only. :obj:`True`, if the user
can send audios, documents, photos, videos, video notes and voice notes.
.. deprecated:: 13.7
can_send_polls (:obj:`bool`, optional): Restricted only. :obj:`True`, if the user is
allowed to send polls.
.. deprecated:: 13.7
can_send_other_messages (:obj:`bool`, optional): Restricted only. :obj:`True`, if the user
can send animations, games, stickers and use inline bots.
.. deprecated:: 13.7
can_add_web_page_previews (:obj:`bool`, optional): Restricted only. :obj:`True`, if user
may add web page previews to his messages.
.. deprecated:: 13.7
Attributes:
user (:class:`telegram.User`): Information about the user.
status (:obj:`str`): The member's status in the chat.
custom_title (:obj:`str`): Optional. Custom title for owner and administrators.
.. deprecated:: 13.7
is_anonymous (:obj:`bool`): Optional. :obj:`True`, if the user's presence in the chat is
hidden.
.. deprecated:: 13.7
until_date (:class:`datetime.datetime`): Optional. Date when restrictions will be lifted
for this user.
.. deprecated:: 13.7
can_be_edited (:obj:`bool`): Optional. If the bot is allowed to edit administrator
privileges of that user.
.. deprecated:: 13.7
can_manage_chat (:obj:`bool`): Optional. If the administrator can access the chat event
log, chat statistics, message statistics in channels, see channel members, see
anonymous administrators in supergroups and ignore slow mode.
.. versionadded:: 13.4
.. deprecated:: 13.7
can_manage_voice_chats (:obj:`bool`): Optional. if the administrator can manage
voice chats.
.. versionadded:: 13.4
.. deprecated:: 13.7
can_change_info (:obj:`bool`): Optional. If the user can change the chat title, photo and
other settings.
.. deprecated:: 13.7
can_post_messages (:obj:`bool`): Optional. If the administrator can post in the channel.
.. deprecated:: 13.7
can_edit_messages (:obj:`bool`): Optional. If the administrator can edit messages of other
users.
.. deprecated:: 13.7
can_delete_messages (:obj:`bool`): Optional. If the administrator can delete messages of
other users.
.. deprecated:: 13.7
can_invite_users (:obj:`bool`): Optional. If the user can invite new users to the chat.
.. deprecated:: 13.7
can_restrict_members (:obj:`bool`): Optional. If the administrator can restrict, ban or
unban chat members.
.. deprecated:: 13.7
can_pin_messages (:obj:`bool`): Optional. If the user can pin messages.
.. deprecated:: 13.7
can_promote_members (:obj:`bool`): Optional. If the administrator can add new
administrators.
.. deprecated:: 13.7
is_member (:obj:`bool`): Optional. Restricted only. :obj:`True`, if the user is a member of
the chat at the moment of the request.
.. deprecated:: 13.7
can_send_messages (:obj:`bool`): Optional. If the user can send text messages, contacts,
locations and venues.
.. deprecated:: 13.7
can_send_media_messages (:obj:`bool`): Optional. If the user can send media messages,
implies can_send_messages.
.. deprecated:: 13.7
can_send_polls (:obj:`bool`): Optional. :obj:`True`, if the user is allowed to
send polls.
.. deprecated:: 13.7
can_send_other_messages (:obj:`bool`): Optional. If the user can send animations, games,
stickers and use inline bots, implies can_send_media_messages.
.. deprecated:: 13.7
can_add_web_page_previews (:obj:`bool`): Optional. If user may add web page previews to his
messages, implies can_send_media_messages
.. deprecated:: 13.7
"""
__slots__ = (
@@ -242,6 +368,17 @@ class ChatMember(TelegramObject):
data['user'] = User.de_json(data.get('user'), bot)
data['until_date'] = from_timestamp(data.get('until_date', None))
_class_mapping: Dict[str, Type['ChatMember']] = {
cls.CREATOR: ChatMemberOwner,
cls.ADMINISTRATOR: ChatMemberAdministrator,
cls.MEMBER: ChatMemberMember,
cls.RESTRICTED: ChatMemberRestricted,
cls.LEFT: ChatMemberLeft,
cls.KICKED: ChatMemberBanned,
}
if cls is ChatMember:
return _class_mapping.get(data['status'], cls)(**data, bot=bot)
return cls(**data)
def to_dict(self) -> JSONDict:
@@ -251,3 +388,328 @@ class ChatMember(TelegramObject):
data['until_date'] = to_timestamp(self.until_date)
return data
class ChatMemberOwner(ChatMember):
"""
Represents a chat member that owns the chat
and has all administrator privileges.
.. versionadded:: 13.7
Args:
user (:class:`telegram.User`): Information about the user.
custom_title (:obj:`str`, optional): Custom title for this user.
is_anonymous (:obj:`bool`, optional): :obj:`True`, if the
user's presence in the chat is hidden.
Attributes:
status (:obj:`str`): The member's status in the chat,
always :attr:`telegram.ChatMember.CREATOR`.
user (:class:`telegram.User`): Information about the user.
custom_title (:obj:`str`): Optional. Custom title for
this user.
is_anonymous (:obj:`bool`): Optional. :obj:`True`, if the user's
presence in the chat is hidden.
"""
__slots__ = ()
def __init__(
self,
user: User,
custom_title: str = None,
is_anonymous: bool = None,
**_kwargs: Any,
):
super().__init__(
status=ChatMember.CREATOR,
user=user,
custom_title=custom_title,
is_anonymous=is_anonymous,
)
class ChatMemberAdministrator(ChatMember):
"""
Represents a chat member that has some additional privileges.
.. versionadded:: 13.7
Args:
user (:class:`telegram.User`): Information about the user.
can_be_edited (:obj:`bool`, optional): :obj:`True`, if the bot
is allowed to edit administrator privileges of that user.
custom_title (:obj:`str`, optional): Custom title for this user.
is_anonymous (:obj:`bool`, optional): :obj:`True`, if the user's
presence in the chat is hidden.
can_manage_chat (:obj:`bool`, optional): :obj:`True`, if the administrator
can access the chat event log, chat statistics, message statistics in
channels, see channel members, see anonymous administrators in supergroups
and ignore slow mode. Implied by any other administrator privilege.
can_post_messages (:obj:`bool`, optional): :obj:`True`, if the
administrator can post in the channel, channels only.
can_edit_messages (:obj:`bool`, optional): :obj:`True`, if the
administrator can edit messages of other users and can pin
messages; channels only.
can_delete_messages (:obj:`bool`, optional): :obj:`True`, if the
administrator can delete messages of other users.
can_manage_voice_chats (:obj:`bool`, optional): :obj:`True`, if the
administrator can manage voice chats.
can_restrict_members (:obj:`bool`, optional): :obj:`True`, if the
administrator can restrict, ban or unban chat members.
can_promote_members (:obj:`bool`, optional): :obj:`True`, if the administrator
can add new administrators with a subset of his own privileges or demote
administrators that he has promoted, directly or indirectly (promoted by
administrators that were appointed by the user).
can_change_info (:obj:`bool`, optional): :obj:`True`, if the user can change
the chat title, photo and other settings.
can_invite_users (:obj:`bool`, optional): :obj:`True`, if the user can invite
new users to the chat.
can_pin_messages (:obj:`bool`, optional): :obj:`True`, if the user is allowed
to pin messages; groups and supergroups only.
Attributes:
status (:obj:`str`): The member's status in the chat,
always :attr:`telegram.ChatMember.ADMINISTRATOR`.
user (:class:`telegram.User`): Information about the user.
can_be_edited (:obj:`bool`): Optional. :obj:`True`, if the bot
is allowed to edit administrator privileges of that user.
custom_title (:obj:`str`): Optional. Custom title for this user.
is_anonymous (:obj:`bool`): Optional. :obj:`True`, if the user's
presence in the chat is hidden.
can_manage_chat (:obj:`bool`): Optional. :obj:`True`, if the administrator
can access the chat event log, chat statistics, message statistics in
channels, see channel members, see anonymous administrators in supergroups
and ignore slow mode. Implied by any other administrator privilege.
can_post_messages (:obj:`bool`): Optional. :obj:`True`, if the
administrator can post in the channel, channels only.
can_edit_messages (:obj:`bool`): Optional. :obj:`True`, if the
administrator can edit messages of other users and can pin
messages; channels only.
can_delete_messages (:obj:`bool`): Optional. :obj:`True`, if the
administrator can delete messages of other users.
can_manage_voice_chats (:obj:`bool`): Optional. :obj:`True`, if the
administrator can manage voice chats.
can_restrict_members (:obj:`bool`): Optional. :obj:`True`, if the
administrator can restrict, ban or unban chat members.
can_promote_members (:obj:`bool`): Optional. :obj:`True`, if the administrator
can add new administrators with a subset of his own privileges or demote
administrators that he has promoted, directly or indirectly (promoted by
administrators that were appointed by the user).
can_change_info (:obj:`bool`): Optional. :obj:`True`, if the user can change
the chat title, photo and other settings.
can_invite_users (:obj:`bool`): Optional. :obj:`True`, if the user can invite
new users to the chat.
can_pin_messages (:obj:`bool`): Optional. :obj:`True`, if the user is allowed
to pin messages; groups and supergroups only.
"""
__slots__ = ()
def __init__(
self,
user: User,
can_be_edited: bool = None,
custom_title: str = None,
is_anonymous: bool = None,
can_manage_chat: bool = None,
can_post_messages: bool = None,
can_edit_messages: bool = None,
can_delete_messages: bool = None,
can_manage_voice_chats: bool = None,
can_restrict_members: bool = None,
can_promote_members: bool = None,
can_change_info: bool = None,
can_invite_users: bool = None,
can_pin_messages: bool = None,
**_kwargs: Any,
):
super().__init__(
status=ChatMember.ADMINISTRATOR,
user=user,
can_be_edited=can_be_edited,
custom_title=custom_title,
is_anonymous=is_anonymous,
can_manage_chat=can_manage_chat,
can_post_messages=can_post_messages,
can_edit_messages=can_edit_messages,
can_delete_messages=can_delete_messages,
can_manage_voice_chats=can_manage_voice_chats,
can_restrict_members=can_restrict_members,
can_promote_members=can_promote_members,
can_change_info=can_change_info,
can_invite_users=can_invite_users,
can_pin_messages=can_pin_messages,
)
class ChatMemberMember(ChatMember):
"""
Represents a chat member that has no additional
privileges or restrictions.
.. versionadded:: 13.7
Args:
user (:class:`telegram.User`): Information about the user.
Attributes:
status (:obj:`str`): The member's status in the chat,
always :attr:`telegram.ChatMember.MEMBER`.
user (:class:`telegram.User`): Information about the user.
"""
__slots__ = ()
def __init__(self, user: User, **_kwargs: Any):
super().__init__(status=ChatMember.MEMBER, user=user)
class ChatMemberRestricted(ChatMember):
"""
Represents a chat member that is under certain restrictions
in the chat. Supergroups only.
.. versionadded:: 13.7
Args:
user (:class:`telegram.User`): Information about the user.
is_member (:obj:`bool`, optional): :obj:`True`, if the user is a
member of the chat at the moment of the request.
can_change_info (:obj:`bool`, optional): :obj:`True`, if the user can change
the chat title, photo and other settings.
can_invite_users (:obj:`bool`, optional): :obj:`True`, if the user can invite
new users to the chat.
can_pin_messages (:obj:`bool`, optional): :obj:`True`, if the user is allowed
to pin messages; groups and supergroups only.
can_send_messages (:obj:`bool`, optional): :obj:`True`, if the user is allowed
to send text messages, contacts, locations and venues.
can_send_media_messages (:obj:`bool`, optional): :obj:`True`, if the user is allowed
to send audios, documents, photos, videos, video notes and voice notes.
can_send_polls (:obj:`bool`, optional): :obj:`True`, if the user is allowed
to send polls.
can_send_other_messages (:obj:`bool`, optional): :obj:`True`, if the user is allowed
to send animations, games, stickers and use inline bots.
can_add_web_page_previews (:obj:`bool`, optional): :obj:`True`, if the user is
allowed to add web page previews to their messages.
until_date (:class:`datetime.datetime`, optional): Date when restrictions
will be lifted for this user.
Attributes:
status (:obj:`str`): The member's status in the chat,
always :attr:`telegram.ChatMember.RESTRICTED`.
user (:class:`telegram.User`): Information about the user.
is_member (:obj:`bool`): Optional. :obj:`True`, if the user is a
member of the chat at the moment of the request.
can_change_info (:obj:`bool`): Optional. :obj:`True`, if the user can change
the chat title, photo and other settings.
can_invite_users (:obj:`bool`): Optional. :obj:`True`, if the user can invite
new users to the chat.
can_pin_messages (:obj:`bool`): Optional. :obj:`True`, if the user is allowed
to pin messages; groups and supergroups only.
can_send_messages (:obj:`bool`): Optional. :obj:`True`, if the user is allowed
to send text messages, contacts, locations and venues.
can_send_media_messages (:obj:`bool`): Optional. :obj:`True`, if the user is allowed
to send audios, documents, photos, videos, video notes and voice notes.
can_send_polls (:obj:`bool`): Optional. :obj:`True`, if the user is allowed
to send polls.
can_send_other_messages (:obj:`bool`): Optional. :obj:`True`, if the user is allowed
to send animations, games, stickers and use inline bots.
can_add_web_page_previews (:obj:`bool`): Optional. :obj:`True`, if the user is
allowed to add web page previews to their messages.
until_date (:class:`datetime.datetime`): Optional. Date when restrictions
will be lifted for this user.
"""
__slots__ = ()
def __init__(
self,
user: User,
is_member: bool = None,
can_change_info: bool = None,
can_invite_users: bool = None,
can_pin_messages: bool = None,
can_send_messages: bool = None,
can_send_media_messages: bool = None,
can_send_polls: bool = None,
can_send_other_messages: bool = None,
can_add_web_page_previews: bool = None,
until_date: datetime.datetime = None,
**_kwargs: Any,
):
super().__init__(
status=ChatMember.RESTRICTED,
user=user,
is_member=is_member,
can_change_info=can_change_info,
can_invite_users=can_invite_users,
can_pin_messages=can_pin_messages,
can_send_messages=can_send_messages,
can_send_media_messages=can_send_media_messages,
can_send_polls=can_send_polls,
can_send_other_messages=can_send_other_messages,
can_add_web_page_previews=can_add_web_page_previews,
until_date=until_date,
)
class ChatMemberLeft(ChatMember):
"""
Represents a chat member that isn't currently a member of the chat,
but may join it themselves.
.. versionadded:: 13.7
Args:
user (:class:`telegram.User`): Information about the user.
Attributes:
status (:obj:`str`): The member's status in the chat,
always :attr:`telegram.ChatMember.LEFT`.
user (:class:`telegram.User`): Information about the user.
"""
__slots__ = ()
def __init__(self, user: User, **_kwargs: Any):
super().__init__(status=ChatMember.LEFT, user=user)
class ChatMemberBanned(ChatMember):
"""
Represents a chat member that was banned in the chat and
can't return to the chat or view chat messages.
.. versionadded:: 13.7
Args:
user (:class:`telegram.User`): Information about the user.
until_date (:class:`datetime.datetime`, optional): Date when restrictions
will be lifted for this user.
Attributes:
status (:obj:`str`): The member's status in the chat,
always :attr:`telegram.ChatMember.KICKED`.
user (:class:`telegram.User`): Information about the user.
until_date (:class:`datetime.datetime`): Optional. Date when restrictions
will be lifted for this user.
"""
__slots__ = ()
def __init__(
self,
user: User,
until_date: datetime.datetime = None,
**_kwargs: Any,
):
super().__init__(
status=ChatMember.KICKED,
user=user,
until_date=until_date,
)
+45 -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.2`. Telegram Bot API version supported by this
BOT_API_VERSION (:obj:`str`): `5.3`. Telegram Bot API version supported by this
version of `python-telegram-bot`. Also available as ``telegram.bot_api_version``.
.. versionadded:: 13.4
@@ -86,6 +86,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,14 +204,43 @@ 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`:
Attributes:
BOT_COMMAND_SCOPE_DEFAULT (:obj:`str`): ``'default'``
..versionadded:: 13.7
BOT_COMMAND_SCOPE_ALL_PRIVATE_CHATS (:obj:`str`): ``'all_private_chats'``
..versionadded:: 13.7
BOT_COMMAND_SCOPE_ALL_GROUP_CHATS (:obj:`str`): ``'all_group_chats'``
..versionadded:: 13.7
BOT_COMMAND_SCOPE_ALL_CHAT_ADMINISTRATORS (:obj:`str`): ``'all_chat_administrators'``
..versionadded:: 13.7
BOT_COMMAND_SCOPE_CHAT (:obj:`str`): ``'chat'``
..versionadded:: 13.7
BOT_COMMAND_SCOPE_CHAT_ADMINISTRATORS (:obj:`str`): ``'chat_administrators'``
..versionadded:: 13.7
BOT_COMMAND_SCOPE_CHAT_MEMBER (:obj:`str`): ``'chat_member'``
..versionadded:: 13.7
"""
from typing import List
BOT_API_VERSION: str = '5.2'
BOT_API_VERSION: str = '5.4'
MAX_MESSAGE_LENGTH: int = 4096
MAX_CAPTION_LENGTH: int = 1024
ANONYMOUS_ADMIN_ID: int = 1087968824
@@ -242,6 +274,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'
@@ -328,6 +361,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,
@@ -342,4 +376,13 @@ UPDATE_ALL_TYPES = [
UPDATE_POLL_ANSWER,
UPDATE_MY_CHAT_MEMBER,
UPDATE_CHAT_MEMBER,
UPDATE_CHAT_JOIN_REQUEST,
]
BOT_COMMAND_SCOPE_DEFAULT = 'default'
BOT_COMMAND_SCOPE_ALL_PRIVATE_CHATS = 'all_private_chats'
BOT_COMMAND_SCOPE_ALL_GROUP_CHATS = 'all_group_chats'
BOT_COMMAND_SCOPE_ALL_CHAT_ADMINISTRATORS = 'all_chat_administrators'
BOT_COMMAND_SCOPE_CHAT = 'chat'
BOT_COMMAND_SCOPE_CHAT_ADMINISTRATORS = 'chat_administrators'
BOT_COMMAND_SCOPE_CHAT_MEMBER = 'chat_member'
+3 -1
View File
@@ -32,7 +32,7 @@ from .dispatcher import Dispatcher, DispatcherHandlerStop, run_async
# try-except is just here in case the __init__ is called twice (like in the tests)
# this block is also the reason for the pylint-ignore at the top of the file
try:
del Dispatcher.__slots__ # type: ignore[has-type]
del Dispatcher.__slots__
except AttributeError as exc:
if str(exc) == '__slots__':
pass
@@ -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',
+67 -39
View File
@@ -212,7 +212,8 @@ class BasePersistence(Generic[UD, CD, BD], ABC):
:attr:`REPLACED_BOT`. Currently, this handles objects of type ``list``, ``tuple``, ``set``,
``frozenset``, ``dict``, ``defaultdict`` and objects that have a ``__dict__`` or
``__slots__`` attribute, excluding classes and objects that can't be copied with
``copy.copy``.
``copy.copy``. If the parsing of an object fails, the object will be returned unchanged and
the error will be logged.
Args:
obj (:obj:`object`): The object
@@ -276,21 +277,34 @@ 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 hasattr(obj, '__dict__'):
for attr_name, attr in new_obj.__dict__.items():
setattr(new_obj, attr_name, cls._replace_bot(attr, memo))
memo[obj_id] = new_obj
return new_obj
if hasattr(obj, '__slots__'):
for attr_name in new_obj.__slots__:
setattr(
new_obj,
attr_name,
cls._replace_bot(cls._replace_bot(getattr(new_obj, attr_name), memo), memo),
)
memo[obj_id] = new_obj
return new_obj
try:
if hasattr(obj, '__slots__'):
for attr_name in new_obj.__slots__:
setattr(
new_obj,
attr_name,
cls._replace_bot(
cls._replace_bot(getattr(new_obj, attr_name), memo), memo
),
)
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))
memo[obj_id] = new_obj
return new_obj
except Exception as exception:
warnings.warn(
f'Parsing of an object failed with the following exception: {exception}. '
f'See the docs of BasePersistence.replace_bot for more information.',
RuntimeWarning,
)
memo[obj_id] = obj
return obj
def insert_bot(self, obj: object) -> object:
@@ -299,7 +313,8 @@ class BasePersistence(Generic[UD, CD, BD], ABC):
:attr:`bot`. Currently, this handles objects of type ``list``, ``tuple``, ``set``,
``frozenset``, ``dict``, ``defaultdict`` and objects that have a ``__dict__`` or
``__slots__`` attribute, excluding classes and objects that can't be copied with
``copy.copy``.
``copy.copy``. If the parsing of an object fails, the object will be returned unchanged and
the error will be logged.
Args:
obj (:obj:`object`): The object
@@ -364,21 +379,34 @@ 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 hasattr(obj, '__dict__'):
for attr_name, attr in new_obj.__dict__.items():
setattr(new_obj, attr_name, self._insert_bot(attr, memo))
memo[obj_id] = new_obj
return new_obj
if hasattr(obj, '__slots__'):
for attr_name in obj.__slots__:
setattr(
new_obj,
attr_name,
self._insert_bot(self._insert_bot(getattr(new_obj, attr_name), memo), memo),
)
memo[obj_id] = new_obj
return new_obj
try:
if hasattr(obj, '__slots__'):
for attr_name in obj.__slots__:
setattr(
new_obj,
attr_name,
self._insert_bot(
self._insert_bot(getattr(new_obj, attr_name), memo), memo
),
)
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))
memo[obj_id] = new_obj
return new_obj
except Exception as exception:
warnings.warn(
f'Parsing of an object failed with the following exception: {exception}. '
f'See the docs of BasePersistence.insert_bot for more information.',
RuntimeWarning,
)
memo[obj_id] = obj
return obj
@abstractmethod
@@ -418,8 +446,8 @@ class BasePersistence(Generic[UD, CD, BD], ABC):
.. versionadded:: 13.6
Returns:
Optional[:class:`telegram.ext.utils.types.CDCData`]: The restored meta data or
:obj:`None`, if no data was stored.
Optional[:class:`telegram.ext.utils.types.CDCData`]: The restored meta data or
:obj:`None`, if no data was stored.
"""
raise NotImplementedError
@@ -441,8 +469,8 @@ class BasePersistence(Generic[UD, CD, BD], ABC):
def update_conversation(
self, name: str, key: Tuple[int, ...], new_state: Optional[object]
) -> None:
"""Will be called when a :attr:`telegram.ext.ConversationHandler.update_state`
is called. This allows the storage of the new state in the persistence.
"""Will be called when a :class:`telegram.ext.ConversationHandler` changes states.
This allows the storage of the new state in the persistence.
Args:
name (:obj:`str`): The handler's name.
@@ -458,7 +486,7 @@ class BasePersistence(Generic[UD, CD, BD], ABC):
Args:
user_id (:obj:`int`): The user the data might have been changed for.
data (:class:`telegram.ext.utils.types.UD`): The
:attr:`telegram.ext.dispatcher.user_data` ``[user_id]``.
:attr:`telegram.ext.Dispatcher.user_data` ``[user_id]``.
"""
@abstractmethod
@@ -469,7 +497,7 @@ class BasePersistence(Generic[UD, CD, BD], ABC):
Args:
chat_id (:obj:`int`): The chat the data might have been changed for.
data (:class:`telegram.ext.utils.types.CD`): The
:attr:`telegram.ext.dispatcher.chat_data` ``[chat_id]``.
:attr:`telegram.ext.Dispatcher.chat_data` ``[chat_id]``.
"""
@abstractmethod
@@ -479,7 +507,7 @@ class BasePersistence(Generic[UD, CD, BD], ABC):
Args:
data (:class:`telegram.ext.utils.types.BD`): The
:attr:`telegram.ext.dispatcher.bot_data`.
:attr:`telegram.ext.Dispatcher.bot_data`.
"""
def refresh_user_data(self, user_id: int, user_data: UD) -> None:
@@ -524,8 +552,8 @@ class BasePersistence(Generic[UD, CD, BD], ABC):
.. versionadded:: 13.6
Args:
data (:class:`telegram.ext.utils.types.CDCData`:): The relevant data to restore
:attr:`telegram.ext.dispatcher.bot.callback_data_cache`.
data (:class:`telegram.ext.utils.types.CDCData`): The relevant data to restore
:class:`telegram.ext.CallbackDataCache`.
"""
raise NotImplementedError
+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)
+36 -28
View File
@@ -50,9 +50,11 @@ class ContextTypes(Generic[CCT, UD, CD, BD]):
__slots__ = ('_context', '_bot_data', '_chat_data', '_user_data')
# overload signatures generated with https://git.io/JtJPj
@overload
def __init__(
self: 'ContextTypes[CallbackContext, Dict, Dict, Dict]',
self: 'ContextTypes[CallbackContext[Dict, Dict, Dict], Dict, Dict, Dict]',
):
...
@@ -61,20 +63,26 @@ class ContextTypes(Generic[CCT, UD, CD, BD]):
...
@overload
def __init__(self: 'ContextTypes[CallbackContext, UD, Dict, Dict]', bot_data: Type[UD]):
...
@overload
def __init__(self: 'ContextTypes[CallbackContext, Dict, CD, Dict]', chat_data: Type[CD]):
...
@overload
def __init__(self: 'ContextTypes[CallbackContext, Dict, Dict, BD]', user_data: Type[BD]):
def __init__(
self: 'ContextTypes[CallbackContext[UD, Dict, Dict], UD, Dict, Dict]', user_data: Type[UD]
):
...
@overload
def __init__(
self: 'ContextTypes[CCT, UD, Dict, Dict]', context: Type[CCT], bot_data: Type[UD]
self: 'ContextTypes[CallbackContext[Dict, CD, Dict], Dict, CD, Dict]', chat_data: Type[CD]
):
...
@overload
def __init__(
self: 'ContextTypes[CallbackContext[Dict, Dict, BD], Dict, Dict, BD]', bot_data: Type[BD]
):
...
@overload
def __init__(
self: 'ContextTypes[CCT, UD, Dict, Dict]', context: Type[CCT], user_data: Type[UD]
):
...
@@ -86,31 +94,31 @@ class ContextTypes(Generic[CCT, UD, CD, BD]):
@overload
def __init__(
self: 'ContextTypes[CCT, Dict, Dict, BD]', context: Type[CCT], user_data: Type[BD]
self: 'ContextTypes[CCT, Dict, Dict, BD]', context: Type[CCT], bot_data: Type[BD]
):
...
@overload
def __init__(
self: 'ContextTypes[CallbackContext, UD, CD, Dict]',
bot_data: Type[UD],
self: 'ContextTypes[CallbackContext[UD, CD, Dict], UD, CD, Dict]',
user_data: Type[UD],
chat_data: Type[CD],
):
...
@overload
def __init__(
self: 'ContextTypes[CallbackContext, UD, Dict, BD]',
bot_data: Type[UD],
user_data: Type[BD],
self: 'ContextTypes[CallbackContext[UD, Dict, BD], UD, Dict, BD]',
user_data: Type[UD],
bot_data: Type[BD],
):
...
@overload
def __init__(
self: 'ContextTypes[CallbackContext, Dict, CD, BD]',
self: 'ContextTypes[CallbackContext[Dict, CD, BD], Dict, CD, BD]',
chat_data: Type[CD],
user_data: Type[BD],
bot_data: Type[BD],
):
...
@@ -118,7 +126,7 @@ class ContextTypes(Generic[CCT, UD, CD, BD]):
def __init__(
self: 'ContextTypes[CCT, UD, CD, Dict]',
context: Type[CCT],
bot_data: Type[UD],
user_data: Type[UD],
chat_data: Type[CD],
):
...
@@ -127,8 +135,8 @@ class ContextTypes(Generic[CCT, UD, CD, BD]):
def __init__(
self: 'ContextTypes[CCT, UD, Dict, BD]',
context: Type[CCT],
bot_data: Type[UD],
user_data: Type[BD],
user_data: Type[UD],
bot_data: Type[BD],
):
...
@@ -137,16 +145,16 @@ class ContextTypes(Generic[CCT, UD, CD, BD]):
self: 'ContextTypes[CCT, Dict, CD, BD]',
context: Type[CCT],
chat_data: Type[CD],
user_data: Type[BD],
bot_data: Type[BD],
):
...
@overload
def __init__(
self: 'ContextTypes[CallbackContext, UD, CD, BD]',
bot_data: Type[UD],
self: 'ContextTypes[CallbackContext[UD, CD, BD], UD, CD, BD]',
user_data: Type[UD],
chat_data: Type[CD],
user_data: Type[BD],
bot_data: Type[BD],
):
...
@@ -154,9 +162,9 @@ class ContextTypes(Generic[CCT, UD, CD, BD]):
def __init__(
self: 'ContextTypes[CCT, UD, CD, BD]',
context: Type[CCT],
bot_data: Type[UD],
user_data: Type[UD],
chat_data: Type[CD],
user_data: Type[BD],
bot_data: Type[BD],
):
...
+1 -1
View File
@@ -350,11 +350,11 @@ class ConversationHandler(Handler[Update, CCT]):
@property
def allow_reentry(self) -> bool:
""":obj:`bool`: Determines if a user can restart a conversation with an entry point."""
return self._allow_reentry
@allow_reentry.setter
def allow_reentry(self, value: object) -> NoReturn:
""":obj:`bool`: Determines if a user can restart a conversation with an entry point."""
raise ValueError('You can not assign a new value to allow_reentry after initialization.')
@property
+6 -6
View File
@@ -294,7 +294,7 @@ class DictPersistence(BasePersistence):
Returns:
Optional[:class:`telegram.ext.utils.types.CDCData`]: The restored meta data or
:obj:`None`, if no data was stored.
:obj:`None`, if no data was stored.
"""
if self.callback_data is None:
self._callback_data = None
@@ -334,7 +334,7 @@ class DictPersistence(BasePersistence):
Args:
user_id (:obj:`int`): The user the data might have been changed for.
data (:obj:`dict`): The :attr:`telegram.ext.dispatcher.user_data` ``[user_id]``.
data (:obj:`dict`): The :attr:`telegram.ext.Dispatcher.user_data` ``[user_id]``.
"""
if self._user_data is None:
self._user_data = defaultdict(dict)
@@ -348,7 +348,7 @@ class DictPersistence(BasePersistence):
Args:
chat_id (:obj:`int`): The chat the data might have been changed for.
data (:obj:`dict`): The :attr:`telegram.ext.dispatcher.chat_data` ``[chat_id]``.
data (:obj:`dict`): The :attr:`telegram.ext.Dispatcher.chat_data` ``[chat_id]``.
"""
if self._chat_data is None:
self._chat_data = defaultdict(dict)
@@ -361,7 +361,7 @@ class DictPersistence(BasePersistence):
"""Will update the bot_data (if changed).
Args:
data (:obj:`dict`): The :attr:`telegram.ext.dispatcher.bot_data`.
data (:obj:`dict`): The :attr:`telegram.ext.Dispatcher.bot_data`.
"""
if self._bot_data == data:
return
@@ -374,8 +374,8 @@ class DictPersistence(BasePersistence):
.. versionadded:: 13.6
Args:
data (:class:`telegram.ext.utils.types.CDCData`:): The relevant data to restore
:attr:`telegram.ext.dispatcher.bot.callback_data_cache`.
data (:class:`telegram.ext.utils.types.CDCData`): The relevant data to restore
:class:`telegram.ext.CallbackDataCache`.
"""
if self._callback_data == data:
return
+12 -1
View File
@@ -101,8 +101,9 @@ class ExtBot(telegram.bot.Bot):
request=request,
private_key=private_key,
private_key_password=private_key_password,
defaults=defaults,
)
# We don't pass this to super().__init__ to avoid the deprecation warning
self.defaults = defaults
# set up callback_data
if not isinstance(arbitrary_callback_data, bool):
@@ -324,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`"""
+3
View File
@@ -518,6 +518,9 @@ class Job:
With the current backend APScheduler, :attr:`job` holds a :class:`apscheduler.job.Job`
instance.
Objects of this class are comparable in terms of equality. Two objects of this class are
considered equal, if their :attr:`id` is equal.
Note:
* All attributes and instance methods of :attr:`job` are also directly available as
attributes/methods of the corresponding :class:`telegram.ext.Job` object.
+8 -8
View File
@@ -60,7 +60,7 @@ class PicklePersistence(BasePersistence[UD, CD, BD]):
.. versionadded:: 13.6
single_file (:obj:`bool`, optional): When :obj:`False` will store 5 separate files of
`filename_user_data`, `filename_chat_data`, `filename_bot_data`, `filename_chat_data`,
`filename_user_data`, `filename_bot_data`, `filename_chat_data`,
`filename_callback_data` and `filename_conversations`. Default is :obj:`True`.
on_flush (:obj:`bool`, optional): When :obj:`True` will only save to file when
:meth:`flush` is called and keep data in memory until that happens. When
@@ -87,7 +87,7 @@ class PicklePersistence(BasePersistence[UD, CD, BD]):
.. versionadded:: 13.6
single_file (:obj:`bool`): Optional. When :obj:`False` will store 5 separate files of
`filename_user_data`, `filename_chat_data`, `filename_bot_data`, `filename_chat_data`,
`filename_user_data`, `filename_bot_data`, `filename_chat_data`,
`filename_callback_data` and `filename_conversations`. Default is :obj:`True`.
on_flush (:obj:`bool`, optional): When :obj:`True` will only save to file when
:meth:`flush` is called and keep data in memory until that happens. When
@@ -281,7 +281,7 @@ class PicklePersistence(BasePersistence[UD, CD, BD]):
Returns:
Optional[:class:`telegram.ext.utils.types.CDCData`]: The restored meta data or
:obj:`None`, if no data was stored.
:obj:`None`, if no data was stored.
"""
if self.callback_data:
pass
@@ -347,7 +347,7 @@ class PicklePersistence(BasePersistence[UD, CD, BD]):
Args:
user_id (:obj:`int`): The user the data might have been changed for.
data (:class:`telegram.ext.utils.types.UD`): The
:attr:`telegram.ext.dispatcher.user_data` ``[user_id]``.
:attr:`telegram.ext.Dispatcher.user_data` ``[user_id]``.
"""
if self.user_data is None:
self.user_data = defaultdict(self.context_types.user_data)
@@ -367,7 +367,7 @@ class PicklePersistence(BasePersistence[UD, CD, BD]):
Args:
chat_id (:obj:`int`): The chat the data might have been changed for.
data (:class:`telegram.ext.utils.types.CD`): The
:attr:`telegram.ext.dispatcher.chat_data` ``[chat_id]``.
:attr:`telegram.ext.Dispatcher.chat_data` ``[chat_id]``.
"""
if self.chat_data is None:
self.chat_data = defaultdict(self.context_types.chat_data)
@@ -386,7 +386,7 @@ class PicklePersistence(BasePersistence[UD, CD, BD]):
Args:
data (:class:`telegram.ext.utils.types.BD`): The
:attr:`telegram.ext.dispatcher.bot_data`.
:attr:`telegram.ext.Dispatcher.bot_data`.
"""
if self.bot_data == data:
return
@@ -405,8 +405,8 @@ class PicklePersistence(BasePersistence[UD, CD, BD]):
.. versionadded:: 13.6
Args:
data (:class:`telegram.ext.utils.types.CDCData`:): The relevant data to restore
:attr:`telegram.ext.dispatcher.bot.callback_data`.
data (:class:`telegram.ext.utils.types.CDCData`): The relevant data to restore
:class:`telegram.ext.CallbackDataCache`.
"""
if self.callback_data == data:
return
+21 -4
View File
@@ -37,25 +37,42 @@ class ForceReply(ReplyMarkup):
selective (:obj:`bool`, optional): Use this parameter if you want to force reply from
specific users only. Targets:
1) Users that are @mentioned in the text of the Message object.
2) If the bot's message is a reply (has reply_to_message_id), sender of the
1) Users that are @mentioned in the :attr:`~telegram.Message.text` of the
:class:`telegram.Message` object.
2) If the bot's message is a reply (has ``reply_to_message_id``), sender of the
original message.
input_field_placeholder (:obj:`str`, optional): The placeholder to be shown in the input
field when the reply is active; 1-64 characters.
.. versionadded:: 13.7
**kwargs (:obj:`dict`): Arbitrary keyword arguments.
Attributes:
force_reply (:obj:`True`): Shows reply interface to the user, as if they manually selected
the bots message and tapped 'Reply'.
selective (:obj:`bool`): Optional. Force reply from specific users only.
input_field_placeholder (:obj:`str`): Optional. The placeholder shown in the input
field when the reply is active.
.. versionadded:: 13.7
"""
__slots__ = ('selective', 'force_reply', '_id_attrs')
__slots__ = ('selective', 'force_reply', 'input_field_placeholder', '_id_attrs')
def __init__(self, force_reply: bool = True, selective: bool = False, **_kwargs: Any):
def __init__(
self,
force_reply: bool = True,
selective: bool = False,
input_field_placeholder: str = None,
**_kwargs: Any,
):
# Required
self.force_reply = bool(force_reply)
# Optionals
self.selective = bool(selective)
self.input_field_placeholder = input_field_placeholder
self._id_attrs = (self.selective,)
+1 -1
View File
@@ -29,7 +29,7 @@ class LoginUrl(TelegramObject):
coming from Telegram. All the user needs to do is tap/click a button and confirm that they want
to log in. Telegram apps support these buttons as of version 5.7.
Sample bot: `@discussbot <https://t.me/dicussbot>`_
Sample bot: `@discussbot <https://t.me/discussbot>`_
Objects of this class are comparable in terms of equality. Two objects of this class are
considered equal, if their :attr:`url` is equal.
+48 -8
View File
@@ -48,12 +48,18 @@ class ReplyKeyboardMarkup(ReplyMarkup):
selective (:obj:`bool`, optional): Use this parameter if you want to show the keyboard to
specific users only. Targets:
1) Users that are @mentioned in the text of the Message object.
1) Users that are @mentioned in the :attr:`~telegram.Message.text` of the
:class:`telegram.Message` object.
2) If the bot's message is a reply (has ``reply_to_message_id``), sender of the
original message.
Defaults to :obj:`False`.
input_field_placeholder (:obj:`str`, optional): The placeholder to be shown in the input
field when the keyboard is active; 1-64 characters.
.. versionadded:: 13.7
**kwargs (:obj:`dict`): Arbitrary keyword arguments.
Attributes:
@@ -62,10 +68,21 @@ class ReplyKeyboardMarkup(ReplyMarkup):
one_time_keyboard (:obj:`bool`): Optional. Requests clients to hide the keyboard as soon as
it's been used.
selective (:obj:`bool`): Optional. Show the keyboard to specific users only.
input_field_placeholder (:obj:`str`): Optional. The placeholder shown in the input
field when the reply is active.
.. versionadded:: 13.7
"""
__slots__ = ('selective', 'keyboard', 'resize_keyboard', 'one_time_keyboard', '_id_attrs')
__slots__ = (
'selective',
'keyboard',
'resize_keyboard',
'one_time_keyboard',
'input_field_placeholder',
'_id_attrs',
)
def __init__(
self,
@@ -73,6 +90,7 @@ class ReplyKeyboardMarkup(ReplyMarkup):
resize_keyboard: bool = False,
one_time_keyboard: bool = False,
selective: bool = False,
input_field_placeholder: str = None,
**_kwargs: Any,
):
# Required
@@ -90,6 +108,7 @@ class ReplyKeyboardMarkup(ReplyMarkup):
self.resize_keyboard = bool(resize_keyboard)
self.one_time_keyboard = bool(one_time_keyboard)
self.selective = bool(selective)
self.input_field_placeholder = input_field_placeholder
self._id_attrs = (self.keyboard,)
@@ -109,6 +128,7 @@ class ReplyKeyboardMarkup(ReplyMarkup):
resize_keyboard: bool = False,
one_time_keyboard: bool = False,
selective: bool = False,
input_field_placeholder: str = None,
**kwargs: object,
) -> 'ReplyKeyboardMarkup':
"""Shortcut for::
@@ -133,10 +153,15 @@ class ReplyKeyboardMarkup(ReplyMarkup):
to specific users only. Targets:
1) Users that are @mentioned in the text of the Message object.
2) If the bot's message is a reply (has reply_to_message_id), sender of the
original message.
2) If the bot's message is a reply (has ``reply_to_message_id``), sender of the
original message.
Defaults to :obj:`False`.
input_field_placeholder (:obj:`str`): Optional. The placeholder shown in the input
field when the reply is active.
.. versionadded:: 13.7
**kwargs (:obj:`dict`): Arbitrary keyword arguments.
"""
return cls(
@@ -144,6 +169,7 @@ class ReplyKeyboardMarkup(ReplyMarkup):
resize_keyboard=resize_keyboard,
one_time_keyboard=one_time_keyboard,
selective=selective,
input_field_placeholder=input_field_placeholder,
**kwargs,
)
@@ -154,6 +180,7 @@ class ReplyKeyboardMarkup(ReplyMarkup):
resize_keyboard: bool = False,
one_time_keyboard: bool = False,
selective: bool = False,
input_field_placeholder: str = None,
**kwargs: object,
) -> 'ReplyKeyboardMarkup':
"""Shortcut for::
@@ -178,10 +205,15 @@ class ReplyKeyboardMarkup(ReplyMarkup):
to specific users only. Targets:
1) Users that are @mentioned in the text of the Message object.
2) If the bot's message is a reply (has reply_to_message_id), sender of the
original message.
2) If the bot's message is a reply (has ``reply_to_message_id``), sender of the
original message.
Defaults to :obj:`False`.
input_field_placeholder (:obj:`str`): Optional. The placeholder shown in the input
field when the reply is active.
.. versionadded:: 13.7
**kwargs (:obj:`dict`): Arbitrary keyword arguments.
"""
@@ -190,6 +222,7 @@ class ReplyKeyboardMarkup(ReplyMarkup):
resize_keyboard=resize_keyboard,
one_time_keyboard=one_time_keyboard,
selective=selective,
input_field_placeholder=input_field_placeholder,
**kwargs,
)
@@ -200,6 +233,7 @@ class ReplyKeyboardMarkup(ReplyMarkup):
resize_keyboard: bool = False,
one_time_keyboard: bool = False,
selective: bool = False,
input_field_placeholder: str = None,
**kwargs: object,
) -> 'ReplyKeyboardMarkup':
"""Shortcut for::
@@ -224,10 +258,15 @@ class ReplyKeyboardMarkup(ReplyMarkup):
to specific users only. Targets:
1) Users that are @mentioned in the text of the Message object.
2) If the bot's message is a reply (has reply_to_message_id), sender of the
original message.
2) If the bot's message is a reply (has ``reply_to_message_id``), sender of the
original message.
Defaults to :obj:`False`.
input_field_placeholder (:obj:`str`): Optional. The placeholder shown in the input
field when the reply is active.
.. versionadded:: 13.7
**kwargs (:obj:`dict`): Arbitrary keyword arguments.
"""
@@ -237,6 +276,7 @@ class ReplyKeyboardMarkup(ReplyMarkup):
resize_keyboard=resize_keyboard,
one_time_keyboard=one_time_keyboard,
selective=selective,
input_field_placeholder=input_field_placeholder,
**kwargs,
)
+30 -2
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
@@ -334,8 +359,10 @@ class Update(TelegramObject):
:class:`telegram.Message`: The message included in this update, no matter what kind of
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` and
:attr:`poll_answer`.
:attr:`shipping_query`, :attr:`pre_checkout_query`, :attr:`poll`,
: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)
+64 -18
View File
@@ -193,7 +193,7 @@ class User(TelegramObject):
def mention_markdown(self, name: str = None) -> str:
"""
Note:
:attr:`telegram.ParseMode.MARKDOWN` is is a legacy mode, retained by Telegram for
:attr:`telegram.ParseMode.MARKDOWN` is a legacy mode, retained by Telegram for
backward compatibility. You should use :meth:`mention_markdown_v2` instead.
Args:
@@ -362,7 +362,7 @@ class User(TelegramObject):
) -> 'Message':
"""Shortcut for::
bot.send_message(update.effective_user.id, *args, **kwargs)
bot.send_photo(update.effective_user.id, *args, **kwargs)
For the documentation of the arguments, please see :meth:`telegram.Bot.send_photo`.
@@ -398,7 +398,7 @@ class User(TelegramObject):
) -> List['Message']:
"""Shortcut for::
bot.send_message(update.effective_user.id, *args, **kwargs)
bot.send_media_group(update.effective_user.id, *args, **kwargs)
For the documentation of the arguments, please see :meth:`telegram.Bot.send_media_group`.
@@ -436,7 +436,7 @@ class User(TelegramObject):
) -> 'Message':
"""Shortcut for::
bot.send_message(update.effective_user.id, *args, **kwargs)
bot.send_audio(update.effective_user.id, *args, **kwargs)
For the documentation of the arguments, please see :meth:`telegram.Bot.send_audio`.
@@ -471,7 +471,7 @@ class User(TelegramObject):
) -> bool:
"""Shortcut for::
bot.send_message(update.effective_user.id, *args, **kwargs)
bot.send_chat_action(update.effective_user.id, *args, **kwargs)
For the documentation of the arguments, please see :meth:`telegram.Bot.send_chat_action`.
@@ -505,7 +505,7 @@ class User(TelegramObject):
) -> 'Message':
"""Shortcut for::
bot.send_message(update.effective_user.id, *args, **kwargs)
bot.send_contact(update.effective_user.id, *args, **kwargs)
For the documentation of the arguments, please see :meth:`telegram.Bot.send_contact`.
@@ -540,7 +540,7 @@ class User(TelegramObject):
) -> 'Message':
"""Shortcut for::
bot.send_message(update.effective_user.id, *args, **kwargs)
bot.send_dice(update.effective_user.id, *args, **kwargs)
For the documentation of the arguments, please see :meth:`telegram.Bot.send_dice`.
@@ -577,7 +577,7 @@ class User(TelegramObject):
) -> 'Message':
"""Shortcut for::
bot.send_message(update.effective_user.id, *args, **kwargs)
bot.send_document(update.effective_user.id, *args, **kwargs)
For the documentation of the arguments, please see :meth:`telegram.Bot.send_document`.
@@ -614,7 +614,7 @@ class User(TelegramObject):
) -> 'Message':
"""Shortcut for::
bot.send_message(update.effective_user.id, *args, **kwargs)
bot.send_game(update.effective_user.id, *args, **kwargs)
For the documentation of the arguments, please see :meth:`telegram.Bot.send_game`.
@@ -665,7 +665,7 @@ class User(TelegramObject):
) -> 'Message':
"""Shortcut for::
bot.send_message(update.effective_user.id, *args, **kwargs)
bot.send_invoice(update.effective_user.id, *args, **kwargs)
For the documentation of the arguments, please see :meth:`telegram.Bot.send_invoice`.
@@ -730,7 +730,7 @@ class User(TelegramObject):
) -> 'Message':
"""Shortcut for::
bot.send_message(update.effective_user.id, *args, **kwargs)
bot.send_location(update.effective_user.id, *args, **kwargs)
For the documentation of the arguments, please see :meth:`telegram.Bot.send_location`.
@@ -775,7 +775,7 @@ class User(TelegramObject):
) -> 'Message':
"""Shortcut for::
bot.send_message(update.effective_user.id, *args, **kwargs)
bot.send_animation(update.effective_user.id, *args, **kwargs)
For the documentation of the arguments, please see :meth:`telegram.Bot.send_animation`.
@@ -814,7 +814,7 @@ class User(TelegramObject):
) -> 'Message':
"""Shortcut for::
bot.send_message(update.effective_user.id, *args, **kwargs)
bot.send_sticker(update.effective_user.id, *args, **kwargs)
For the documentation of the arguments, please see :meth:`telegram.Bot.send_sticker`.
@@ -854,7 +854,7 @@ class User(TelegramObject):
) -> 'Message':
"""Shortcut for::
bot.send_message(update.effective_user.id, *args, **kwargs)
bot.send_video(update.effective_user.id, *args, **kwargs)
For the documentation of the arguments, please see :meth:`telegram.Bot.send_video`.
@@ -902,7 +902,7 @@ class User(TelegramObject):
) -> 'Message':
"""Shortcut for::
bot.send_message(update.effective_user.id, *args, **kwargs)
bot.send_venue(update.effective_user.id, *args, **kwargs)
For the documentation of the arguments, please see :meth:`telegram.Bot.send_venue`.
@@ -945,7 +945,7 @@ class User(TelegramObject):
) -> 'Message':
"""Shortcut for::
bot.send_message(update.effective_user.id, *args, **kwargs)
bot.send_video_note(update.effective_user.id, *args, **kwargs)
For the documentation of the arguments, please see :meth:`telegram.Bot.send_video_note`.
@@ -985,7 +985,7 @@ class User(TelegramObject):
) -> 'Message':
"""Shortcut for::
bot.send_message(update.effective_user.id, *args, **kwargs)
bot.send_voice(update.effective_user.id, *args, **kwargs)
For the documentation of the arguments, please see :meth:`telegram.Bot.send_voice`.
@@ -1033,7 +1033,7 @@ class User(TelegramObject):
) -> 'Message':
"""Shortcut for::
bot.send_message(update.effective_user.id, *args, **kwargs)
bot.send_poll(update.effective_user.id, *args, **kwargs)
For the documentation of the arguments, please see :meth:`telegram.Bot.send_poll`.
@@ -1140,3 +1140,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
)
+3 -1
View File
@@ -40,6 +40,8 @@ def set_new_attribute_deprecated(self: object, key: str, value: object) -> None:
new = len(self.__dict__)
if new > org:
warnings.warn(
"Setting custom attributes on objects of the PTB library is deprecated.",
f"Setting custom attributes such as {key!r} on objects such as "
f"{self.__class__.__name__!r} of the PTB library is deprecated.",
TelegramDeprecationWarning,
stacklevel=3,
)
+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.6'
__version__ = '13.8'
bot_api_version = constants.BOT_API_VERSION # pylint: disable=C0103
-1
View File
@@ -104,7 +104,6 @@ class TestAnimation:
assert message.animation.thumb.height == self.height
@flaky(3, 1)
@pytest.mark.filterwarnings("ignore:.*custom attributes")
def test_send_animation_custom_filename(self, bot, chat_id, animation_file, monkeypatch):
def make_assertion(url, data, **kwargs):
return data['animation'].filename == 'custom_filename'
+213 -19
View File
@@ -51,9 +51,10 @@ from telegram import (
Chat,
InlineQueryResultVoice,
PollOption,
BotCommandScopeChat,
)
from telegram.constants import MAX_INLINE_QUERY_RESULTS
from telegram.ext import ExtBot
from telegram.ext import ExtBot, Defaults
from telegram.error import BadRequest, InvalidToken, NetworkError, RetryAfter
from telegram.ext.callbackdatacache import InvalidCallbackData
from telegram.utils.helpers import (
@@ -65,6 +66,24 @@ 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
class BotSubClass(Bot):
# used for test_defaults_warning below
pass
@pytest.fixture(scope='class')
def message(bot, chat_id):
to_reply_to = bot.send_message(
@@ -699,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):
@@ -707,7 +727,6 @@ class TestBot:
bot.send_chat_action(chat_id, 'unknown action')
# TODO: Needs improvement. We need incoming inline query to test answer.
@pytest.mark.filterwarnings("ignore:.*custom attributes")
def test_answer_inline_query(self, monkeypatch, bot):
# For now just test that our internals pass the correct data
def test(url, data, *args, **kwargs):
@@ -960,8 +979,7 @@ class TestBot:
monkeypatch.delattr(bot, '_post')
# TODO: Needs improvement. No feasible way to test until bots can add members.
@pytest.mark.filterwarnings("ignore:.*custom attributes")
def test_kick_chat_member(self, monkeypatch, bot):
def test_ban_chat_member(self, monkeypatch, bot):
def test(url, data, *args, **kwargs):
chat_id = data['chat_id'] == 2
user_id = data['user_id'] == 32
@@ -972,13 +990,13 @@ class TestBot:
monkeypatch.setattr(bot.request, 'post', test)
until = from_timestamp(1577887200)
assert bot.kick_chat_member(2, 32)
assert bot.kick_chat_member(2, 32, until_date=until)
assert bot.kick_chat_member(2, 32, until_date=1577887200)
assert bot.kick_chat_member(2, 32, revoke_messages=True)
assert bot.ban_chat_member(2, 32)
assert bot.ban_chat_member(2, 32, until_date=until)
assert bot.ban_chat_member(2, 32, until_date=1577887200)
assert bot.ban_chat_member(2, 32, revoke_messages=True)
monkeypatch.delattr(bot.request, 'post')
def test_kick_chat_member_default_tz(self, monkeypatch, tz_bot):
def test_ban_chat_member_default_tz(self, monkeypatch, tz_bot):
until = dtm.datetime(2020, 1, 11, 16, 13)
until_timestamp = to_timestamp(until, tzinfo=tz_bot.defaults.tzinfo)
@@ -990,9 +1008,21 @@ class TestBot:
monkeypatch.setattr(tz_bot.request, 'post', test)
assert tz_bot.kick_chat_member(2, 32)
assert tz_bot.kick_chat_member(2, 32, until_date=until)
assert tz_bot.kick_chat_member(2, 32, until_date=until_timestamp)
assert tz_bot.ban_chat_member(2, 32)
assert tz_bot.ban_chat_member(2, 32, until_date=until)
assert tz_bot.ban_chat_member(2, 32, until_date=until_timestamp)
def test_kick_chat_member_warning(self, monkeypatch, bot, recwarn):
def test(url, data, *args, **kwargs):
chat_id = data['chat_id'] == 2
user_id = data['user_id'] == 32
return chat_id and user_id
monkeypatch.setattr(bot.request, 'post', test)
bot.kick_chat_member(2, 32)
assert len(recwarn) == 1
assert '`bot.kick_chat_member` is deprecated' in str(recwarn[0].message)
monkeypatch.delattr(bot.request, 'post')
# TODO: Needs improvement.
@pytest.mark.parametrize('only_if_banned', [True, False, None])
@@ -1330,11 +1360,21 @@ class TestBot:
assert a.status in ('administrator', 'creator')
@flaky(3, 1)
def test_get_chat_members_count(self, bot, channel_id):
count = bot.get_chat_members_count(channel_id)
def test_get_chat_member_count(self, bot, channel_id):
count = bot.get_chat_member_count(channel_id)
assert isinstance(count, int)
assert count > 3
def test_get_chat_members_count_warning(self, bot, channel_id, recwarn):
bot.get_chat_members_count(channel_id)
assert len(recwarn) == 1
assert '`bot.get_chat_members_count` is deprecated' in str(recwarn[0].message)
def test_bot_command_property_warning(self, bot, recwarn):
_ = bot.commands
assert len(recwarn) == 1
assert 'Bot.commands has been deprecated since there can' in str(recwarn[0].message)
@flaky(3, 1)
def test_get_chat_member(self, bot, channel_id, chat_id):
chat_member = bot.get_chat_member(channel_id, chat_id)
@@ -1657,6 +1697,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):
@@ -1681,12 +1752,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
@@ -1711,16 +1799,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():
@@ -1935,6 +2056,44 @@ class TestBot:
assert bc[1].command == 'cmd2'
assert bc[1].description == 'descr2'
@flaky(3, 1)
def test_get_set_delete_my_commands_with_scope(self, bot, super_group_id, chat_id):
group_cmds = [BotCommand('group_cmd', 'visible to this supergroup only')]
private_cmds = [BotCommand('private_cmd', 'visible to this private chat only')]
group_scope = BotCommandScopeChat(super_group_id)
private_scope = BotCommandScopeChat(chat_id)
# Set supergroup command list with lang code and check if the same can be returned from api
bot.set_my_commands(group_cmds, scope=group_scope, language_code='en')
gotten_group_cmds = bot.get_my_commands(scope=group_scope, language_code='en')
assert len(gotten_group_cmds) == len(group_cmds)
assert gotten_group_cmds[0].command == group_cmds[0].command
# Set private command list and check if same can be returned from the api
bot.set_my_commands(private_cmds, scope=private_scope)
gotten_private_cmd = bot.get_my_commands(scope=private_scope)
assert len(gotten_private_cmd) == len(private_cmds)
assert gotten_private_cmd[0].command == private_cmds[0].command
assert len(bot.commands) == 2 # set from previous test. Makes sure this hasn't changed.
assert bot.commands[0].command == 'cmd1'
# Delete command list from that supergroup and private chat-
bot.delete_my_commands(private_scope)
bot.delete_my_commands(group_scope, 'en')
# Check if its been deleted-
deleted_priv_cmds = bot.get_my_commands(scope=private_scope)
deleted_grp_cmds = bot.get_my_commands(scope=group_scope, language_code='en')
assert len(deleted_grp_cmds) == 0 == len(group_cmds) - 1
assert len(deleted_priv_cmds) == 0 == len(private_cmds) - 1
bot.delete_my_commands() # Delete commands from default scope
assert not bot.commands # Check if this has been updated to reflect the deletion.
def test_log_out(self, monkeypatch, bot):
# We don't actually make a request as to not break the test setup
def assertion(url, data, *args, **kwargs):
@@ -1955,7 +2114,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")]]
)
@@ -1964,7 +2124,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()
@@ -1977,7 +2137,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,
@@ -2373,3 +2533,37 @@ class TestBot:
bot.arbitrary_callback_data = False
bot.callback_data_cache.clear_callback_data()
bot.callback_data_cache.clear_callback_queries()
@pytest.mark.parametrize(
'cls,warn', [(Bot, True), (BotSubClass, True), (ExtBot, False), (ExtBotSubClass, False)]
)
def test_defaults_warning(self, bot, recwarn, cls, warn):
defaults = Defaults()
cls(bot.token, defaults=defaults)
if warn:
assert len(recwarn) == 1
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 == []
+205
View File
@@ -0,0 +1,205 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
# Copyright (C) 2021
# Leandro Toledo de Souza <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/].
from copy import deepcopy
import pytest
from telegram import (
Dice,
BotCommandScope,
BotCommandScopeDefault,
BotCommandScopeAllPrivateChats,
BotCommandScopeAllGroupChats,
BotCommandScopeAllChatAdministrators,
BotCommandScopeChat,
BotCommandScopeChatAdministrators,
BotCommandScopeChatMember,
)
@pytest.fixture(scope="class", params=['str', 'int'])
def chat_id(request):
if request.param == 'str':
return '@supergroupusername'
return 43
@pytest.fixture(
scope="class",
params=[
BotCommandScope.DEFAULT,
BotCommandScope.ALL_PRIVATE_CHATS,
BotCommandScope.ALL_GROUP_CHATS,
BotCommandScope.ALL_CHAT_ADMINISTRATORS,
BotCommandScope.CHAT,
BotCommandScope.CHAT_ADMINISTRATORS,
BotCommandScope.CHAT_MEMBER,
],
)
def scope_type(request):
return request.param
@pytest.fixture(
scope="class",
params=[
BotCommandScopeDefault,
BotCommandScopeAllPrivateChats,
BotCommandScopeAllGroupChats,
BotCommandScopeAllChatAdministrators,
BotCommandScopeChat,
BotCommandScopeChatAdministrators,
BotCommandScopeChatMember,
],
ids=[
BotCommandScope.DEFAULT,
BotCommandScope.ALL_PRIVATE_CHATS,
BotCommandScope.ALL_GROUP_CHATS,
BotCommandScope.ALL_CHAT_ADMINISTRATORS,
BotCommandScope.CHAT,
BotCommandScope.CHAT_ADMINISTRATORS,
BotCommandScope.CHAT_MEMBER,
],
)
def scope_class(request):
return request.param
@pytest.fixture(
scope="class",
params=[
(BotCommandScopeDefault, BotCommandScope.DEFAULT),
(BotCommandScopeAllPrivateChats, BotCommandScope.ALL_PRIVATE_CHATS),
(BotCommandScopeAllGroupChats, BotCommandScope.ALL_GROUP_CHATS),
(BotCommandScopeAllChatAdministrators, BotCommandScope.ALL_CHAT_ADMINISTRATORS),
(BotCommandScopeChat, BotCommandScope.CHAT),
(BotCommandScopeChatAdministrators, BotCommandScope.CHAT_ADMINISTRATORS),
(BotCommandScopeChatMember, BotCommandScope.CHAT_MEMBER),
],
ids=[
BotCommandScope.DEFAULT,
BotCommandScope.ALL_PRIVATE_CHATS,
BotCommandScope.ALL_GROUP_CHATS,
BotCommandScope.ALL_CHAT_ADMINISTRATORS,
BotCommandScope.CHAT,
BotCommandScope.CHAT_ADMINISTRATORS,
BotCommandScope.CHAT_MEMBER,
],
)
def scope_class_and_type(request):
return request.param
@pytest.fixture(scope='class')
def bot_command_scope(scope_class_and_type, chat_id):
return scope_class_and_type[0](type=scope_class_and_type[1], chat_id=chat_id, user_id=42)
# All the scope types are very similar, so we test everything via parametrization
class TestBotCommandScope:
def test_slot_behaviour(self, bot_command_scope, mro_slots, recwarn):
for attr in bot_command_scope.__slots__:
assert getattr(bot_command_scope, attr, 'err') != 'err', f"got extra slot '{attr}'"
assert not bot_command_scope.__dict__, f"got missing slot(s): {bot_command_scope.__dict__}"
assert len(mro_slots(bot_command_scope)) == len(
set(mro_slots(bot_command_scope))
), "duplicate slot"
bot_command_scope.custom, bot_command_scope.type = 'warning!', bot_command_scope.type
assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list
def test_de_json(self, bot, scope_class_and_type, chat_id):
cls = scope_class_and_type[0]
type_ = scope_class_and_type[1]
assert cls.de_json({}, bot) is None
json_dict = {'type': type_, 'chat_id': chat_id, 'user_id': 42}
bot_command_scope = BotCommandScope.de_json(json_dict, bot)
assert isinstance(bot_command_scope, BotCommandScope)
assert isinstance(bot_command_scope, cls)
assert bot_command_scope.type == type_
if 'chat_id' in cls.__slots__:
assert bot_command_scope.chat_id == chat_id
if 'user_id' in cls.__slots__:
assert bot_command_scope.user_id == 42
def test_de_json_invalid_type(self, bot):
json_dict = {'type': 'invalid', 'chat_id': chat_id, 'user_id': 42}
bot_command_scope = BotCommandScope.de_json(json_dict, bot)
assert type(bot_command_scope) is BotCommandScope
assert bot_command_scope.type == 'invalid'
def test_de_json_subclass(self, scope_class, bot, chat_id):
"""This makes sure that e.g. BotCommandScopeDefault(data) never returns a
BotCommandScopeChat instance."""
json_dict = {'type': 'invalid', 'chat_id': chat_id, 'user_id': 42}
assert type(scope_class.de_json(json_dict, bot)) is scope_class
def test_to_dict(self, bot_command_scope):
bot_command_scope_dict = bot_command_scope.to_dict()
assert isinstance(bot_command_scope_dict, dict)
assert bot_command_scope['type'] == bot_command_scope.type
if hasattr(bot_command_scope, 'chat_id'):
assert bot_command_scope['chat_id'] == bot_command_scope.chat_id
if hasattr(bot_command_scope, 'user_id'):
assert bot_command_scope['user_id'] == bot_command_scope.user_id
def test_equality(self, bot_command_scope, bot):
a = BotCommandScope('base_type')
b = BotCommandScope('base_type')
c = bot_command_scope
d = deepcopy(bot_command_scope)
e = Dice(4, 'emoji')
assert a == b
assert hash(a) == hash(b)
assert a != c
assert hash(a) != hash(c)
assert a != d
assert hash(a) != hash(d)
assert a != e
assert hash(a) != hash(e)
assert c == d
assert hash(c) == hash(d)
assert c != e
assert hash(c) != hash(e)
if hasattr(c, 'chat_id'):
json_dict = c.to_dict()
json_dict['chat_id'] = 0
f = c.__class__.de_json(json_dict, bot)
assert c != f
assert hash(c) != hash(f)
if hasattr(c, 'user_id'):
json_dict = c.to_dict()
json_dict['user_id'] = 0
g = c.__class__.de_json(json_dict, bot)
assert c != g
assert hash(c) != hash(g)
+61 -10
View File
@@ -176,18 +176,27 @@ class TestChat:
monkeypatch.setattr(chat.bot, 'get_chat_administrators', make_assertion)
assert chat.get_administrators()
def test_get_members_count(self, monkeypatch, chat):
def test_get_member_count(self, monkeypatch, chat):
def make_assertion(*_, **kwargs):
return kwargs['chat_id'] == chat.id
assert check_shortcut_signature(
Chat.get_members_count, Bot.get_chat_members_count, ['chat_id'], []
Chat.get_member_count, Bot.get_chat_member_count, ['chat_id'], []
)
assert check_shortcut_call(chat.get_members_count, chat.bot, 'get_chat_members_count')
assert check_defaults_handling(chat.get_members_count, chat.bot)
assert check_shortcut_call(chat.get_member_count, chat.bot, 'get_chat_member_count')
assert check_defaults_handling(chat.get_member_count, chat.bot)
monkeypatch.setattr(chat.bot, 'get_chat_members_count', make_assertion)
monkeypatch.setattr(chat.bot, 'get_chat_member_count', make_assertion)
assert chat.get_member_count()
def test_get_members_count_warning(self, chat, monkeypatch, recwarn):
def make_assertion(*_, **kwargs):
return kwargs['chat_id'] == chat.id
monkeypatch.setattr(chat.bot, 'get_chat_member_count', make_assertion)
assert chat.get_members_count()
assert len(recwarn) == 1
assert '`Chat.get_members_count` is deprecated' in str(recwarn[0].message)
def test_get_member(self, monkeypatch, chat):
def make_assertion(*_, **kwargs):
@@ -202,19 +211,31 @@ class TestChat:
monkeypatch.setattr(chat.bot, 'get_chat_member', make_assertion)
assert chat.get_member(user_id=42)
def test_kick_member(self, monkeypatch, chat):
def test_ban_member(self, monkeypatch, chat):
def make_assertion(*_, **kwargs):
chat_id = kwargs['chat_id'] == chat.id
user_id = kwargs['user_id'] == 42
until = kwargs['until_date'] == 43
return chat_id and user_id and until
assert check_shortcut_signature(Chat.kick_member, Bot.kick_chat_member, ['chat_id'], [])
assert check_shortcut_call(chat.kick_member, chat.bot, 'kick_chat_member')
assert check_defaults_handling(chat.kick_member, chat.bot)
assert check_shortcut_signature(Chat.ban_member, Bot.ban_chat_member, ['chat_id'], [])
assert check_shortcut_call(chat.ban_member, chat.bot, 'ban_chat_member')
assert check_defaults_handling(chat.ban_member, chat.bot)
monkeypatch.setattr(chat.bot, 'kick_chat_member', make_assertion)
monkeypatch.setattr(chat.bot, 'ban_chat_member', make_assertion)
assert chat.ban_member(user_id=42, until_date=43)
def test_kick_member_warning(self, chat, monkeypatch, recwarn):
def make_assertion(*_, **kwargs):
chat_id = kwargs['chat_id'] == chat.id
user_id = kwargs['user_id'] == 42
until = kwargs['until_date'] == 43
return chat_id and user_id and until
monkeypatch.setattr(chat.bot, 'ban_chat_member', make_assertion)
assert chat.kick_member(user_id=42, until_date=43)
assert len(recwarn) == 1
assert '`Chat.kick_member` is deprecated' in str(recwarn[0].message)
@pytest.mark.parametrize('only_if_banned', [True, False, None])
def test_unban_member(self, monkeypatch, chat, only_if_banned):
@@ -615,6 +636,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
+180 -53
View File
@@ -17,11 +17,22 @@
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
import datetime
from copy import deepcopy
import pytest
from telegram import User, ChatMember
from telegram.utils.helpers import to_timestamp
from telegram import (
User,
ChatMember,
ChatMemberOwner,
ChatMemberAdministrator,
ChatMemberMember,
ChatMemberRestricted,
ChatMemberLeft,
ChatMemberBanned,
Dice,
)
@pytest.fixture(scope='class')
@@ -29,38 +40,68 @@ def user():
return User(1, 'First name', False)
@pytest.fixture(
scope="class",
params=[
(ChatMemberOwner, ChatMember.CREATOR),
(ChatMemberAdministrator, ChatMember.ADMINISTRATOR),
(ChatMemberMember, ChatMember.MEMBER),
(ChatMemberRestricted, ChatMember.RESTRICTED),
(ChatMemberLeft, ChatMember.LEFT),
(ChatMemberBanned, ChatMember.KICKED),
],
ids=[
ChatMember.CREATOR,
ChatMember.ADMINISTRATOR,
ChatMember.MEMBER,
ChatMember.RESTRICTED,
ChatMember.LEFT,
ChatMember.KICKED,
],
)
def chat_member_class_and_status(request):
return request.param
@pytest.fixture(scope='class')
def chat_member(user):
return ChatMember(user, TestChatMember.status)
def chat_member_types(chat_member_class_and_status, user):
return chat_member_class_and_status[0](status=chat_member_class_and_status[1], user=user)
class TestChatMember:
status = ChatMember.CREATOR
def test_slot_behaviour(self, chat_member, recwarn, mro_slots):
for attr in chat_member.__slots__:
assert getattr(chat_member, attr, 'err') != 'err', f"got extra slot '{attr}'"
assert not chat_member.__dict__, f"got missing slot(s): {chat_member.__dict__}"
assert len(mro_slots(chat_member)) == len(set(mro_slots(chat_member))), "duplicate slot"
chat_member.custom, chat_member.status = 'should give warning', self.status
def test_slot_behaviour(self, chat_member_types, mro_slots, recwarn):
for attr in chat_member_types.__slots__:
assert getattr(chat_member_types, attr, 'err') != 'err', f"got extra slot '{attr}'"
assert not chat_member_types.__dict__, f"got missing slot(s): {chat_member_types.__dict__}"
assert len(mro_slots(chat_member_types)) == len(
set(mro_slots(chat_member_types))
), "duplicate slot"
chat_member_types.custom, chat_member_types.status = 'warning!', chat_member_types.status
assert len(recwarn) == 1 and 'custom' in str(recwarn[0].message), recwarn.list
def test_de_json_required_args(self, bot, user):
json_dict = {'user': user.to_dict(), 'status': self.status}
def test_de_json_required_args(self, bot, chat_member_class_and_status, user):
cls = chat_member_class_and_status[0]
status = chat_member_class_and_status[1]
chat_member = ChatMember.de_json(json_dict, bot)
assert cls.de_json({}, bot) is None
assert chat_member.user == user
assert chat_member.status == self.status
json_dict = {'status': status, 'user': user.to_dict()}
chat_member_type = ChatMember.de_json(json_dict, bot)
def test_de_json_all_args(self, bot, user):
assert isinstance(chat_member_type, ChatMember)
assert isinstance(chat_member_type, cls)
assert chat_member_type.status == status
assert chat_member_type.user == user
def test_de_json_all_args(self, bot, chat_member_class_and_status, user):
cls = chat_member_class_and_status[0]
status = chat_member_class_and_status[1]
time = datetime.datetime.utcnow()
custom_title = 'custom_title'
json_dict = {
'user': user.to_dict(),
'status': self.status,
'custom_title': custom_title,
'status': status,
'custom_title': 'PTB',
'is_anonymous': True,
'until_date': to_timestamp(time),
'can_be_edited': False,
@@ -80,48 +121,134 @@ class TestChatMember:
'can_manage_chat': True,
'can_manage_voice_chats': True,
}
chat_member_type = ChatMember.de_json(json_dict, bot)
chat_member = ChatMember.de_json(json_dict, bot)
assert isinstance(chat_member_type, ChatMember)
assert isinstance(chat_member_type, cls)
assert chat_member_type.user == user
assert chat_member_type.status == status
if chat_member_type.custom_title is not None:
assert chat_member_type.custom_title == 'PTB'
assert type(chat_member_type) in {ChatMemberOwner, ChatMemberAdministrator}
if chat_member_type.is_anonymous is not None:
assert chat_member_type.is_anonymous is True
assert type(chat_member_type) in {ChatMemberOwner, ChatMemberAdministrator}
if chat_member_type.until_date is not None:
assert type(chat_member_type) in {ChatMemberBanned, ChatMemberRestricted}
if chat_member_type.can_be_edited is not None:
assert chat_member_type.can_be_edited is False
assert type(chat_member_type) == ChatMemberAdministrator
if chat_member_type.can_change_info is not None:
assert chat_member_type.can_change_info is True
assert type(chat_member_type) in {ChatMemberAdministrator, ChatMemberRestricted}
if chat_member_type.can_post_messages is not None:
assert chat_member_type.can_post_messages is False
assert type(chat_member_type) == ChatMemberAdministrator
if chat_member_type.can_edit_messages is not None:
assert chat_member_type.can_edit_messages is True
assert type(chat_member_type) == ChatMemberAdministrator
if chat_member_type.can_delete_messages is not None:
assert chat_member_type.can_delete_messages is True
assert type(chat_member_type) == ChatMemberAdministrator
if chat_member_type.can_invite_users is not None:
assert chat_member_type.can_invite_users is False
assert type(chat_member_type) in {ChatMemberAdministrator, ChatMemberRestricted}
if chat_member_type.can_restrict_members is not None:
assert chat_member_type.can_restrict_members is True
assert type(chat_member_type) == ChatMemberAdministrator
if chat_member_type.can_pin_messages is not None:
assert chat_member_type.can_pin_messages is False
assert type(chat_member_type) in {ChatMemberAdministrator, ChatMemberRestricted}
if chat_member_type.can_promote_members is not None:
assert chat_member_type.can_promote_members is True
assert type(chat_member_type) == ChatMemberAdministrator
if chat_member_type.can_send_messages is not None:
assert chat_member_type.can_send_messages is False
assert type(chat_member_type) == ChatMemberRestricted
if chat_member_type.can_send_media_messages is not None:
assert chat_member_type.can_send_media_messages is True
assert type(chat_member_type) == ChatMemberRestricted
if chat_member_type.can_send_polls is not None:
assert chat_member_type.can_send_polls is False
assert type(chat_member_type) == ChatMemberRestricted
if chat_member_type.can_send_other_messages is not None:
assert chat_member_type.can_send_other_messages is True
assert type(chat_member_type) == ChatMemberRestricted
if chat_member_type.can_add_web_page_previews is not None:
assert chat_member_type.can_add_web_page_previews is False
assert type(chat_member_type) == ChatMemberRestricted
if chat_member_type.can_manage_chat is not None:
assert chat_member_type.can_manage_chat is True
assert type(chat_member_type) == ChatMemberAdministrator
if chat_member_type.can_manage_voice_chats is not None:
assert chat_member_type.can_manage_voice_chats is True
assert type(chat_member_type) == ChatMemberAdministrator
assert chat_member.user == user
assert chat_member.status == self.status
assert chat_member.custom_title == custom_title
assert chat_member.is_anonymous is True
assert chat_member.can_be_edited is False
assert chat_member.can_change_info is True
assert chat_member.can_post_messages is False
assert chat_member.can_edit_messages is True
assert chat_member.can_delete_messages is True
assert chat_member.can_invite_users is False
assert chat_member.can_restrict_members is True
assert chat_member.can_pin_messages is False
assert chat_member.can_promote_members is True
assert chat_member.can_send_messages is False
assert chat_member.can_send_media_messages is True
assert chat_member.can_send_polls is False
assert chat_member.can_send_other_messages is True
assert chat_member.can_add_web_page_previews is False
assert chat_member.can_manage_chat is True
assert chat_member.can_manage_voice_chats is True
def test_de_json_invalid_status(self, bot, user):
json_dict = {'status': 'invalid', 'user': user.to_dict()}
chat_member_type = ChatMember.de_json(json_dict, bot)
assert type(chat_member_type) is ChatMember
assert chat_member_type.status == 'invalid'
def test_de_json_subclass(self, chat_member_class_and_status, bot, chat_id, user):
"""This makes sure that e.g. ChatMemberAdministrator(data, bot) never returns a
ChatMemberKicked instance."""
cls = chat_member_class_and_status[0]
time = datetime.datetime.utcnow()
json_dict = {
'user': user.to_dict(),
'status': 'status',
'custom_title': 'PTB',
'is_anonymous': True,
'until_date': to_timestamp(time),
'can_be_edited': False,
'can_change_info': True,
'can_post_messages': False,
'can_edit_messages': True,
'can_delete_messages': True,
'can_invite_users': False,
'can_restrict_members': True,
'can_pin_messages': False,
'can_promote_members': True,
'can_send_messages': False,
'can_send_media_messages': True,
'can_send_polls': False,
'can_send_other_messages': True,
'can_add_web_page_previews': False,
'can_manage_chat': True,
'can_manage_voice_chats': True,
}
assert type(cls.de_json(json_dict, bot)) is cls
def test_to_dict(self, chat_member_types, user):
chat_member_dict = chat_member_types.to_dict()
def test_to_dict(self, chat_member):
chat_member_dict = chat_member.to_dict()
assert isinstance(chat_member_dict, dict)
assert chat_member_dict['user'] == chat_member.user.to_dict()
assert chat_member['status'] == chat_member.status
assert chat_member_dict['status'] == chat_member_types.status
assert chat_member_dict['user'] == user.to_dict()
def test_equality(self):
a = ChatMember(User(1, '', False), ChatMember.ADMINISTRATOR)
b = ChatMember(User(1, '', False), ChatMember.ADMINISTRATOR)
d = ChatMember(User(2, '', False), ChatMember.ADMINISTRATOR)
d2 = ChatMember(User(1, '', False), ChatMember.CREATOR)
def test_equality(self, chat_member_types, user):
a = ChatMember(status='status', user=user)
b = ChatMember(status='status', user=user)
c = chat_member_types
d = deepcopy(chat_member_types)
e = Dice(4, 'emoji')
assert a == b
assert hash(a) == hash(b)
assert a is not b
assert a != c
assert hash(a) != hash(c)
assert a != d
assert hash(a) != hash(d)
assert a != d2
assert hash(a) != hash(d2)
assert a != e
assert hash(a) != hash(e)
assert c == d
assert hash(c) == hash(d)
assert c != e
assert hash(c) != hash(e)
+8 -1
View File
@@ -25,12 +25,17 @@ from telegram import ForceReply, ReplyKeyboardRemove
@pytest.fixture(scope='class')
def force_reply():
return ForceReply(TestForceReply.force_reply, TestForceReply.selective)
return ForceReply(
TestForceReply.force_reply,
TestForceReply.selective,
TestForceReply.input_field_placeholder,
)
class TestForceReply:
force_reply = True
selective = True
input_field_placeholder = 'force replies can be annoying if not used properly'
def test_slot_behaviour(self, force_reply, recwarn, mro_slots):
for attr in force_reply.__slots__:
@@ -49,6 +54,7 @@ class TestForceReply:
def test_expected(self, force_reply):
assert force_reply.force_reply == self.force_reply
assert force_reply.selective == self.selective
assert force_reply.input_field_placeholder == self.input_field_placeholder
def test_to_dict(self, force_reply):
force_reply_dict = force_reply.to_dict()
@@ -56,6 +62,7 @@ class TestForceReply:
assert isinstance(force_reply_dict, dict)
assert force_reply_dict['force_reply'] == force_reply.force_reply
assert force_reply_dict['selective'] == force_reply.selective
assert force_reply_dict['input_field_placeholder'] == force_reply.input_field_placeholder
def test_equality(self):
a = ForceReply(True, False)
+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']
+33 -2
View File
@@ -118,9 +118,13 @@ def check_object(h4):
if field == 'from':
field = 'from_user'
elif (
name.startswith('InlineQueryResult') or name.startswith('InputMedia')
name.startswith('InlineQueryResult')
or name.startswith('InputMedia')
or name.startswith('BotCommandScope')
) and field == 'type':
continue
elif (name.startswith('ChatMember')) and field == 'status':
continue
elif (
name.startswith('PassportElementError') and field == 'source'
) or field == 'remove_keyboard':
@@ -136,7 +140,34 @@ def check_object(h4):
if name == 'InputFile':
return
if name == 'InlineQueryResult':
ignored |= {'id', 'type'}
ignored |= {'id', 'type'} # attributes common to all subclasses
if name == 'ChatMember':
ignored |= {'user', 'status'} # attributes common to all subclasses
if name == 'ChatMember':
ignored |= {
'can_add_web_page_previews', # for backwards compatibility
'can_be_edited',
'can_change_info',
'can_delete_messages',
'can_edit_messages',
'can_invite_users',
'can_manage_chat',
'can_manage_voice_chats',
'can_pin_messages',
'can_post_messages',
'can_promote_members',
'can_restrict_members',
'can_send_media_messages',
'can_send_messages',
'can_send_other_messages',
'can_send_polls',
'custom_title',
'is_anonymous',
'is_member',
'until_date',
}
if name == 'BotCommandScope':
ignored |= {'type'} # attributes common to all subclasses
elif name == 'User':
ignored |= {'type'} # TODO: Deprecation
elif name in ('PassportFile', 'EncryptedPassportElement'):
+53 -2
View File
@@ -18,6 +18,7 @@
# along with this program. If not, see [http://www.gnu.org/licenses/].
import gzip
import signal
import uuid
from threading import Lock
from telegram.ext.callbackdatacache import CallbackDataCache
@@ -31,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
@@ -561,13 +563,28 @@ class TestBasePersistence:
def test_bot_replace_insert_bot(self, bot, bot_persistence):
class CustomSlottedClass:
__slots__ = ('bot',)
__slots__ = ('bot', '__dict__')
def __init__(self):
self.bot = bot
self.not_in_slots = bot
def __eq__(self, other):
if isinstance(other, CustomSlottedClass):
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, CustomSlottedClass):
if isinstance(other, DictNotInSlots):
return self.bot is other.bot
return False
@@ -575,6 +592,7 @@ class TestBasePersistence:
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_)
@@ -587,6 +605,8 @@ class TestBasePersistence:
cc = CustomClass()
cc.bot = BasePersistence.REPLACED_BOT
cc.slotted_object.bot = 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_)
@@ -600,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_
@@ -687,6 +708,36 @@ class TestBasePersistence:
"BasePersistence.insert_bot does not handle objects that can not be copied."
)
def test_bot_replace_insert_bot_unparsable_objects(self, bot, bot_persistence, recwarn):
"""Here check that objects in __dict__ or __slots__ that can't
be parsed are just returned verbatim."""
persistence = bot_persistence
persistence.set_bot(bot)
uuid_obj = uuid.uuid4()
persistence.update_bot_data({1: uuid_obj})
assert persistence.bot_data[1] is uuid_obj
persistence.update_chat_data(123, {1: uuid_obj})
assert persistence.chat_data[123][1] is uuid_obj
persistence.update_user_data(123, {1: uuid_obj})
assert persistence.user_data[123][1] is uuid_obj
persistence.update_callback_data(([('1', 2, {0: uuid_obj})], {'1': '2'}))
assert persistence.callback_data[0][0][2][0] is uuid_obj
assert persistence.get_bot_data()[1] is uuid_obj
assert persistence.get_chat_data()[123][1] is uuid_obj
assert persistence.get_user_data()[123][1] is uuid_obj
assert persistence.get_callback_data()[0][0][2][0] is uuid_obj
assert len(recwarn) == 2
assert str(recwarn[0].message).startswith(
"Parsing of an object failed with the following exception: "
)
assert str(recwarn[1].message).startswith(
"Parsing of an object failed with the following exception: "
)
def test_bot_replace_insert_bot_classes(self, bot, bot_persistence, recwarn):
"""Here check that classes are just returned verbatim."""
persistence = bot_persistence
+7
View File
@@ -29,6 +29,7 @@ def reply_keyboard_markup():
resize_keyboard=TestReplyKeyboardMarkup.resize_keyboard,
one_time_keyboard=TestReplyKeyboardMarkup.one_time_keyboard,
selective=TestReplyKeyboardMarkup.selective,
input_field_placeholder=TestReplyKeyboardMarkup.input_field_placeholder,
)
@@ -37,6 +38,7 @@ class TestReplyKeyboardMarkup:
resize_keyboard = True
one_time_keyboard = True
selective = True
input_field_placeholder = 'lol a keyboard'
def test_slot_behaviour(self, reply_keyboard_markup, mro_slots, recwarn):
inst = reply_keyboard_markup
@@ -101,6 +103,7 @@ class TestReplyKeyboardMarkup:
assert reply_keyboard_markup.resize_keyboard == self.resize_keyboard
assert reply_keyboard_markup.one_time_keyboard == self.one_time_keyboard
assert reply_keyboard_markup.selective == self.selective
assert reply_keyboard_markup.input_field_placeholder == self.input_field_placeholder
def test_to_dict(self, reply_keyboard_markup):
reply_keyboard_markup_dict = reply_keyboard_markup.to_dict()
@@ -122,6 +125,10 @@ class TestReplyKeyboardMarkup:
== reply_keyboard_markup.one_time_keyboard
)
assert reply_keyboard_markup_dict['selective'] == reply_keyboard_markup.selective
assert (
reply_keyboard_markup_dict['input_field_placeholder']
== reply_keyboard_markup.input_field_placeholder
)
def test_equality(self):
a = ReplyKeyboardMarkup.from_column(['button1', 'button2', 'button3'])
+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:
+23 -5
View File
@@ -504,8 +504,13 @@ class TestUpdater:
updater.start_webhook(ip, port, clean=True, force_event_loop=False)
updater.stop()
for warning in recwarn.list:
print(warning.message)
for warning in recwarn:
print(warning)
try: # This is for flaky tests (there's an unclosed socket sometimes)
recwarn.pop(ResourceWarning) # internally iterates through recwarn.list and deletes it
except AssertionError:
pass
assert len(recwarn) == 3
assert str(recwarn[0].message).startswith('Old Handler API')
@@ -523,6 +528,12 @@ class TestUpdater:
updater.stop()
for msg in recwarn:
print(msg)
try: # This is for flaky tests (there's an unclosed socket sometimes)
recwarn.pop(ResourceWarning) # internally iterates through recwarn.list and deletes it
except AssertionError:
pass
assert len(recwarn) == 2
assert str(recwarn[0].message).startswith('Old Handler API')
assert str(recwarn[1].message).startswith('The argument `clean` of')
@@ -621,10 +632,17 @@ class TestUpdater:
# There is a chance of a conflict when getting updates since there can be many tests
# (bots) running simultaneously while testing in github actions.
for idx, log in enumerate(caplog.records):
if log.getMessage().startswith('Error while getting Updates: Conflict'):
records = caplog.records.copy() # To avoid iterating and removing at same time
for idx, log in enumerate(records):
print(log)
msg = log.getMessage()
if msg.startswith('Error while getting Updates: Conflict'):
caplog.records.pop(idx) # For stability
assert len(caplog.records) == 2, caplog.records
if msg.startswith('No error handlers are registered'):
caplog.records.pop(idx)
assert len(caplog.records) == 2, caplog.records
rec = caplog.records[-2]
assert rec.getMessage().startswith(f'Received signal {signal.SIGTERM}')
+34
View File
@@ -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>'