Compare commits

..

36 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
Hinrich Mahler e982a5a567 Bump version to v13.6 2021-06-06 12:28:01 +02:00
Bibo-Joshi cf4d3cae01 Doc Fixes (#2495)
Co-authored-by: Poolitzer <25934244+Poolitzer@users.noreply.github.com>
Co-authored-by: Bas ten Berge <bas+github@tenberge-ict.nl>
Co-authored-by: Harshil <ilovebhagwan@gmail.com>
2021-06-06 12:16:23 +02:00
Bibo-Joshi 8531a7a40c Arbitrary callback_data (#1844) 2021-06-06 11:48:48 +02:00
Bibo-Joshi fce7cc903c Add ContextTypes & BasePersistence.refresh_user/chat/bot_data (#2262) 2021-06-06 10:37:53 +02:00
GauthamramRavichandran 5da1dd7ce9 Add max_connections Parameter to Updater.start_webhook (#2547)
* Include max_connections args

* Update docs & add test

Co-authored-by: Hinrich Mahler <hinrich.mahler@freenet.de>
2021-06-06 09:27:45 +02:00
Stɑrry Shivɑm 46cdeb495a Fix for Promise.done_callback (#2544)
* Don't call done_cb on exceptions

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

* improve docs

Co-authored-by: Bibo-Joshi <hinrich.mahler@freenet.de>

* revert black

Co-authored-by: Bibo-Joshi <hinrich.mahler@freenet.de>
2021-06-05 17:08:45 +02:00
Bibo-Joshi 653691fafb Improve Slot Tests (#2541) 2021-05-30 20:30:26 +02:00
Harshil 92ff6a8e2b Add __slots__ (#2345)
Co-authored-by: deepsource-autofix[bot] <62050782+deepsource-autofix[bot]@users.noreply.github.com>
Co-authored-by: Hinrich Mahler <hinrich.mahler@freenet.de>
2021-05-29 16:18:16 +02:00
Bibo-Joshi cc43aef64b Fix Test Failing Due To Telegram Updates (#2537)
* Update TestPhoto

* try to fix test_idle again...

* fix test_send_game due to photo size changes...

Co-authored-by: Harshil <ilovebhagwan@gmail.com>
2021-05-28 22:24:43 +02:00
Bibo-Joshi e2c6d60721 Improve Code Quality (#2536)
* Start fixing stuff

* More docstrings

* stabilize test_idle

Co-authored-by: Harshil <ilovebhagwan@gmail.com>
2021-05-27 20:34:58 +02:00
Bibo-Joshi 8bf88c3231 Supplement Codacy with DeepSource (#2454)
* Add deepsource config

* Update Badges

* Update Badges some more

* Stupid change to trigger analysis of all files

* Try to get ignore right

* Update badges again

* Get started on fixing issues

* Fix some more issues

* Remove more plank lines

* Docs for de_json/list & to_dict/json

* Some improvements from deepcode.ai

* Some more improvements

* Some more improvements

* More docstrnigs & let's run DS on the tests just for fun

* Autofix issues in 10 files

Resolved issues in the following files via DeepSource Autofix:
1. tests/conftest.py
2. tests/test_bot.py
3. tests/test_commandhandler.py
4. tests/test_conversationhandler.py
5. tests/test_dispatcher.py
6. tests/test_filters.py
7. tests/test_inputmedia.py
8. tests/test_messagehandler.py
9. tests/test_official.py
10. tests/test_persistence.py

* Some more improvements for tests, but that shall be enough

* Some more docstrings for functions

* Some minor stuff, try to fix tests

* Update DS config

* Still more docs

* Doc fixes

* More fixes

* Fix: indent docstring

* Some fixes

* Revert "Stupid change to trigger analysis of all files"

This reverts commit dd46c260

* Review

Co-authored-by: deepsource-autofix[bot] <62050782+deepsource-autofix[bot]@users.noreply.github.com>
Co-authored-by: Poolitzer <25934244+Poolitzer@users.noreply.github.com>
2021-05-27 09:38:17 +02:00
Poolitzer 1572c61063 Increase Test Coverage of CallbackQueryHandler (#2520)
* Test: let's see

* Test: let's see, now in the correct place

* Fix: Explicitly return None in else clause

also documented this behaviour clearly in the docstring

* add link in doc

Co-authored-by: Bibo-Joshi <hinrich.mahler@freenet.de>

* Fix: remove unnecessary else statement

Co-authored-by: Bibo-Joshi <hinrich.mahler@freenet.de>
2021-05-27 09:37:37 +02:00
Harshil 5ff3b76e18 Stabilize CI (#2522)
* asking the test what is wrong with them

* fix botscore_not_modified tests for good

* xfail game tests due to race conditions

* address review (add a comment)

* fix xfail marker

* address review

* simplify expression
2021-05-19 13:33:41 +02:00
Poolitzer cd69f69b28 Add Filters.attachment (#2528)
* feat: attachment filter

* fix: add versionadded statement

* Fix: small doc string changes

Co-authored-by: Bibo-Joshi <hinrich.mahler@freenet.de>

Co-authored-by: Bibo-Joshi <hinrich.mahler@freenet.de>
2021-05-19 10:39:10 +02:00
Poolitzer 8b0d2e5f75 Add pattern Argument to ChosenInlineResultHandler (#2517)
* Feat: add pattern arg to ChosenInlineResultHandler

* Fix: remove unnecessary if clause

the attribute must be present, since it's not optional.

* Fix: wrong type documentation

Co-authored-by: Bibo-Joshi <hinrich.mahler@freenet.de>

* Fix: Addressing code review

small documentation fixes, and moving one test.

* Fix: link to matches

Co-authored-by: Bibo-Joshi <hinrich.mahler@freenet.de>

* Fix: also link here

Co-authored-by: Bibo-Joshi <hinrich.mahler@freenet.de>
2021-05-19 10:32:11 +02:00
Nikolai Krivenko 7d0fb85c8c Fix send_phone_number_to_provider argument for Bot.send_invoice (#2527)
* [#2526] set data['send_phone_number_to_provider'] from corresponding variable

* [#2526] Add myself to AUTHORS.rst

* Add unit test

Co-authored-by: Hinrich Mahler <hinrich.mahler@freenet.de>
2021-05-16 20:56:28 +02:00
Bibo-Joshi 08ba7c7793 Handle Classes as Input for BasePersistence.replace/insert_bot (#2523)
* Ignore classes on replace/insert_bot

* Review
2021-05-16 20:02:35 +02:00
Bibo-Joshi 9737b1d3c7 Bump Tornado Version and Remove Workaround from #2067 (#2494) 2021-05-05 20:59:06 +02:00
337 changed files with 12018 additions and 1814 deletions
+20
View File
@@ -0,0 +1,20 @@
version = 1
test_patterns = ["tests/**"]
exclude_patterns = [
"tests/**",
"docs/**",
"telegram/vendor/**",
"setup.py",
"setup-raw.py"
]
[[analyzers]]
name = "python"
enabled = true
[analyzers.meta]
runtime_version = "3.x.x"
max_line_length = 99
skip_doc_coverage = ["module", "magic", "init", "nonpublic"]
+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)
+3 -3
View File
@@ -3,6 +3,6 @@ contact_links:
- name: Telegram Group
url: https://telegram.me/pythontelegrambotgroup
about: Questions asked on the group usually get answered faster.
- name: IRC Channel
url: https://webchat.freenode.net/?channels=##python-telegram-bot
about: In case you are unable to join our group due to Telegram restrictions, you can use our IRC channel
- name: GitHub Discussions
url: https://github.com/python-telegram-bot/python-telegram-bot/discussions
about: For getting answers to usage on GitHub, Discussions is even better than this bug tracker :)
@@ -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. In case you are unable to join our group due to Telegram restrictions, you can use our IRC channel at https://webchat.freenode.net/?channels=##python-telegram-bot to participate in the group.
-->
### Issue I am facing
Please describe the issue here in as much detail as possible
### Traceback to the issue
```
put it here
```
### Related part of your code
```python
put it here
```
+68
View File
@@ -0,0 +1,68 @@
name: Question
description: Get help with errors or general questions
title: "[QUESTION]"
labels: ["question"]
body:
- type: markdown
attributes:
value: |
Hey there, you have a question? We are happy to answer. Please make sure no similar question was opened already.
To make it easier for us to help you, please read this [article](https://git.io/JURJO).
Please mind that there is also a users' [Telegram group](https://t.me/pythontelegrambotgroup) for questions about the library. Questions asked there might be answered quicker than here. Moreover, [GitHub Discussions](https://git.io/JG3rk) offer a slightly better format to discuss usage questions.
- type: textarea
id: issue-faced
attributes:
label: "Issue I am facing"
description: "Please describe the issue here in as much detail as possible"
validations:
required: true
- type: textarea
id: traceback
attributes:
label: "Traceback to the issue"
description: "If you are facing a specific error message, please paste the traceback here. This will be automatically formatted into python code, so no need for backticks."
placeholder: |
Traceback (most recent call last):
File "/home/bot.py", line 1, in main
foo = bar()
...
telegram.error.BadRequest: Traceback not found
render: python
- type: textarea
id: related-code
attributes:
label: "Related part of your code"
description: "This will be automatically formatted into code (python), so no need for backticks."
placeholder: |
logging.basicConfig(
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", level=logging.INFO
)
logger = logging.getLogger(__name__)
render: python
- type: markdown
attributes:
value: "### Configuration"
- type: input
id: operating-system
attributes:
label: Operating System
validations:
required: true
- type: textarea
id: versions
attributes:
label: Version of Python, python-telegram-bot & dependencies
description: Paste the output of `$ python -m telegram` here. This will be automatically formatted into code, so no need for backticks.
render: shell
validations:
required: true
+1
View File
@@ -28,3 +28,4 @@ Hey! You're PRing? Cool! Please have a look at the below checklist. It's here to
- [ ] Added new filters for new message (sub)types
- [ ] Added or updated documentation for the changed class(es) and/or method(s)
- [ ] Updated the Bot API version number in all places: `README.rst` and `README_RAW.rst` (including the badge), as well as `telegram.constants.BOT_API_VERSION`
- [ ] Added logic for arbitrary callback data in `tg.ext.Bot` for new methods that either accept a `reply_markup` in some form or have a return type that is/contains `telegram.Message`
+3 -3
View File
@@ -60,7 +60,7 @@ jobs:
shell: bash --noprofile --norc {0}
- name: Submit coverage
uses: codecov/codecov-action@v1.0.13
uses: codecov/codecov-action@v1
with:
env_vars: OS,PYTHON
name: ${{ matrix.os }}-${{ matrix.python-version }}
@@ -79,7 +79,7 @@ jobs:
run:
git submodule update --init --recursive
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v1
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
@@ -108,7 +108,7 @@ jobs:
run:
git submodule update --init --recursive
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v1
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
+9 -6
View File
@@ -10,11 +10,11 @@ repos:
- --diff
- --check
- repo: https://gitlab.com/pycqa/flake8
rev: 3.9.1
rev: 3.9.2
hooks:
- id: flake8
- repo: https://github.com/PyCQA/pylint
rev: v2.8.2
rev: v2.8.3
hooks:
- id: pylint
files: ^(telegram|examples)/.*\.py$
@@ -22,8 +22,9 @@ repos:
- --rcfile=setup.cfg
additional_dependencies:
- certifi
- tornado>=5.1
- tornado>=6.1
- APScheduler==3.6.3
- cachetools==4.2.2
- . # this basically does `pip install -e .`
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v0.812
@@ -33,8 +34,9 @@ repos:
files: ^telegram/.*\.py$
additional_dependencies:
- certifi
- tornado>=5.1
- tornado>=6.1
- APScheduler==3.6.3
- cachetools==4.2.2
- . # this basically does `pip install -e .`
- id: mypy
name: mypy-examples
@@ -44,11 +46,12 @@ repos:
- --follow-imports=silent
additional_dependencies:
- certifi
- tornado>=5.1
- tornado>=6.1
- APScheduler==3.6.3
- cachetools==4.2.2
- . # this basically does `pip install -e .`
- repo: https://github.com/asottile/pyupgrade
rev: v2.13.0
rev: v2.19.1
hooks:
- id: pyupgrade
files: ^(telegram|examples|tests)/.*\.py$
+5
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>`_
@@ -77,12 +79,14 @@ The following wonderful people contributed directly or indirectly to this projec
- `naveenvhegde <https://github.com/naveenvhegde>`_
- `neurrone <https://github.com/neurrone>`_
- `NikitaPirate <https://github.com/NikitaPirate>`_
- `Nikolai Krivenko <https://github.com/nkrivenko>`_
- `njittam <https://github.com/njittam>`_
- `Noam Meltzer <https://github.com/tsnoam>`_
- `Oleg Shlyazhko <https://github.com/ollmer>`_
- `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>`_
@@ -105,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.
+108
View File
@@ -2,6 +2,114 @@
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*
New Features:
- Arbitrary ``callback_data`` (`#1844`_)
- Add ``ContextTypes`` & ``BasePersistence.refresh_user/chat/bot_data`` (`#2262`_)
- Add ``Filters.attachment`` (`#2528`_)
- Add ``pattern`` Argument to ``ChosenInlineResultHandler`` (`#2517`_)
Major Changes:
- Add ``slots`` (`#2345`_)
Minor changes, CI improvements, Doc fixes and Type hinting:
- Doc Fixes (`#2495`_, `#2510`_)
- Add ``max_connections`` Parameter to ``Updater.start_webhook`` (`#2547`_)
- Fix for ``Promise.done_callback`` (`#2544`_)
- Improve Code Quality (`#2536`_, `#2454`_)
- Increase Test Coverage of ``CallbackQueryHandler`` (`#2520`_)
- Stabilize CI (`#2522`_, `#2537`_, `#2541`_)
- Fix ``send_phone_number_to_provider`` argument for ``Bot.send_invoice`` (`#2527`_)
- Handle Classes as Input for ``BasePersistence.replace/insert_bot`` (`#2523`_)
- Bump Tornado Version and Remove Workaround from `#2067`_ (`#2494`_)
.. _`#1844`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1844
.. _`#2262`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2262
.. _`#2528`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2528
.. _`#2517`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2517
.. _`#2345`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2345
.. _`#2495`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2495
.. _`#2547`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2547
.. _`#2544`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2544
.. _`#2536`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2536
.. _`#2454`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2454
.. _`#2520`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2520
.. _`#2522`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2522
.. _`#2537`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2537
.. _`#2541`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2541
.. _`#2527`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2527
.. _`#2523`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2523
.. _`#2067`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2067
.. _`#2494`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2494
.. _`#2510`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2510
Version 13.5
============
*Released 2021-04-30*
+12 -14
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
@@ -50,19 +50,19 @@ We have a vibrant community of developers helping each other in our `Telegram gr
.. image:: https://api.codacy.com/project/badge/Grade/99d901eaa09b44b4819aec05c330c968
:target: https://www.codacy.com/app/python-telegram-bot/python-telegram-bot?utm_source=github.com&amp;utm_medium=referral&amp;utm_content=python-telegram-bot/python-telegram-bot&amp;utm_campaign=Badge_Grade
:alt: Code quality
:alt: Code quality: Codacy
.. image:: https://deepsource.io/gh/python-telegram-bot/python-telegram-bot.svg/?label=active+issues
:target: https://deepsource.io/gh/python-telegram-bot/python-telegram-bot/?ref=repository-badge
:alt: Code quality: DeepSource
.. image:: https://img.shields.io/badge/code%20style-black-000000.svg
:target: https://github.com/psf/black
:target: https://github.com/psf/black
.. image:: https://img.shields.io/badge/Telegram-Group-blue.svg?logo=telegram
:target: https://telegram.me/pythontelegrambotgroup
:alt: Telegram Group
.. image:: https://img.shields.io/badge/IRC-Channel-blue.svg
:target: https://webchat.freenode.net/?channels=##python-telegram-bot
:alt: IRC Bridge
=================
Table of contents
=================
@@ -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+. 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
@@ -215,13 +215,11 @@ You can get help in several ways:
1. We have a vibrant community of developers helping each other in our `Telegram group <https://telegram.me/pythontelegrambotgroup>`_. Join us!
2. In case you are unable to join our group due to Telegram restrictions, you can use our `IRC channel <https://webchat.freenode.net/?channels=##python-telegram-bot>`_.
2. Report bugs, request new features or ask questions by `creating an issue <https://github.com/python-telegram-bot/python-telegram-bot/issues/new/choose>`_ or `a discussion <https://github.com/python-telegram-bot/python-telegram-bot/discussions/new>`_.
3. Report bugs, request new features or ask questions by `creating an issue <https://github.com/python-telegram-bot/python-telegram-bot/issues/new/choose>`_ or `a discussion <https://github.com/python-telegram-bot/python-telegram-bot/discussions/new>`_.
3. Our `Wiki pages <https://github.com/python-telegram-bot/python-telegram-bot/wiki/>`_ offer a growing amount of resources.
4. Our `Wiki pages <https://github.com/python-telegram-bot/python-telegram-bot/wiki/>`_ offer a growing amount of resources.
5. You can even ask for help on Stack Overflow using the `python-telegram-bot tag <https://stackoverflow.com/questions/tagged/python-telegram-bot>`_.
4. You can even ask for help on Stack Overflow using the `python-telegram-bot tag <https://stackoverflow.com/questions/tagged/python-telegram-bot>`_.
============
+12 -14
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
@@ -50,19 +50,19 @@ We have a vibrant community of developers helping each other in our `Telegram gr
.. image:: https://api.codacy.com/project/badge/Grade/99d901eaa09b44b4819aec05c330c968
:target: https://www.codacy.com/app/python-telegram-bot/python-telegram-bot?utm_source=github.com&amp;utm_medium=referral&amp;utm_content=python-telegram-bot/python-telegram-bot&amp;utm_campaign=Badge_Grade
:alt: Code quality
:alt: Code quality: Codacy
.. image:: https://deepsource.io/gh/python-telegram-bot/python-telegram-bot.svg/?label=active+issues
:target: https://deepsource.io/gh/python-telegram-bot/python-telegram-bot/?ref=repository-badge
:alt: Code quality: DeepSource
.. image:: https://img.shields.io/badge/code%20style-black-000000.svg
:target: https://github.com/psf/black
:target: https://github.com/psf/black
.. image:: https://img.shields.io/badge/Telegram-Group-blue.svg?logo=telegram
:target: https://telegram.me/pythontelegrambotgroup
:alt: Telegram Group
.. image:: https://img.shields.io/badge/IRC-Channel-blue.svg
:target: https://webchat.freenode.net/?channels=##python-telegram-bot
:alt: IRC Bridge
=================
Table of contents
=================
@@ -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+. 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
@@ -198,13 +198,11 @@ You can get help in several ways:
1. We have a vibrant community of developers helping each other in our `Telegram group <https://telegram.me/pythontelegrambotgroup>`_. Join us!
2. In case you are unable to join our group due to Telegram restrictions, you can use our `IRC channel <https://webchat.freenode.net/?channels=##python-telegram-bot>`_.
2. Report bugs, request new features or ask questions by `creating an issue <https://github.com/python-telegram-bot/python-telegram-bot/issues/new/choose>`_ or `a discussion <https://github.com/python-telegram-bot/python-telegram-bot/discussions/new>`_.
3. Report bugs, request new features or ask questions by `creating an issue <https://github.com/python-telegram-bot/python-telegram-bot/issues/new/choose>`_ or `a discussion <https://github.com/python-telegram-bot/python-telegram-bot/discussions/new>`_.
3. Our `Wiki pages <https://github.com/python-telegram-bot/python-telegram-bot/wiki/>`_ offer a growing amount of resources.
4. Our `Wiki pages <https://github.com/python-telegram-bot/python-telegram-bot/wiki/>`_ offer a growing amount of resources.
5. You can even ask for help on Stack Overflow using the `python-telegram-bot tag <https://stackoverflow.com/questions/tagged/python-telegram-bot>`_.
4. You can even ask for help on Stack Overflow using the `python-telegram-bot tag <https://stackoverflow.com/questions/tagged/python-telegram-bot>`_.
============
Contributing
+2 -2
View File
@@ -60,9 +60,9 @@ author = u'Leandro Toledo'
# built documents.
#
# The short X.Y version.
version = '13.5' # telegram.__version__[:3]
version = '13.8' # telegram.__version__[:3]
# The full version, including alpha/beta/rc tags.
release = '13.5' # 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:
@@ -0,0 +1,8 @@
:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/ext/callbackdatacache.py
telegram.ext.CallbackDataCache
==============================
.. autoclass:: telegram.ext.CallbackDataCache
:members:
:show-inheritance:
@@ -0,0 +1,8 @@
:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/ext/contexttypes.py
telegram.ext.ContextTypes
=========================
.. autoclass:: telegram.ext.ContextTypes
:members:
:show-inheritance:
+9
View File
@@ -0,0 +1,9 @@
:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/ext/extbot.py
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:
@@ -0,0 +1,8 @@
:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/ext/callbackdatacache.py
telegram.ext.InvalidCallbackData
================================
.. autoclass:: telegram.ext.InvalidCallbackData
:members:
:show-inheritance:
+13 -2
View File
@@ -3,15 +3,17 @@ telegram.ext package
.. toctree::
telegram.ext.extbot
telegram.ext.updater
telegram.ext.dispatcher
telegram.ext.dispatcherhandlerstop
telegram.ext.callbackcontext
telegram.ext.defaults
telegram.ext.job
telegram.ext.jobqueue
telegram.ext.messagequeue
telegram.ext.delayqueue
telegram.ext.contexttypes
telegram.ext.defaults
Handlers
--------
@@ -46,9 +48,18 @@ Persistence
telegram.ext.picklepersistence
telegram.ext.dictpersistence
Arbitrary Callback Data
-----------------------
.. toctree::
telegram.ext.callbackdatacache
telegram.ext.invalidcallbackdata
utils
-----
.. toctree::
telegram.ext.utils.promise
telegram.ext.utils.promise
telegram.ext.utils.types
+8
View File
@@ -0,0 +1,8 @@
:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/ext/utils/types.py
telegram.ext.utils.types Module
================================
.. automodule:: telegram.ext.utils.types
: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:
+9 -1
View File
@@ -4,6 +4,8 @@ In this folder are small examples to show what a bot written with `python-telegr
All examples are licensed under the [CC0 License](https://github.com/python-telegram-bot/python-telegram-bot/blob/master/examples/LICENSE.txt) and are therefore fully dedicated to the public domain. You can use them as the base for your own bots without worrying about copyrights.
Do note that we ignore one pythonic convention. Best practice would dictate, in many handler callbacks function signatures, to replace the argument `context` with an underscore, since `context` is an unused local variable in those callbacks. However, since these are examples and not having a name for that argument confuses beginners, we decided to have it present.
### [`echobot.py`](https://github.com/python-telegram-bot/python-telegram-bot/blob/master/examples/echobot.py)
This is probably the base for most of the bots made with `python-telegram-bot`. It simply replies to each text message with a message that contains the same text.
@@ -47,7 +49,13 @@ A basic example of a bot that can accept payments. Don't forget to enable and co
A basic example on how to set up a custom error handler.
### [`chatmemberbot.py`](https://github.com/python-telegram-bot/python-telegram-bot/blob/master/examples/chatmemberbot.py)
A basic example on how `(my_)chat_member` updates can be used.
A basic example on how `(my_)chat_member` updates can be used.
### [`contexttypesbot.py`](https://github.com/python-telegram-bot/python-telegram-bot/blob/master/examples/contexttypesbot.py)
This example showcases how `telegram.ext.ContextTypes` can be used to customize the `context` argument of handler and job callbacks.
### [`arbitrarycallbackdatabot.py`](https://github.com/python-telegram-bot/python-telegram-bot/blob/master/examples/arbitrarycallbackdatabot.py)
This example showcases how PTBs "arbitrary callback data" feature can be used.
## Pure API
The [`rawapibot.py`](https://github.com/python-telegram-bot/python-telegram-bot/blob/master/examples/rawapibot.py) example uses only the pure, "bare-metal" API wrapper.
+110
View File
@@ -0,0 +1,110 @@
#!/usr/bin/env python
# pylint: disable=C0116,W0613
# This program is dedicated to the public domain under the CC0 license.
"""This example showcases how PTBs "arbitrary callback data" feature can be used.
For detailed info on arbitrary callback data, see the wiki page at https://git.io/JGBDI
"""
import logging
from typing import List, Tuple, cast
from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Update
from telegram.ext import (
Updater,
CommandHandler,
CallbackQueryHandler,
CallbackContext,
InvalidCallbackData,
PicklePersistence,
)
logging.basicConfig(
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', level=logging.INFO
)
logger = logging.getLogger(__name__)
def start(update: Update, context: CallbackContext) -> None:
"""Sends a message with 5 inline buttons attached."""
number_list: List[int] = []
update.message.reply_text('Please choose:', reply_markup=build_keyboard(number_list))
def help_command(update: Update, context: CallbackContext) -> None:
"""Displays info on how to use the bot."""
update.message.reply_text(
"Use /start to test this bot. Use /clear to clear the stored data so that you can see "
"what happens, if the button data is not available. "
)
def clear(update: Update, context: CallbackContext) -> None:
"""Clears the callback data cache"""
context.bot.callback_data_cache.clear_callback_data() # type: ignore[attr-defined]
context.bot.callback_data_cache.clear_callback_queries() # type: ignore[attr-defined]
update.effective_message.reply_text('All clear!')
def build_keyboard(current_list: List[int]) -> InlineKeyboardMarkup:
"""Helper function to build the next inline keyboard."""
return InlineKeyboardMarkup.from_column(
[InlineKeyboardButton(str(i), callback_data=(i, current_list)) for i in range(1, 6)]
)
def list_button(update: Update, context: CallbackContext) -> None:
"""Parses the CallbackQuery and updates the message text."""
query = update.callback_query
query.answer()
# Get the data from the callback_data.
# If you're using a type checker like MyPy, you'll have to use typing.cast
# to make the checker get the expected type of the callback_data
number, number_list = cast(Tuple[int, List[int]], query.data)
# append the number to the list
number_list.append(number)
query.edit_message_text(
text=f"So far you've selected {number_list}. Choose the next item:",
reply_markup=build_keyboard(number_list),
)
# we can delete the data stored for the query, because we've replaced the buttons
context.drop_callback_data(query)
def handle_invalid_button(update: Update, context: CallbackContext) -> None:
"""Informs the user that the button is no longer available."""
update.callback_query.answer()
update.effective_message.edit_text(
'Sorry, I could not process this button click 😕 Please send /start to get a new keyboard.'
)
def main() -> None:
"""Run the bot."""
# We use persistence to demonstrate how buttons can still work after the bot was restarted
persistence = PicklePersistence(
filename='arbitrarycallbackdatabot.pickle', store_callback_data=True
)
# Create the Updater and pass it your bot's token.
updater = Updater("TOKEN", persistence=persistence, arbitrary_callback_data=True)
updater.dispatcher.add_handler(CommandHandler('start', start))
updater.dispatcher.add_handler(CommandHandler('help', help_command))
updater.dispatcher.add_handler(CommandHandler('clear', clear))
updater.dispatcher.add_handler(
CallbackQueryHandler(handle_invalid_button, pattern=InvalidCallbackData)
)
updater.dispatcher.add_handler(CallbackQueryHandler(list_button))
# Start the Bot
updater.start_polling()
# Run the bot until the user presses Ctrl-C or the process receives SIGINT,
# SIGTERM or SIGABRT
updater.idle()
if __name__ == '__main__':
main()
+7 -8
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python
# pylint: disable=C0116
# pylint: disable=C0116,W0613
# This program is dedicated to the public domain under the CC0 license.
"""
@@ -35,7 +35,8 @@ def extract_status_change(
) -> Optional[Tuple[bool, bool]]:
"""Takes a ChatMemberUpdated instance and extracts whether the 'old_chat_member' was a member
of the chat and whether the 'new_chat_member' is a member of the chat. Returns None, if
the status didn't change."""
the status didn't change.
"""
status_change = chat_member_update.difference().get("status")
old_is_member, new_is_member = chat_member_update.difference().get("is_member", (None, None))
@@ -113,7 +114,7 @@ def show_chats(update: Update, context: CallbackContext) -> None:
update.effective_message.reply_text(text)
def greet_chat_members(update: Update, _: CallbackContext) -> None:
def greet_chat_members(update: Update, context: CallbackContext) -> None:
"""Greets new users in chats and announces when someone leaves"""
result = extract_status_change(update.chat_member)
if result is None:
@@ -151,11 +152,9 @@ def main() -> None:
dispatcher.add_handler(ChatMemberHandler(greet_chat_members, ChatMemberHandler.CHAT_MEMBER))
# Start the Bot
# We pass 'allowed_updates' to *only* handle updates with '(my_)chat_member' or 'message'
# If you want to handle *all* updates, pass Update.ALL_TYPES
updater.start_polling(
allowed_updates=[Update.MESSAGE, Update.CHAT_MEMBER, Update.MY_CHAT_MEMBER]
)
# We pass 'allowed_updates' handle *all* updates including `chat_member` updates
# To reset this, simply pass `allowed_updates=[]`
updater.start_polling(allowed_updates=Update.ALL_TYPES)
# Run the bot until you press Ctrl-C or the process receives SIGINT,
# SIGTERM or SIGABRT. This should be used most of the time, since
+129
View File
@@ -0,0 +1,129 @@
#!/usr/bin/env python
# pylint: disable=C0116,W0613
# This program is dedicated to the public domain under the CC0 license.
"""
Simple Bot to showcase `telegram.ext.ContextTypes`.
Usage:
Press Ctrl-C on the command line or send a signal to the process to stop the
bot.
"""
from collections import defaultdict
from typing import DefaultDict, Optional, Set
from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup, ParseMode
from telegram.ext import (
Updater,
CommandHandler,
CallbackContext,
ContextTypes,
CallbackQueryHandler,
TypeHandler,
Dispatcher,
)
class ChatData:
"""Custom class for chat_data. Here we store data per message."""
def __init__(self) -> None:
self.clicks_per_message: DefaultDict[int, int] = defaultdict(int)
# The [dict, ChatData, dict] is for type checkers like mypy
class CustomContext(CallbackContext[dict, ChatData, dict]):
"""Custom class for context."""
def __init__(self, dispatcher: Dispatcher):
super().__init__(dispatcher=dispatcher)
self._message_id: Optional[int] = None
@property
def bot_user_ids(self) -> Set[int]:
"""Custom shortcut to access a value stored in the bot_data dict"""
return self.bot_data.setdefault('user_ids', set())
@property
def message_clicks(self) -> Optional[int]:
"""Access the number of clicks for the message this context object was built for."""
if self._message_id:
return self.chat_data.clicks_per_message[self._message_id]
return None
@message_clicks.setter
def message_clicks(self, value: int) -> None:
"""Allow to change the count"""
if not self._message_id:
raise RuntimeError('There is no message associated with this context obejct.')
self.chat_data.clicks_per_message[self._message_id] = value
@classmethod
def from_update(cls, update: object, dispatcher: 'Dispatcher') -> 'CustomContext':
"""Override from_update to set _message_id."""
# Make sure to call super()
context = super().from_update(update, dispatcher)
if context.chat_data and isinstance(update, Update) and update.effective_message:
context._message_id = update.effective_message.message_id # pylint: disable=W0212
# Remember to return the object
return context
def start(update: Update, context: CustomContext) -> None:
"""Display a message with a button."""
update.message.reply_html(
'This button was clicked <i>0</i> times.',
reply_markup=InlineKeyboardMarkup.from_button(
InlineKeyboardButton(text='Click me!', callback_data='button')
),
)
def count_click(update: Update, context: CustomContext) -> None:
"""Update the click count for the message."""
context.message_clicks += 1
update.callback_query.answer()
update.effective_message.edit_text(
f'This button was clicked <i>{context.message_clicks}</i> times.',
reply_markup=InlineKeyboardMarkup.from_button(
InlineKeyboardButton(text='Click me!', callback_data='button')
),
parse_mode=ParseMode.HTML,
)
def print_users(update: Update, context: CustomContext) -> None:
"""Show which users have been using this bot."""
update.message.reply_text(
'The following user IDs have used this bot: '
f'{", ".join(map(str, context.bot_user_ids))}'
)
def track_users(update: Update, context: CustomContext) -> None:
"""Store the user id of the incoming update, if any."""
if update.effective_user:
context.bot_user_ids.add(update.effective_user.id)
def main() -> None:
"""Run the bot."""
context_types = ContextTypes(context=CustomContext, chat_data=ChatData)
updater = Updater("TOKEN", context_types=context_types)
dispatcher = updater.dispatcher
# run track_users in its own group to not interfere with the user handlers
dispatcher.add_handler(TypeHandler(Update, track_users), group=-1)
dispatcher.add_handler(CommandHandler("start", start))
dispatcher.add_handler(CallbackQueryHandler(count_click))
dispatcher.add_handler(CommandHandler("print_users", print_users))
updater.start_polling()
updater.idle()
if __name__ == '__main__':
main()
+21 -10
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python
# pylint: disable=C0116
# pylint: disable=C0116,W0613
# This program is dedicated to the public domain under the CC0 license.
"""
@@ -36,20 +36,24 @@ logger = logging.getLogger(__name__)
GENDER, PHOTO, LOCATION, BIO = range(4)
def start(update: Update, _: CallbackContext) -> int:
def start(update: Update, context: CallbackContext) -> int:
"""Starts the conversation and asks the user about their gender."""
reply_keyboard = [['Boy', 'Girl', 'Other']]
update.message.reply_text(
'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
def gender(update: Update, _: CallbackContext) -> int:
def gender(update: Update, context: CallbackContext) -> int:
"""Stores the selected gender and asks for a photo."""
user = update.message.from_user
logger.info("Gender of %s: %s", user.first_name, update.message.text)
update.message.reply_text(
@@ -61,7 +65,8 @@ def gender(update: Update, _: CallbackContext) -> int:
return PHOTO
def photo(update: Update, _: CallbackContext) -> int:
def photo(update: Update, context: CallbackContext) -> int:
"""Stores the photo and asks for a location."""
user = update.message.from_user
photo_file = update.message.photo[-1].get_file()
photo_file.download('user_photo.jpg')
@@ -73,7 +78,8 @@ def photo(update: Update, _: CallbackContext) -> int:
return LOCATION
def skip_photo(update: Update, _: CallbackContext) -> int:
def skip_photo(update: Update, context: CallbackContext) -> int:
"""Skips the photo and asks for a location."""
user = update.message.from_user
logger.info("User %s did not send a photo.", user.first_name)
update.message.reply_text(
@@ -83,7 +89,8 @@ def skip_photo(update: Update, _: CallbackContext) -> int:
return LOCATION
def location(update: Update, _: CallbackContext) -> int:
def location(update: Update, context: CallbackContext) -> int:
"""Stores the location and asks for some info about the user."""
user = update.message.from_user
user_location = update.message.location
logger.info(
@@ -96,7 +103,8 @@ def location(update: Update, _: CallbackContext) -> int:
return BIO
def skip_location(update: Update, _: CallbackContext) -> int:
def skip_location(update: Update, context: CallbackContext) -> int:
"""Skips the location and asks for info about the user."""
user = update.message.from_user
logger.info("User %s did not send a location.", user.first_name)
update.message.reply_text(
@@ -106,7 +114,8 @@ def skip_location(update: Update, _: CallbackContext) -> int:
return BIO
def bio(update: Update, _: CallbackContext) -> int:
def bio(update: Update, context: CallbackContext) -> int:
"""Stores the info about the user and ends the conversation."""
user = update.message.from_user
logger.info("Bio of %s: %s", user.first_name, update.message.text)
update.message.reply_text('Thank you! I hope we can talk again some day.')
@@ -114,7 +123,8 @@ def bio(update: Update, _: CallbackContext) -> int:
return ConversationHandler.END
def cancel(update: Update, _: CallbackContext) -> int:
def cancel(update: Update, context: CallbackContext) -> int:
"""Cancels and ends the conversation."""
user = update.message.from_user
logger.info("User %s canceled the conversation.", user.first_name)
update.message.reply_text(
@@ -125,6 +135,7 @@ def cancel(update: Update, _: CallbackContext) -> int:
def main() -> None:
"""Run the bot."""
# Create the Updater and pass it your bot's token.
updater = Updater("TOKEN")
+11 -8
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python
# pylint: disable=C0116
# pylint: disable=C0116,W0613
# This program is dedicated to the public domain under the CC0 license.
"""
@@ -45,15 +45,13 @@ markup = ReplyKeyboardMarkup(reply_keyboard, one_time_keyboard=True)
def facts_to_str(user_data: Dict[str, str]) -> str:
facts = list()
for key, value in user_data.items():
facts.append(f'{key} - {value}')
"""Helper function for formatting the gathered user info."""
facts = [f'{key} - {value}' for key, value in user_data.items()]
return "\n".join(facts).join(['\n', '\n'])
def start(update: Update, _: CallbackContext) -> int:
def start(update: Update, context: CallbackContext) -> int:
"""Start the conversation and ask user for input."""
update.message.reply_text(
"Hi! My name is Doctor Botter. I will hold a more complex conversation with you. "
"Why don't you tell me something about yourself?",
@@ -64,6 +62,7 @@ def start(update: Update, _: CallbackContext) -> int:
def regular_choice(update: Update, context: CallbackContext) -> int:
"""Ask the user for info about the selected predefined choice."""
text = update.message.text
context.user_data['choice'] = text
update.message.reply_text(f'Your {text.lower()}? Yes, I would love to hear about that!')
@@ -71,7 +70,8 @@ def regular_choice(update: Update, context: CallbackContext) -> int:
return TYPING_REPLY
def custom_choice(update: Update, _: CallbackContext) -> int:
def custom_choice(update: Update, context: CallbackContext) -> int:
"""Ask the user for a description of a custom category."""
update.message.reply_text(
'Alright, please send me the category first, for example "Most impressive skill"'
)
@@ -80,6 +80,7 @@ def custom_choice(update: Update, _: CallbackContext) -> int:
def received_information(update: Update, context: CallbackContext) -> int:
"""Store info provided by user and ask for the next category."""
user_data = context.user_data
text = update.message.text
category = user_data['choice']
@@ -97,6 +98,7 @@ def received_information(update: Update, context: CallbackContext) -> int:
def done(update: Update, context: CallbackContext) -> int:
"""Display the gathered info and end the conversation."""
user_data = context.user_data
if 'choice' in user_data:
del user_data['choice']
@@ -111,6 +113,7 @@ def done(update: Update, context: CallbackContext) -> int:
def main() -> None:
"""Run the bot."""
# Create the Updater and pass it your bot's token.
updater = Updater("TOKEN")
+3 -3
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python
# pylint: disable=C0116
# pylint: disable=C0116,W0613
# This program is dedicated to the public domain under the CC0 license.
"""Bot that explains Telegram's "Deep Linking Parameters" functionality.
@@ -78,7 +78,7 @@ def deep_linked_level_2(update: Update, context: CallbackContext) -> None:
update.message.reply_text(text, parse_mode=ParseMode.MARKDOWN, disable_web_page_preview=True)
def deep_linked_level_3(update: Update, _: CallbackContext) -> None:
def deep_linked_level_3(update: Update, context: CallbackContext) -> None:
"""Reached through the USING_ENTITIES payload"""
update.message.reply_text(
"It is also possible to make deep-linking using InlineKeyboardButtons.",
@@ -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
+4 -4
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python
# pylint: disable=C0116
# pylint: disable=C0116,W0613
# This program is dedicated to the public domain under the CC0 license.
"""
@@ -30,7 +30,7 @@ logger = logging.getLogger(__name__)
# Define a few command handlers. These usually take the two arguments update and
# context.
def start(update: Update, _: CallbackContext) -> None:
def start(update: Update, context: CallbackContext) -> None:
"""Send a message when the command /start is issued."""
user = update.effective_user
update.message.reply_markdown_v2(
@@ -39,12 +39,12 @@ def start(update: Update, _: CallbackContext) -> None:
)
def help_command(update: Update, _: CallbackContext) -> None:
def help_command(update: Update, context: CallbackContext) -> None:
"""Send a message when the command /help is issued."""
update.message.reply_text('Help!')
def echo(update: Update, _: CallbackContext) -> None:
def echo(update: Update, context: CallbackContext) -> None:
"""Echo the user message."""
update.message.reply_text(update.message.text)
+6 -6
View File
@@ -1,10 +1,8 @@
#!/usr/bin/env python
# pylint: disable=C0116
# pylint: disable=C0116,W0613
# This program is dedicated to the public domain under the CC0 license.
"""
This is a very simple example on how one could implement a custom error handler
"""
"""This is a very simple example on how one could implement a custom error handler."""
import html
import json
import logging
@@ -53,12 +51,13 @@ def error_handler(update: object, context: CallbackContext) -> None:
context.bot.send_message(chat_id=DEVELOPER_CHAT_ID, text=message, parse_mode=ParseMode.HTML)
def bad_command(_: Update, context: CallbackContext) -> None:
def bad_command(update: Update, context: CallbackContext) -> None:
"""Raise an error to trigger the error handler."""
context.bot.wrong_method_name() # type: ignore[attr-defined]
def start(update: Update, _: CallbackContext) -> None:
def start(update: Update, context: CallbackContext) -> None:
"""Displays info on how to trigger an error."""
update.effective_message.reply_html(
'Use /bad_command to cause an error.\n'
f'Your chat id is <code>{update.effective_chat.id}</code>.'
@@ -66,6 +65,7 @@ def start(update: Update, _: CallbackContext) -> None:
def main() -> None:
"""Run the bot."""
# Create the Updater and pass it your bot's token.
updater = Updater(BOT_TOKEN)
+5 -4
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python
# pylint: disable=C0116
# pylint: disable=C0116,W0613
# This program is dedicated to the public domain under the CC0 license.
"""
@@ -29,17 +29,17 @@ logger = logging.getLogger(__name__)
# Define a few command handlers. These usually take the two arguments update and
# context. Error handlers also receive the raised TelegramError object in error.
def start(update: Update, _: CallbackContext) -> None:
def start(update: Update, context: CallbackContext) -> None:
"""Send a message when the command /start is issued."""
update.message.reply_text('Hi!')
def help_command(update: Update, _: CallbackContext) -> None:
def help_command(update: Update, context: CallbackContext) -> None:
"""Send a message when the command /help is issued."""
update.message.reply_text('Help!')
def inlinequery(update: Update, _: CallbackContext) -> None:
def inlinequery(update: Update, context: CallbackContext) -> None:
"""Handle the inline query."""
query = update.inline_query.query
@@ -72,6 +72,7 @@ def inlinequery(update: Update, _: CallbackContext) -> None:
def main() -> None:
"""Run the bot."""
# Create the Updater and pass it your bot's token.
updater = Updater("TOKEN")
+8 -4
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python
# pylint: disable=C0116
# pylint: disable=C0116,W0613
# This program is dedicated to the public domain under the CC0 license.
"""
@@ -17,7 +17,8 @@ logging.basicConfig(
logger = logging.getLogger(__name__)
def start(update: Update, _: CallbackContext) -> None:
def start(update: Update, context: CallbackContext) -> None:
"""Sends a message with three inline buttons attached."""
keyboard = [
[
InlineKeyboardButton("Option 1", callback_data='1'),
@@ -31,7 +32,8 @@ def start(update: Update, _: CallbackContext) -> None:
update.message.reply_text('Please choose:', reply_markup=reply_markup)
def button(update: Update, _: CallbackContext) -> None:
def button(update: Update, context: CallbackContext) -> None:
"""Parses the CallbackQuery and updates the message text."""
query = update.callback_query
# CallbackQueries need to be answered, even if no notification to the user is needed
@@ -41,11 +43,13 @@ def button(update: Update, _: CallbackContext) -> None:
query.edit_message_text(text=f"Selected option: {query.data}")
def help_command(update: Update, _: CallbackContext) -> None:
def help_command(update: Update, context: CallbackContext) -> None:
"""Displays info on how to use the bot."""
update.message.reply_text("Use /start to test this bot.")
def main() -> None:
"""Run the bot."""
# Create the Updater and pass it your bot's token.
updater = Updater("TOKEN")
+11 -9
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python
# pylint: disable=C0116
# pylint: disable=C0116,W0613
# This program is dedicated to the public domain under the CC0 license.
"""Simple inline keyboard bot with multiple CallbackQueryHandlers.
@@ -37,7 +37,7 @@ FIRST, SECOND = range(2)
ONE, TWO, THREE, FOUR = range(4)
def start(update: Update, _: CallbackContext) -> int:
def start(update: Update, context: CallbackContext) -> int:
"""Send message on `/start`."""
# Get user that sent /start and log his name
user = update.message.from_user
@@ -59,7 +59,7 @@ def start(update: Update, _: CallbackContext) -> int:
return FIRST
def start_over(update: Update, _: CallbackContext) -> int:
def start_over(update: Update, context: CallbackContext) -> int:
"""Prompt same text & keyboard as `start` does but not as new message"""
# Get CallbackQuery from Update
query = update.callback_query
@@ -80,7 +80,7 @@ def start_over(update: Update, _: CallbackContext) -> int:
return FIRST
def one(update: Update, _: CallbackContext) -> int:
def one(update: Update, context: CallbackContext) -> int:
"""Show new choice of buttons"""
query = update.callback_query
query.answer()
@@ -97,7 +97,7 @@ def one(update: Update, _: CallbackContext) -> int:
return FIRST
def two(update: Update, _: CallbackContext) -> int:
def two(update: Update, context: CallbackContext) -> int:
"""Show new choice of buttons"""
query = update.callback_query
query.answer()
@@ -114,7 +114,7 @@ def two(update: Update, _: CallbackContext) -> int:
return FIRST
def three(update: Update, _: CallbackContext) -> int:
def three(update: Update, context: CallbackContext) -> int:
"""Show new choice of buttons"""
query = update.callback_query
query.answer()
@@ -132,7 +132,7 @@ def three(update: Update, _: CallbackContext) -> int:
return SECOND
def four(update: Update, _: CallbackContext) -> int:
def four(update: Update, context: CallbackContext) -> int:
"""Show new choice of buttons"""
query = update.callback_query
query.answer()
@@ -149,9 +149,10 @@ def four(update: Update, _: CallbackContext) -> int:
return FIRST
def end(update: Update, _: CallbackContext) -> int:
def end(update: Update, context: CallbackContext) -> int:
"""Returns `ConversationHandler.END`, which tells the
ConversationHandler that the conversation is over"""
ConversationHandler that the conversation is over.
"""
query = update.callback_query
query.answer()
query.edit_message_text(text="See you next time!")
@@ -159,6 +160,7 @@ def end(update: Update, _: CallbackContext) -> int:
def main() -> None:
"""Run the bot."""
# Create the Updater and pass it your bot's token.
updater = Updater("TOKEN")
+6 -5
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python
# pylint: disable=C0116
# pylint: disable=C0116,W0613
# This program is dedicated to the public domain under the CC0 license.
"""
@@ -152,14 +152,14 @@ def show_data(update: Update, context: CallbackContext) -> str:
return SHOWING
def stop(update: Update, _: CallbackContext) -> int:
def stop(update: Update, context: CallbackContext) -> int:
"""End Conversation by command."""
update.message.reply_text('Okay, bye.')
return END
def end(update: Update, _: CallbackContext) -> int:
def end(update: Update, context: CallbackContext) -> int:
"""End conversation from InlineKeyboardButton."""
update.callback_query.answer()
@@ -170,7 +170,7 @@ def end(update: Update, _: CallbackContext) -> int:
# Second level conversation callbacks
def select_level(update: Update, _: CallbackContext) -> str:
def select_level(update: Update, context: CallbackContext) -> str:
"""Choose to add a parent or a child."""
text = 'You may add a parent or a child. Also you can show the gathered data or go back.'
buttons = [
@@ -293,7 +293,7 @@ def end_describing(update: Update, context: CallbackContext) -> int:
return END
def stop_nested(update: Update, _: CallbackContext) -> str:
def stop_nested(update: Update, context: CallbackContext) -> str:
"""Completely end conversation from within nested conversation."""
update.message.reply_text('Okay, bye.')
@@ -301,6 +301,7 @@ def stop_nested(update: Update, _: CallbackContext) -> str:
def main() -> None:
"""Run the bot."""
# Create the Updater and pass it your bot's token.
updater = Updater("TOKEN")
+23 -18
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python
# pylint: disable=C0116
# pylint: disable=C0116,W0613
# This program is dedicated to the public domain under the CC0 license.
"""
@@ -23,7 +23,8 @@ logging.basicConfig(
logger = logging.getLogger(__name__)
def msg(update: Update, _: CallbackContext) -> None:
def msg(update: Update, context: CallbackContext) -> None:
"""Downloads and prints the received passport data."""
# Retrieve passport data
passport_data = update.message.passport_data
# If our nonce doesn't match what we think, this Update did not originate from us
@@ -61,21 +62,24 @@ def msg(update: Update, _: CallbackContext) -> None:
actual_file = file.get_file()
print(actual_file)
actual_file.download()
if data.type in ('passport', 'driver_license', 'identity_card', 'internal_passport'):
if data.front_side:
front_file = data.front_side.get_file()
print(data.type, front_file)
front_file.download()
if data.type in ('driver_license' and 'identity_card'):
if data.reverse_side:
reverse_file = data.reverse_side.get_file()
print(data.type, reverse_file)
reverse_file.download()
if data.type in ('passport', 'driver_license', 'identity_card', 'internal_passport'):
if data.selfie:
selfie_file = data.selfie.get_file()
print(data.type, selfie_file)
selfie_file.download()
if (
data.type in ('passport', 'driver_license', 'identity_card', 'internal_passport')
and data.front_side
):
front_file = data.front_side.get_file()
print(data.type, front_file)
front_file.download()
if data.type in ('driver_license' and 'identity_card') and data.reverse_side:
reverse_file = data.reverse_side.get_file()
print(data.type, reverse_file)
reverse_file.download()
if (
data.type in ('passport', 'driver_license', 'identity_card', 'internal_passport')
and data.selfie
):
selfie_file = data.selfie.get_file()
print(data.type, selfie_file)
selfie_file.download()
if data.type in (
'passport',
'driver_license',
@@ -97,7 +101,8 @@ def msg(update: Update, _: CallbackContext) -> None:
def main() -> None:
"""Start the bot."""
# Create the Updater and pass it your token and private key
updater = Updater("TOKEN", private_key=open('private.key', 'rb').read())
with open('private.key', 'rb') as private_key:
updater = Updater("TOKEN", private_key=private_key.read())
# Get the dispatcher to register handlers
dispatcher = updater.dispatcher
+16 -12
View File
@@ -1,10 +1,8 @@
#!/usr/bin/env python
# pylint: disable=C0116
# pylint: disable=C0116,W0613
# This program is dedicated to the public domain under the CC0 license.
"""
Basic example for a bot that can receive payment from user.
"""
"""Basic example for a bot that can receive payment from user."""
import logging
@@ -27,7 +25,8 @@ logging.basicConfig(
logger = logging.getLogger(__name__)
def start_callback(update: Update, _: CallbackContext) -> None:
def start_callback(update: Update, context: CallbackContext) -> None:
"""Displays info on how to use the bot."""
msg = (
"Use /shipping to get an invoice for shipping-payment, or /noshipping for an "
"invoice without shipping."
@@ -37,6 +36,7 @@ def start_callback(update: Update, _: CallbackContext) -> None:
def start_with_shipping_callback(update: Update, context: CallbackContext) -> None:
"""Sends an invoice with shipping-payment."""
chat_id = update.message.chat_id
title = "Payment Example"
description = "Payment Example using python-telegram-bot"
@@ -70,6 +70,7 @@ def start_with_shipping_callback(update: Update, context: CallbackContext) -> No
def start_without_shipping_callback(update: Update, context: CallbackContext) -> None:
"""Sends an invoice without shipping-payment."""
chat_id = update.message.chat_id
title = "Payment Example"
description = "Payment Example using python-telegram-bot"
@@ -90,7 +91,8 @@ def start_without_shipping_callback(update: Update, context: CallbackContext) ->
)
def shipping_callback(update: Update, _: CallbackContext) -> None:
def shipping_callback(update: Update, context: CallbackContext) -> None:
"""Answers the ShippingQuery with ShippingOptions"""
query = update.shipping_query
# check the payload, is this from your bot?
if query.invoice_payload != 'Custom-Payload':
@@ -98,17 +100,17 @@ def shipping_callback(update: Update, _: CallbackContext) -> None:
query.answer(ok=False, error_message="Something went wrong...")
return
options = list()
# a single LabeledPrice
options.append(ShippingOption('1', 'Shipping Option A', [LabeledPrice('A', 100)]))
# an array of LabeledPrice objects
# First option has a single LabeledPrice
options = [ShippingOption('1', 'Shipping Option A', [LabeledPrice('A', 100)])]
# second option has an array of LabeledPrice objects
price_list = [LabeledPrice('B1', 150), LabeledPrice('B2', 200)]
options.append(ShippingOption('2', 'Shipping Option B', price_list))
query.answer(ok=True, shipping_options=options)
# after (optional) shipping, it's the pre-checkout
def precheckout_callback(update: Update, _: CallbackContext) -> None:
def precheckout_callback(update: Update, context: CallbackContext) -> None:
"""Answers the PreQecheckoutQuery"""
query = update.pre_checkout_query
# check the payload, is this from your bot?
if query.invoice_payload != 'Custom-Payload':
@@ -119,12 +121,14 @@ def precheckout_callback(update: Update, _: CallbackContext) -> None:
# finally, after contacting the payment provider...
def successful_payment_callback(update: Update, _: CallbackContext) -> None:
def successful_payment_callback(update: Update, context: CallbackContext) -> None:
"""Confirms the successful payment."""
# do something after successfully receiving payment?
update.message.reply_text("Thank you for your payment!")
def main() -> None:
"""Run the bot."""
# Create the Updater and pass it your bot's token.
updater = Updater("TOKEN")
+13 -11
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python
# pylint: disable=C0116
# pylint: disable=C0116,W0613
# This program is dedicated to the public domain under the CC0 license.
"""
@@ -28,7 +28,6 @@ from telegram.ext import (
CallbackContext,
)
# Enable logging
logging.basicConfig(
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', level=logging.INFO
@@ -47,15 +46,13 @@ markup = ReplyKeyboardMarkup(reply_keyboard, one_time_keyboard=True)
def facts_to_str(user_data: Dict[str, str]) -> str:
facts = []
for key, value in user_data.items():
facts.append(f'{key} - {value}')
"""Helper function for formatting the gathered user info."""
facts = [f'{key} - {value}' for key, value in user_data.items()]
return "\n".join(facts).join(['\n', '\n'])
def start(update: Update, context: CallbackContext) -> int:
"""Start the conversation, display any stored data and ask user for input."""
reply_text = "Hi! My name is Doctor Botter."
if context.user_data:
reply_text += (
@@ -73,6 +70,7 @@ def start(update: Update, context: CallbackContext) -> int:
def regular_choice(update: Update, context: CallbackContext) -> int:
"""Ask the user for info about the selected predefined choice."""
text = update.message.text.lower()
context.user_data['choice'] = text
if context.user_data.get(text):
@@ -86,7 +84,8 @@ def regular_choice(update: Update, context: CallbackContext) -> int:
return TYPING_REPLY
def custom_choice(update: Update, _: CallbackContext) -> int:
def custom_choice(update: Update, context: CallbackContext) -> int:
"""Ask the user for a description of a custom category."""
update.message.reply_text(
'Alright, please send me the category first, for example "Most impressive skill"'
)
@@ -95,6 +94,7 @@ def custom_choice(update: Update, _: CallbackContext) -> int:
def received_information(update: Update, context: CallbackContext) -> int:
"""Store info provided by user and ask for the next category."""
text = update.message.text
category = context.user_data['choice']
context.user_data[category] = text.lower()
@@ -103,8 +103,7 @@ def received_information(update: Update, context: CallbackContext) -> int:
update.message.reply_text(
"Neat! Just so you know, this is what you already told me:"
f"{facts_to_str(context.user_data)}"
"You can tell me more, or change your opinion on "
"something.",
"You can tell me more, or change your opinion on something.",
reply_markup=markup,
)
@@ -112,23 +111,26 @@ def received_information(update: Update, context: CallbackContext) -> int:
def show_data(update: Update, context: CallbackContext) -> None:
"""Display the gathered info."""
update.message.reply_text(
f"This is what you already told me: {facts_to_str(context.user_data)}"
)
def done(update: Update, context: CallbackContext) -> int:
"""Display the gathered info and end the conversation."""
if 'choice' in context.user_data:
del context.user_data['choice']
update.message.reply_text(
"I learned these facts about you:" f"{facts_to_str(context.user_data)}Until next time!",
f"I learned these facts about you: {facts_to_str(context.user_data)}Until next time!",
reply_markup=ReplyKeyboardRemove(),
)
return ConversationHandler.END
def main() -> None:
"""Run the bot."""
# Create the Updater and pass it your bot's token.
persistence = PicklePersistence(filename='conversationbot')
updater = Updater("TOKEN", persistence=persistence)
+6 -5
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python
# pylint: disable=C0116
# pylint: disable=C0116,W0613
# This program is dedicated to the public domain under the CC0 license.
"""
@@ -34,7 +34,7 @@ logging.basicConfig(
logger = logging.getLogger(__name__)
def start(update: Update, _: CallbackContext) -> None:
def start(update: Update, context: CallbackContext) -> None:
"""Inform user about what this bot can do"""
update.message.reply_text(
'Please select /poll to get a Poll, /quiz to get a Quiz or /preview'
@@ -120,7 +120,7 @@ def receive_quiz_answer(update: Update, context: CallbackContext) -> None:
context.bot.stop_poll(quiz_data["chat_id"], quiz_data["message_id"])
def preview(update: Update, _: CallbackContext) -> None:
def preview(update: Update, context: CallbackContext) -> None:
"""Ask user to create a poll and display a preview of it"""
# using this without a type lets the user chooses what he wants (quiz or poll)
button = [[KeyboardButton("Press me!", request_poll=KeyboardButtonPollType())]]
@@ -131,7 +131,7 @@ def preview(update: Update, _: CallbackContext) -> None:
)
def receive_poll(update: Update, _: CallbackContext) -> None:
def receive_poll(update: Update, context: CallbackContext) -> None:
"""On receiving polls, reply to it by a closed poll copying the received poll"""
actual_poll = update.effective_message.poll
# Only need to set the question and options, since all other parameters don't matter for
@@ -145,12 +145,13 @@ def receive_poll(update: Update, _: CallbackContext) -> None:
)
def help_handler(update: Update, _: CallbackContext) -> None:
def help_handler(update: Update, context: CallbackContext) -> None:
"""Display a help message"""
update.message.reply_text("Use /quiz, /poll or /preview to test this bot.")
def main() -> None:
"""Run bot."""
# Create the Updater and pass it your bot's token.
updater = Updater("TOKEN")
dispatcher = updater.dispatcher
+5 -4
View File
@@ -49,10 +49,11 @@ def echo(bot: telegram.Bot) -> None:
for update in bot.get_updates(offset=UPDATE_ID, timeout=10):
UPDATE_ID = update.update_id + 1
if update.message: # your bot can receive updates without messages
if update.message.text: # not all messages contain text
# Reply to the message
update.message.reply_text(update.message.text)
# your bot can receive updates without messages
# and not all messages contain text
if update.message and update.message.text:
# Reply to the message
update.message.reply_text(update.message.text)
if __name__ == '__main__':
+7 -2
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python
# pylint: disable=C0116
# pylint: disable=C0116,W0613
# This program is dedicated to the public domain under the CC0 license.
"""
@@ -33,7 +33,12 @@ logger = logging.getLogger(__name__)
# Define a few command handlers. These usually take the two arguments update and
# context. Error handlers also receive the raised TelegramError object in error.
def start(update: Update, _: CallbackContext) -> None:
# Best practice would be to replace context with an underscore,
# since context is an unused local variable.
# This being an example and not having context present confusing beginners,
# we decided to have it present as context.
def start(update: Update, context: CallbackContext) -> None:
"""Sends explanation on how to use the bot."""
update.message.reply_text('Hi! Use /set <seconds> to set a timer')
+4 -4
View File
@@ -4,12 +4,12 @@ cryptography!=3.4,!=3.4.1,!=3.4.2,!=3.4.3
pre-commit
# Make sure that the versions specified here match the pre-commit settings!
black==20.8b1
flake8==3.9.1
pylint==2.8.2
flake8==3.9.2
pylint==2.8.3
mypy==0.812
pyupgrade==2.13.0
pyupgrade==2.19.1
pytest==6.2.3
pytest==6.2.4
flaky
beautifulsoup4
+2 -1
View File
@@ -2,6 +2,7 @@
# pre-commit hooks for pylint & mypy
certifi
# only telegram.ext: # Keep this line here; used in setup(-raw).py
tornado>=5.1
tornado>=6.1
APScheduler==3.6.3
pytz>=2018.6
cachetools==4.2.2
+3 -1
View File
@@ -43,6 +43,8 @@ omit =
[coverage:report]
exclude_lines =
pragma: no cover
@overload
if TYPE_CHECKING:
[mypy]
@@ -58,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',
+3 -3
View File
@@ -29,7 +29,7 @@ from .constants import BOT_API_VERSION
def _git_revision() -> Optional[str]:
try:
output = subprocess.check_output(
output = subprocess.check_output( # skipcq: BAN-B607
["git", "describe", "--long", "--tags"], stderr=subprocess.STDOUT
)
except (subprocess.SubprocessError, OSError):
@@ -37,7 +37,7 @@ def _git_revision() -> Optional[str]:
return output.decode().strip()
def print_ver_info() -> None:
def print_ver_info() -> None: # skipcq: PY-D0003
git_revision = _git_revision()
print(f'python-telegram-bot {telegram_ver}' + (f' ({git_revision})' if git_revision else ''))
print(f'Bot API {BOT_API_VERSION}')
@@ -46,7 +46,7 @@ def print_ver_info() -> None:
print(f'Python {sys_version}')
def main() -> None:
def main() -> None: # skipcq: PY-D0003
print_ver_info()
+47 -9
View File
@@ -26,6 +26,7 @@ import warnings
from typing import TYPE_CHECKING, List, Optional, Tuple, Type, TypeVar
from telegram.utils.types import JSONDict
from telegram.utils.deprecate import set_new_attribute_deprecated
if TYPE_CHECKING:
from telegram import Bot
@@ -34,23 +35,41 @@ TO = TypeVar('TO', bound='TelegramObject', covariant=True)
class TelegramObject:
"""Base class for most telegram objects."""
"""Base class for most Telegram objects."""
_id_attrs: Tuple[object, ...] = ()
# Adding slots reduces memory usage & allows for faster attribute access.
# Only instance variables should be added to __slots__.
# We add __dict__ here for backward compatibility & also to avoid repetition for subclasses.
__slots__ = ('__dict__',)
def __str__(self) -> str:
return str(self.to_dict())
def __getitem__(self, item: str) -> object:
return self.__dict__[item]
return getattr(self, item, None)
def __setattr__(self, key: str, value: object) -> None:
set_new_attribute_deprecated(self, key, value)
@staticmethod
def parse_data(data: Optional[JSONDict]) -> Optional[JSONDict]:
def _parse_data(data: Optional[JSONDict]) -> Optional[JSONDict]:
return None if data is None else data.copy()
@classmethod
def de_json(cls: Type[TO], data: Optional[JSONDict], bot: 'Bot') -> Optional[TO]:
data = cls.parse_data(data)
"""Converts JSON data to a Telegram object.
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 data is None:
return None
@@ -61,28 +80,47 @@ class TelegramObject:
@classmethod
def de_list(cls: Type[TO], data: Optional[List[JSONDict]], bot: 'Bot') -> List[Optional[TO]]:
"""Converts JSON data to a list of Telegram objects.
Args:
data (Dict[:obj:`str`, ...]): The JSON data.
bot (:class:`telegram.Bot`): The bot associated with these objects.
Returns:
A list of Telegram objects.
"""
if not data:
return []
return [cls.de_json(d, bot) for d in data]
def to_json(self) -> str:
"""
"""Gives a JSON representation of object.
Returns:
:obj:`str`
"""
return json.dumps(self.to_dict())
def to_dict(self) -> JSONDict:
"""Gives representation of object as :obj:`dict`.
Returns:
:obj:`dict`
"""
data = {}
for key in iter(self.__dict__):
# We want to get all attributes for the class, using self.__slots__ only includes the
# attributes used by that class itself, and not its superclass(es). Hence we get its MRO
# and then get their attributes. The `[:-2]` slice excludes the `object` class & the
# TelegramObject class itself.
attrs = {attr for cls in self.__class__.__mro__[:-2] for attr in cls.__slots__}
for key in attrs:
if key == 'bot' or key.startswith('_'):
continue
value = self.__dict__[key]
value = getattr(self, key, None)
if value is not None:
if hasattr(value, 'to_dict'):
data[key] = value.to_dict()
+429 -71
View File
@@ -21,6 +21,7 @@
import functools
import logging
import warnings
from datetime import datetime
from typing import (
@@ -56,6 +57,7 @@ from telegram import (
Animation,
Audio,
BotCommand,
BotCommandScope,
Chat,
ChatMember,
ChatPermissions,
@@ -89,6 +91,7 @@ from telegram import (
)
from telegram.constants import MAX_INLINE_QUERY_RESULTS
from telegram.error import InvalidToken, TelegramError
from telegram.utils.deprecate import TelegramDeprecationWarning
from telegram.utils.helpers import (
DEFAULT_NONE,
DefaultValue,
@@ -116,7 +119,7 @@ if TYPE_CHECKING:
RT = TypeVar('RT')
def log(
def log( # skipcq: PY-D0003
func: Callable[..., RT], *args: object, **kwargs: object # pylint: disable=W0613
) -> Callable[..., RT]:
logger = logging.getLogger(func.__module__)
@@ -156,8 +159,25 @@ class Bot(TelegramObject):
defaults (:class:`telegram.ext.Defaults`, optional): An object containing default values to
be used if not set explicitly in the bot methods.
.. deprecated:: 13.6
Passing :class:`telegram.ext.Defaults` to :class:`telegram.Bot` is deprecated. If
you want to use :class:`telegram.ext.Defaults`, please use
:class:`telegram.ext.ExtBot` instead.
"""
__slots__ = (
'token',
'base_url',
'base_file_url',
'private_key',
'defaults',
'_bot',
'_commands',
'_request',
'logger',
)
def __init__(
self,
token: str,
@@ -173,6 +193,13 @@ class Bot(TelegramObject):
# Gather default
self.defaults = defaults
if self.defaults:
warnings.warn(
'Passing Defaults to telegram.Bot is deprecated. Use telegram.ext.ExtBot instead.',
TelegramDeprecationWarning,
stacklevel=3,
)
if base_url is None:
base_url = 'https://api.telegram.org/bot'
@@ -184,6 +211,7 @@ class Bot(TelegramObject):
self._bot: Optional[User] = None
self._commands: Optional[List[BotCommand]] = None
self._request = request or Request()
self.private_key = None
self.logger = logging.getLogger(__name__)
if private_key:
@@ -196,6 +224,14 @@ class Bot(TelegramObject):
private_key, password=private_key_password, backend=default_backend()
)
# The ext_bot argument is a little hack to get warnings handled correctly.
# It's not very clean, but the warnings will be dropped at some point anyway.
def __setattr__(self, key: str, value: object, ext_bot: bool = False) -> None:
if issubclass(self.__class__, Bot) and self.__class__ is not Bot and not ext_bot:
object.__setattr__(self, key, value)
return
super().__setattr__(key, value)
def _insert_defaults(
self, data: Dict[str, object], timeout: ODVInput[float]
) -> Optional[float]:
@@ -301,7 +337,7 @@ class Bot(TelegramObject):
return Message.de_json(result, self) # type: ignore[return-value, arg-type]
@property
def request(self) -> Request:
def request(self) -> Request: # skip-cq: PY-D0003
return self._request
@staticmethod
@@ -319,7 +355,6 @@ class Bot(TelegramObject):
@property
def bot(self) -> User:
""":class:`telegram.User`: User instance for the bot as returned by :meth:`get_me`."""
if self._bot is None:
self._bot = self.get_me()
return self._bot
@@ -327,54 +362,58 @@ class Bot(TelegramObject):
@property
def id(self) -> int: # pylint: disable=C0103
""":obj:`int`: Unique identifier for this bot."""
return self.bot.id
@property
def first_name(self) -> str:
""":obj:`str`: Bot's first name."""
return self.bot.first_name
@property
def last_name(self) -> str:
""":obj:`str`: Optional. Bot's last name."""
return self.bot.last_name # type: ignore
@property
def username(self) -> str:
""":obj:`str`: Bot's username."""
return self.bot.username # type: ignore
@property
def link(self) -> str:
""":obj:`str`: Convenience property. Returns the t.me link of the bot."""
return f"https://t.me/{self.username}"
@property
def can_join_groups(self) -> bool:
""":obj:`bool`: Bot's :attr:`telegram.User.can_join_groups` attribute."""
return self.bot.can_join_groups # type: ignore
@property
def can_read_all_group_messages(self) -> bool:
""":obj:`bool`: Bot's :attr:`telegram.User.can_read_all_group_messages` attribute."""
return self.bot.can_read_all_group_messages # type: ignore
@property
def supports_inline_queries(self) -> bool:
""":obj:`bool`: Bot's :attr:`telegram.User.supports_inline_queries` attribute."""
return self.bot.supports_inline_queries # type: ignore
@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()
@@ -383,7 +422,6 @@ class Bot(TelegramObject):
@property
def name(self) -> str:
""":obj:`str`: Bot's @username."""
return f'@{self.username}'
@log
@@ -1986,6 +2024,62 @@ class Bot(TelegramObject):
return result # type: ignore[return-value]
def _effective_inline_results( # pylint: disable=R0201
self,
results: Union[
Sequence['InlineQueryResult'], Callable[[int], Optional[Sequence['InlineQueryResult']]]
],
next_offset: str = None,
current_offset: str = None,
) -> Tuple[Sequence['InlineQueryResult'], Optional[str]]:
"""
Builds the effective results from the results input.
We make this a stand-alone method so tg.ext.ExtBot can wrap it.
Returns:
Tuple of 1. the effective results and 2. correct the next_offset
"""
if current_offset is not None and next_offset is not None:
raise ValueError('`current_offset` and `next_offset` are mutually exclusive!')
if current_offset is not None:
# Convert the string input to integer
if current_offset == '':
current_offset_int = 0
else:
current_offset_int = int(current_offset)
# for now set to empty string, stating that there are no more results
# might change later
next_offset = ''
if callable(results):
callable_output = results(current_offset_int)
if not callable_output:
effective_results: Sequence['InlineQueryResult'] = []
else:
effective_results = callable_output
# the callback *might* return more results on the next call, so we increment
# the page count
next_offset = str(current_offset_int + 1)
else:
if len(results) > (current_offset_int + 1) * MAX_INLINE_QUERY_RESULTS:
# we expect more results for the next page
next_offset_int = current_offset_int + 1
next_offset = str(next_offset_int)
effective_results = results[
current_offset_int
* MAX_INLINE_QUERY_RESULTS : next_offset_int
* MAX_INLINE_QUERY_RESULTS
]
else:
effective_results = results[current_offset_int * MAX_INLINE_QUERY_RESULTS :]
else:
effective_results = results # type: ignore[assignment]
return effective_results, next_offset
@log
def answer_inline_query(
self,
@@ -2090,38 +2184,11 @@ class Bot(TelegramObject):
else:
res.input_message_content.disable_web_page_preview = None
if current_offset is not None and next_offset is not None:
raise ValueError('`current_offset` and `next_offset` are mutually exclusive!')
if current_offset is not None:
if current_offset == '':
current_offset_int = 0
else:
current_offset_int = int(current_offset)
next_offset = ''
if callable(results):
callable_output = results(current_offset_int)
if not callable_output:
effective_results: Sequence['InlineQueryResult'] = []
else:
effective_results = callable_output
next_offset = str(current_offset_int + 1)
else:
if len(results) > (current_offset_int + 1) * MAX_INLINE_QUERY_RESULTS:
next_offset_int = current_offset_int + 1
next_offset = str(next_offset_int)
effective_results = results[
current_offset_int
* MAX_INLINE_QUERY_RESULTS : next_offset_int
* MAX_INLINE_QUERY_RESULTS
]
else:
effective_results = results[current_offset_int * MAX_INLINE_QUERY_RESULTS :]
else:
effective_results = results # type: ignore[assignment]
effective_results, next_offset = self._effective_inline_results(
results=results, next_offset=next_offset, current_offset=current_offset
)
# Apply defaults
for result in effective_results:
_set_defaults(result)
@@ -2259,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``).
@@ -2305,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]
@@ -2606,7 +2705,6 @@ class Bot(TelegramObject):
Raises:
:class:`telegram.error.TelegramError`
"""
if inline_message_id is None and (chat_id is None or message_id is None):
raise ValueError(
'edit_message_media: Both chat_id and message_id are required when '
@@ -2758,18 +2856,22 @@ class Bot(TelegramObject):
# * Long polling poses a different problem: the connection might have been dropped while
# waiting for the server to return and there's no way of knowing the connection had been
# dropped in real time.
result = self._post(
'getUpdates', data, timeout=float(read_latency) + float(timeout), api_kwargs=api_kwargs
result = cast(
List[JSONDict],
self._post(
'getUpdates',
data,
timeout=float(read_latency) + float(timeout),
api_kwargs=api_kwargs,
),
)
if result:
self.logger.debug(
'Getting updates: %s', [u['update_id'] for u in result] # type: ignore
)
self.logger.debug('Getting updates: %s', [u['update_id'] for u in result])
else:
self.logger.debug('No new updates found.')
return [Update.de_json(u, self) for u in result] # type: ignore
return Update.de_list(result, self) # type: ignore[return-value]
@log
def set_webhook(
@@ -3005,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``).
@@ -3026,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]
@@ -3227,6 +3351,11 @@ class Bot(TelegramObject):
Use this method to get data for high score tables. Will return the score of the specified
user and several of their neighbors in a game.
Note:
This method will currently return scores for the target user, plus two of their
closest neighbors on each side. Will also return the top three users if the user and
his neighbors are not among them. Please note that this behavior is subject to change.
Args:
user_id (:obj:`int`): Target user id.
chat_id (:obj:`int` | :obj:`str`, optional): Required if inline_message_id is not
@@ -3425,7 +3554,7 @@ class Bot(TelegramObject):
if is_flexible is not None:
data['is_flexible'] = is_flexible
if send_phone_number_to_provider is not None:
data['send_phone_number_to_provider'] = send_email_to_provider
data['send_phone_number_to_provider'] = send_phone_number_to_provider
if send_email_to_provider is not None:
data['send_email_to_provider'] = send_email_to_provider
@@ -3856,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
@@ -3868,7 +3999,7 @@ class Bot(TelegramObject):
chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username
of the target channel (in the format ``@channelusername``).
expire_date (:obj:`int` | :obj:`datetime.datetime`, optional): Date when the link will
expire.
expire. Integer input will be interpreted as Unix timestamp.
For timezone naive :obj:`datetime.datetime` objects, the default timezone of the
bot will be used.
member_limit (:obj:`int`, optional): Maximum number of users that can be members of
@@ -3878,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`
@@ -3886,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,
}
@@ -3900,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]
@@ -3913,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:
@@ -3935,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`
@@ -3943,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:
@@ -3955,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]
@@ -3997,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,
@@ -4257,7 +4508,6 @@ class Bot(TelegramObject):
:class:`telegram.error.TelegramError`
"""
data: JSONDict = {'chat_id': chat_id}
return self._post( # type: ignore[return-value]
@@ -4899,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
@@ -4910,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(
@@ -4930,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
@@ -4943,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`
@@ -4955,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]
@@ -5071,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
@@ -5089,6 +5436,7 @@ class Bot(TelegramObject):
return MessageId.de_json(result, self) # type: ignore[return-value, arg-type]
def to_dict(self) -> JSONDict:
"""See :meth:`telegram.TelegramObject.to_dict`."""
data: JSONDict = {'id': self.id, 'username': self.username, 'first_name': self.first_name}
if self.last_name:
@@ -5149,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
@@ -5181,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
@@ -5206,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
@@ -5251,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
+2
View File
@@ -41,6 +41,8 @@ class BotCommand(TelegramObject):
"""
__slots__ = ('description', '_id_attrs', 'command')
def __init__(self, command: str, description: str, **_kwargs: Any):
self.command = command
self.description = description
+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)
+39 -22
View File
@@ -53,6 +53,13 @@ class CallbackQuery(TelegramObject):
until you call :attr:`answer`. It is, therefore, necessary to react
by calling :attr:`telegram.Bot.answer_callback_query` even if no notification to the user
is needed (e.g., without specifying any of the optional parameters).
* If you're using :attr:`Bot.arbitrary_callback_data`, :attr:`data` may be an instance
of :class:`telegram.ext.InvalidCallbackData`. This will be the case, if the data
associated with the button triggering the :class:`telegram.CallbackQuery` was already
deleted or if :attr:`data` was manipulated by a malicious client.
.. versionadded:: 13.6
Args:
id (:obj:`str`): Unique identifier for this query.
@@ -77,7 +84,7 @@ class CallbackQuery(TelegramObject):
the message with the callback button was sent.
message (:class:`telegram.Message`): Optional. Message with the callback button that
originated the query.
data (:obj:`str`): Optional. Data associated with the callback button.
data (:obj:`str` | :obj:`object`): Optional. Data associated with the callback button.
inline_message_id (:obj:`str`): Optional. Identifier of the message sent via the bot in
inline mode, that originated the query.
game_short_name (:obj:`str`): Optional. Short name of a Game to be returned.
@@ -85,6 +92,18 @@ class CallbackQuery(TelegramObject):
"""
__slots__ = (
'bot',
'game_short_name',
'message',
'chat_instance',
'id',
'from_user',
'inline_message_id',
'data',
'_id_attrs',
)
def __init__(
self,
id: str, # pylint: disable=W0622
@@ -113,7 +132,8 @@ class CallbackQuery(TelegramObject):
@classmethod
def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['CallbackQuery']:
data = cls.parse_data(data)
"""See :meth:`telegram.TelegramObject.de_json`."""
data = cls._parse_data(data)
if not data:
return None
@@ -173,7 +193,7 @@ class CallbackQuery(TelegramObject):
*args, **kwargs)
For the documentation of the arguments, please see
:meth:`telegram.Bot.edit_message_text`.
:meth:`telegram.Bot.edit_message_text` and :meth:`telegram.Message.edit_text`.
Returns:
:class:`telegram.Message`: On success, if edited message is sent by the bot, the
@@ -223,7 +243,7 @@ class CallbackQuery(TelegramObject):
*args, **kwargs)
For the documentation of the arguments, please see
:meth:`telegram.Bot.edit_message_caption`.
:meth:`telegram.Bot.edit_message_caption` and :meth:`telegram.Message.edit_caption`.
Returns:
:class:`telegram.Message`: On success, if edited message is sent by the bot, the
@@ -275,7 +295,8 @@ class CallbackQuery(TelegramObject):
)
For the documentation of the arguments, please see
:meth:`telegram.Bot.edit_message_reply_markup`.
:meth:`telegram.Bot.edit_message_reply_markup` and
:meth:`telegram.Message.edit_reply_markup`.
Returns:
:class:`telegram.Message`: On success, if edited message is sent by the bot, the
@@ -314,7 +335,7 @@ class CallbackQuery(TelegramObject):
*args, **kwargs)
For the documentation of the arguments, please see
:meth:`telegram.Bot.edit_message_media`.
:meth:`telegram.Bot.edit_message_media` and :meth:`telegram.Message.edit_media`.
Returns:
:class:`telegram.Message`: On success, if edited message is sent by the bot, the
@@ -362,7 +383,8 @@ class CallbackQuery(TelegramObject):
)
For the documentation of the arguments, please see
:meth:`telegram.Bot.edit_message_live_location`.
:meth:`telegram.Bot.edit_message_live_location` and
:meth:`telegram.Message.edit_live_location`.
Returns:
:class:`telegram.Message`: On success, if edited message is sent by the bot, the
@@ -414,7 +436,8 @@ class CallbackQuery(TelegramObject):
)
For the documentation of the arguments, please see
:meth:`telegram.Bot.stop_message_live_location`.
:meth:`telegram.Bot.stop_message_live_location` and
:meth:`telegram.Message.stop_live_location`.
Returns:
:class:`telegram.Message`: On success, if edited message is sent by the bot, the
@@ -455,7 +478,7 @@ class CallbackQuery(TelegramObject):
*args, **kwargs)
For the documentation of the arguments, please see
:meth:`telegram.Bot.set_game_score`.
:meth:`telegram.Bot.set_game_score` and :meth:`telegram.Message.set_game_score`.
Returns:
:class:`telegram.Message`: On success, if edited message is sent by the bot, the
@@ -499,7 +522,7 @@ class CallbackQuery(TelegramObject):
*args, **kwargs)
For the documentation of the arguments, please see
:meth:`telegram.Bot.get_game_high_scores`.
:meth:`telegram.Bot.get_game_high_scores` and :meth:`telegram.Message.get_game_high_score`.
Returns:
List[:class:`telegram.GameHighScore`]
@@ -530,7 +553,7 @@ class CallbackQuery(TelegramObject):
update.callback_query.message.delete(*args, **kwargs)
For the documentation of the arguments, please see
:meth:`telegram.Bot.delete_message`.
:meth:`telegram.Message.delete`.
Returns:
:obj:`bool`: On success, :obj:`True` is returned.
@@ -549,13 +572,10 @@ class CallbackQuery(TelegramObject):
) -> bool:
"""Shortcut for::
bot.pin_chat_message(chat_id=message.chat_id,
message_id=message.message_id,
*args,
**kwargs)
update.callback_query.message.pin(*args, **kwargs)
For the documentation of the arguments, please see
:meth:`telegram.Bot.pin_chat_message`.
:meth:`telegram.Message.pin`.
Returns:
:obj:`bool`: On success, :obj:`True` is returned.
@@ -574,13 +594,10 @@ class CallbackQuery(TelegramObject):
) -> bool:
"""Shortcut for::
bot.unpin_chat_message(chat_id=message.chat_id,
message_id=message.message_id,
*args,
**kwargs)
update.callback_query.message.unpin(*args, **kwargs)
For the documentation of the arguments, please see
:meth:`telegram.Bot.unpin_chat_message`.
:meth:`telegram.Message.unpin`.
Returns:
:obj:`bool`: On success, :obj:`True` is returned.
@@ -614,7 +631,7 @@ class CallbackQuery(TelegramObject):
**kwargs)
For the documentation of the arguments, please see
:meth:`telegram.Bot.copy_message`.
:meth:`telegram.Message.copy`.
Returns:
:class:`telegram.MessageId`: On success, returns the MessageId of the sent message.
+151 -23
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
@@ -143,6 +145,30 @@ class Chat(TelegramObject):
"""
__slots__ = (
'bio',
'id',
'type',
'last_name',
'bot',
'sticker_set_name',
'slow_mode_delay',
'location',
'first_name',
'permissions',
'invite_link',
'pinned_message',
'description',
'can_set_sticker_set',
'username',
'title',
'photo',
'linked_chat_id',
'all_members_are_administrators',
'message_auto_delete_time',
'_id_attrs',
)
SENDER: ClassVar[str] = constants.CHAT_SENDER
""":const:`telegram.constants.CHAT_SENDER`
@@ -229,14 +255,16 @@ class Chat(TelegramObject):
@property
def link(self) -> Optional[str]:
""":obj:`str`: Convenience property. If the chat has a :attr:`username`, returns a t.me
link of the chat."""
link of the chat.
"""
if self.username:
return f"https://t.me/{self.username}"
return None
@classmethod
def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['Chat']:
data = cls.parse_data(data)
"""See :meth:`telegram.TelegramObject.de_json`."""
data = cls._parse_data(data)
if not data:
return None
@@ -258,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(
@@ -292,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,
@@ -340,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,
@@ -380,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(
@@ -418,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(
@@ -457,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(
@@ -483,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(
@@ -508,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(
@@ -652,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(
@@ -1457,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::
@@ -1467,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`
@@ -1477,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(
@@ -1486,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::
@@ -1496,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`
@@ -1507,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(
@@ -1531,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
)
+11 -4
View File
@@ -20,19 +20,20 @@
"""This module contains an object that represents a Telegram ChatAction."""
from typing import ClassVar
from telegram import constants
from telegram.utils.deprecate import set_new_attribute_deprecated
class ChatAction:
"""Helper class to provide constants for different chat actions."""
__slots__ = ('__dict__',) # Adding __dict__ here since it doesn't subclass TGObject
FIND_LOCATION: ClassVar[str] = constants.CHATACTION_FIND_LOCATION
""":const:`telegram.constants.CHATACTION_FIND_LOCATION`"""
RECORD_AUDIO: ClassVar[str] = constants.CHATACTION_RECORD_AUDIO
""":const:`telegram.constants.CHATACTION_RECORD_AUDIO`
.. deprecated:: 13.5
Deprecated by Telegram. Use :attr:`RECORD_VOICE` instead, as backwards
compatibility is not guaranteed by Telegram.
Deprecated by Telegram. Use :attr:`RECORD_VOICE` instead.
"""
RECORD_VOICE: ClassVar[str] = constants.CHATACTION_RECORD_VOICE
""":const:`telegram.constants.CHATACTION_RECORD_VOICE`
@@ -49,8 +50,7 @@ class ChatAction:
""":const:`telegram.constants.CHATACTION_UPLOAD_AUDIO`
.. deprecated:: 13.5
Deprecated by Telegram. Use :attr:`UPLOAD_VOICE` instead, as backwards
compatibility is not guaranteed by Telegram.
Deprecated by Telegram. Use :attr:`UPLOAD_VOICE` instead.
"""
UPLOAD_VOICE: ClassVar[str] = constants.CHATACTION_UPLOAD_VOICE
""":const:`telegram.constants.CHATACTION_UPLOAD_VOICE`
@@ -59,9 +59,16 @@ 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
""":const:`telegram.constants.CHATACTION_UPLOAD_VIDEO`"""
UPLOAD_VIDEO_NOTE: ClassVar[str] = constants.CHATACTION_UPLOAD_VIDEO_NOTE
""":const:`telegram.constants.CHATACTION_UPLOAD_VIDEO_NOTE`"""
def __setattr__(self, key: str, value: object) -> None:
set_new_attribute_deprecated(self, key, value)
+46 -2
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,9 +68,33 @@ 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
"""
__slots__ = (
'invite_link',
'creator',
'is_primary',
'is_revoked',
'expire_date',
'member_limit',
'name',
'creates_join_request',
'pending_join_request_count',
'_id_attrs',
)
def __init__(
self,
invite_link: str,
@@ -68,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
@@ -79,12 +117,17 @@ 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
def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['ChatInviteLink']:
data = cls.parse_data(data)
"""See :meth:`telegram.TelegramObject.de_json`."""
data = cls._parse_data(data)
if not data:
return None
@@ -95,6 +138,7 @@ class ChatInviteLink(TelegramObject):
return cls(**data)
def to_dict(self) -> JSONDict:
"""See :meth:`telegram.TelegramObject.to_dict`."""
data = super().to_dict()
data['expire_date'] = to_timestamp(self.expire_date)
+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
)
+4 -1
View File
@@ -47,6 +47,8 @@ class ChatLocation(TelegramObject):
"""
__slots__ = ('location', '_id_attrs', 'address')
def __init__(
self,
location: Location,
@@ -60,7 +62,8 @@ class ChatLocation(TelegramObject):
@classmethod
def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['ChatLocation']:
data = cls.parse_data(data)
"""See :meth:`telegram.TelegramObject.de_json`."""
data = cls._parse_data(data)
if not data:
return None
+495 -5
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,115 +29,267 @@ 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__ = (
'is_member',
'can_restrict_members',
'can_delete_messages',
'custom_title',
'can_be_edited',
'can_post_messages',
'can_send_messages',
'can_edit_messages',
'can_send_media_messages',
'is_anonymous',
'can_add_web_page_previews',
'can_send_other_messages',
'can_invite_users',
'can_send_polls',
'user',
'can_promote_members',
'status',
'can_change_info',
'can_pin_messages',
'can_manage_chat',
'can_manage_voice_chats',
'until_date',
'_id_attrs',
)
ADMINISTRATOR: ClassVar[str] = constants.CHATMEMBER_ADMINISTRATOR
""":const:`telegram.constants.CHATMEMBER_ADMINISTRATOR`"""
CREATOR: ClassVar[str] = constants.CHATMEMBER_CREATOR
@@ -207,7 +359,8 @@ class ChatMember(TelegramObject):
@classmethod
def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['ChatMember']:
data = cls.parse_data(data)
"""See :meth:`telegram.TelegramObject.de_json`."""
data = cls._parse_data(data)
if not data:
return None
@@ -215,11 +368,348 @@ 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:
"""See :meth:`telegram.TelegramObject.to_dict`."""
data = super().to_dict()
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,
)
+13 -1
View File
@@ -62,6 +62,16 @@ class ChatMemberUpdated(TelegramObject):
"""
__slots__ = (
'chat',
'from_user',
'date',
'old_chat_member',
'new_chat_member',
'invite_link',
'_id_attrs',
)
def __init__(
self,
chat: Chat,
@@ -92,7 +102,8 @@ class ChatMemberUpdated(TelegramObject):
@classmethod
def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['ChatMemberUpdated']:
data = cls.parse_data(data)
"""See :meth:`telegram.TelegramObject.de_json`."""
data = cls._parse_data(data)
if not data:
return None
@@ -107,6 +118,7 @@ class ChatMemberUpdated(TelegramObject):
return cls(**data)
def to_dict(self) -> JSONDict:
"""See :meth:`telegram.TelegramObject.to_dict`."""
data = super().to_dict()
# Required
+12
View File
@@ -78,6 +78,18 @@ class ChatPermissions(TelegramObject):
"""
__slots__ = (
'can_send_other_messages',
'can_invite_users',
'can_send_polls',
'_id_attrs',
'can_send_messages',
'can_send_media_messages',
'can_change_info',
'can_pin_messages',
'can_add_web_page_previews',
)
def __init__(
self,
can_send_messages: bool = None,
+4 -1
View File
@@ -61,6 +61,8 @@ class ChosenInlineResult(TelegramObject):
"""
__slots__ = ('location', 'result_id', 'from_user', 'inline_message_id', '_id_attrs', 'query')
def __init__(
self,
result_id: str,
@@ -82,7 +84,8 @@ class ChosenInlineResult(TelegramObject):
@classmethod
def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['ChosenInlineResult']:
data = cls.parse_data(data)
"""See :meth:`telegram.TelegramObject.de_json`."""
data = cls._parse_data(data)
if not data:
return None
+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
@@ -64,13 +64,15 @@ class Dice(TelegramObject):
"""
__slots__ = ('emoji', 'value', '_id_attrs')
def __init__(self, value: int, emoji: str, **_kwargs: Any):
self.value = value
self.emoji = emoji
self._id_attrs = (self.value, self.emoji)
DICE: ClassVar[str] = constants.DICE_DICE
DICE: ClassVar[str] = constants.DICE_DICE # skipcq: PTC-W0052
""":const:`telegram.constants.DICE_DICE`"""
DARTS: ClassVar[str] = constants.DICE_DARTS
""":const:`telegram.constants.DICE_DARTS`"""
+32 -9
View File
@@ -39,6 +39,11 @@ def _lstrip_str(in_s: str, lstr: str) -> str:
class TelegramError(Exception):
"""Base class for Telegram errors."""
# Apparently the base class Exception already has __dict__ in it, so its not included here
__slots__ = ('message',)
def __init__(self, message: str):
super().__init__()
@@ -58,10 +63,16 @@ class TelegramError(Exception):
class Unauthorized(TelegramError):
pass
"""Raised when the bot has not enough rights to perform the requested action."""
__slots__ = ()
class InvalidToken(TelegramError):
"""Raised when the token is invalid."""
__slots__ = ()
def __init__(self) -> None:
super().__init__('Invalid token')
@@ -70,14 +81,22 @@ class InvalidToken(TelegramError):
class NetworkError(TelegramError):
pass
"""Base class for exceptions due to networking errors."""
__slots__ = ()
class BadRequest(NetworkError):
pass
"""Raised when Telegram could not process the request correctly."""
__slots__ = ()
class TimedOut(NetworkError):
"""Raised when a request took too long to finish."""
__slots__ = ()
def __init__(self) -> None:
super().__init__('Timed out')
@@ -87,11 +106,15 @@ class TimedOut(NetworkError):
class ChatMigrated(TelegramError):
"""
Raised when the requested group chat migrated to supergroup and has a new chat id.
Args:
new_chat_id (:obj:`int`): The new chat id of the group.
"""
__slots__ = ('new_chat_id',)
def __init__(self, new_chat_id: int):
super().__init__(f'Group migrated to supergroup. New chat id: {new_chat_id}')
self.new_chat_id = new_chat_id
@@ -102,11 +125,15 @@ class ChatMigrated(TelegramError):
class RetryAfter(TelegramError):
"""
Raised when flood limits where exceeded.
Args:
retry_after (:obj:`int`): Time in seconds, after which the bot can retry the request.
"""
__slots__ = ('retry_after',)
def __init__(self, retry_after: int):
super().__init__(f'Flood control exceeded. Retry in {float(retry_after)} seconds')
self.retry_after = float(retry_after)
@@ -116,13 +143,9 @@ class RetryAfter(TelegramError):
class Conflict(TelegramError):
"""
Raised when a long poll or webhook conflicts with another one.
"""Raised when a long poll or webhook conflicts with another one."""
Args:
msg (:obj:`str`): The message from telegrams server.
"""
__slots__ = ()
def __reduce__(self) -> Tuple[type, Tuple[str]]:
return self.__class__, (self.message,)
+45 -23
View File
@@ -16,14 +16,29 @@
#
# 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=C0413
"""Extensions over the Telegram Bot API to facilitate bot making"""
from .extbot import ExtBot
from .basepersistence import BasePersistence
from .picklepersistence import PicklePersistence
from .dictpersistence import DictPersistence
from .handler import Handler
from .callbackcontext import CallbackContext
from .contexttypes import ContextTypes
from .dispatcher import Dispatcher, DispatcherHandlerStop, run_async
# https://bugs.python.org/issue41451, fixed on 3.7+, doesn't actually remove slots
# 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__
except AttributeError as exc:
if str(exc) == '__slots__':
pass
else:
raise exc
from .jobqueue import JobQueue, Job
from .updater import Updater
from .callbackqueryhandler import CallbackQueryHandler
@@ -44,41 +59,48 @@ 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
__all__ = (
'Dispatcher',
'JobQueue',
'Job',
'Updater',
'BaseFilter',
'BasePersistence',
'CallbackContext',
'CallbackDataCache',
'CallbackQueryHandler',
'ChatJoinRequestHandler',
'ChatMemberHandler',
'ChosenInlineResultHandler',
'CommandHandler',
'ContextTypes',
'ConversationHandler',
'Defaults',
'DelayQueue',
'DictPersistence',
'Dispatcher',
'DispatcherHandlerStop',
'ExtBot',
'Filters',
'Handler',
'InlineQueryHandler',
'MessageHandler',
'BaseFilter',
'InvalidCallbackData',
'Job',
'JobQueue',
'MessageFilter',
'UpdateFilter',
'Filters',
'MessageHandler',
'MessageQueue',
'PicklePersistence',
'PollAnswerHandler',
'PollHandler',
'PreCheckoutQueryHandler',
'PrefixHandler',
'RegexHandler',
'ShippingQueryHandler',
'StringCommandHandler',
'StringRegexHandler',
'TypeHandler',
'ConversationHandler',
'PreCheckoutQueryHandler',
'ShippingQueryHandler',
'MessageQueue',
'DelayQueue',
'DispatcherHandlerStop',
'UpdateFilter',
'Updater',
'run_async',
'CallbackContext',
'BasePersistence',
'PicklePersistence',
'DictPersistence',
'PrefixHandler',
'PollAnswerHandler',
'PollHandler',
'ChatMemberHandler',
'Defaults',
)
+246 -64
View File
@@ -18,16 +18,20 @@
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains the BasePersistence class."""
import warnings
from sys import version_info as py_ver
from abc import ABC, abstractmethod
from copy import copy
from typing import DefaultDict, Dict, Optional, Tuple, cast, ClassVar
from typing import Dict, Optional, Tuple, cast, ClassVar, Generic, DefaultDict
from telegram.utils.deprecate import set_new_attribute_deprecated
from telegram import Bot
import telegram.ext.extbot
from telegram.utils.types import ConversationDict
from telegram.ext.utils.types import UD, CD, BD, ConversationDict, CDCData
class BasePersistence(ABC):
class BasePersistence(Generic[UD, CD, BD], ABC):
"""Interface class for adding persistence to your bot.
Subclass this object for different implementations of a persistent bot.
@@ -35,16 +39,22 @@ class BasePersistence(ABC):
* :meth:`get_bot_data`
* :meth:`update_bot_data`
* :meth:`refresh_bot_data`
* :meth:`get_chat_data`
* :meth:`update_chat_data`
* :meth:`refresh_chat_data`
* :meth:`get_user_data`
* :meth:`update_user_data`
* :meth:`refresh_user_data`
* :meth:`get_callback_data`
* :meth:`update_callback_data`
* :meth:`get_conversations`
* :meth:`update_conversation`
* :meth:`flush`
If you don't actually need one of those methods, a simple ``pass`` is enough. For example, if
``store_bot_data=False``, you don't need :meth:`get_bot_data` and :meth:`update_bot_data`.
``store_bot_data=False``, you don't need :meth:`get_bot_data`, :meth:`update_bot_data` or
:meth:`refresh_bot_data`.
Warning:
Persistence will try to replace :class:`telegram.Bot` instances by :attr:`REPLACED_BOT` and
@@ -64,7 +74,11 @@ class BasePersistence(ABC):
store_chat_data (:obj:`bool`, optional): Whether chat_data should be saved by this
persistence class. Default is :obj:`True` .
store_bot_data (:obj:`bool`, optional): Whether bot_data should be saved by this
persistence class. Default is :obj:`True` .
persistence class. Default is :obj:`True`.
store_callback_data (:obj:`bool`, optional): Whether callback_data should be saved by this
persistence class. Default is :obj:`False`.
.. versionadded:: 13.6
Attributes:
store_user_data (:obj:`bool`): Optional, Whether user_data should be saved by this
@@ -73,43 +87,88 @@ class BasePersistence(ABC):
persistence class.
store_bot_data (:obj:`bool`): Optional. Whether bot_data should be saved by this
persistence class.
store_callback_data (:obj:`bool`): Optional. Whether callback_data should be saved by this
persistence class.
.. versionadded:: 13.6
"""
# Apparently Py 3.7 and below have '__dict__' in ABC
if py_ver < (3, 7):
__slots__ = (
'store_user_data',
'store_chat_data',
'store_bot_data',
'store_callback_data',
'bot',
)
else:
__slots__ = (
'store_user_data', # type: ignore[assignment]
'store_chat_data',
'store_bot_data',
'store_callback_data',
'bot',
'__dict__',
)
def __new__(
cls, *args: object, **kwargs: object # pylint: disable=W0613
) -> 'BasePersistence':
"""This overrides the get_* and update_* methods to use insert/replace_bot.
That has the side effect that we always pass deepcopied data to those methods, so in
Pickle/DictPersistence we don't have to worry about copying the data again.
Note: This doesn't hold for second tuple-entry of callback_data. That's a Dict[str, str],
so no bots to replace anyway.
"""
instance = super().__new__(cls)
get_user_data = instance.get_user_data
get_chat_data = instance.get_chat_data
get_bot_data = instance.get_bot_data
get_callback_data = instance.get_callback_data
update_user_data = instance.update_user_data
update_chat_data = instance.update_chat_data
update_bot_data = instance.update_bot_data
update_callback_data = instance.update_callback_data
def get_user_data_insert_bot() -> DefaultDict[int, Dict[object, object]]:
def get_user_data_insert_bot() -> DefaultDict[int, UD]:
return instance.insert_bot(get_user_data())
def get_chat_data_insert_bot() -> DefaultDict[int, Dict[object, object]]:
def get_chat_data_insert_bot() -> DefaultDict[int, CD]:
return instance.insert_bot(get_chat_data())
def get_bot_data_insert_bot() -> Dict[object, object]:
def get_bot_data_insert_bot() -> BD:
return instance.insert_bot(get_bot_data())
def update_user_data_replace_bot(user_id: int, data: Dict) -> None:
def get_callback_data_insert_bot() -> Optional[CDCData]:
cdc_data = get_callback_data()
if cdc_data is None:
return None
return instance.insert_bot(cdc_data[0]), cdc_data[1]
def update_user_data_replace_bot(user_id: int, data: UD) -> None:
return update_user_data(user_id, instance.replace_bot(data))
def update_chat_data_replace_bot(chat_id: int, data: Dict) -> None:
def update_chat_data_replace_bot(chat_id: int, data: CD) -> None:
return update_chat_data(chat_id, instance.replace_bot(data))
def update_bot_data_replace_bot(data: Dict) -> None:
def update_bot_data_replace_bot(data: BD) -> None:
return update_bot_data(instance.replace_bot(data))
instance.get_user_data = get_user_data_insert_bot
instance.get_chat_data = get_chat_data_insert_bot
instance.get_bot_data = get_bot_data_insert_bot
instance.update_user_data = update_user_data_replace_bot
instance.update_chat_data = update_chat_data_replace_bot
instance.update_bot_data = update_bot_data_replace_bot
def update_callback_data_replace_bot(data: CDCData) -> None:
obj_data, queue = data
return update_callback_data((instance.replace_bot(obj_data), queue))
# We want to ignore TGDeprecation warnings so we use obj.__setattr__. Adds to __dict__
object.__setattr__(instance, 'get_user_data', get_user_data_insert_bot)
object.__setattr__(instance, 'get_chat_data', get_chat_data_insert_bot)
object.__setattr__(instance, 'get_bot_data', get_bot_data_insert_bot)
object.__setattr__(instance, 'get_callback_data', get_callback_data_insert_bot)
object.__setattr__(instance, 'update_user_data', update_user_data_replace_bot)
object.__setattr__(instance, 'update_chat_data', update_chat_data_replace_bot)
object.__setattr__(instance, 'update_bot_data', update_bot_data_replace_bot)
object.__setattr__(instance, 'update_callback_data', update_callback_data_replace_bot)
return instance
def __init__(
@@ -117,18 +176,33 @@ class BasePersistence(ABC):
store_user_data: bool = True,
store_chat_data: bool = True,
store_bot_data: bool = True,
store_callback_data: bool = False,
):
self.store_user_data = store_user_data
self.store_chat_data = store_chat_data
self.store_bot_data = store_bot_data
self.store_callback_data = store_callback_data
self.bot: Bot = None # type: ignore[assignment]
def __setattr__(self, key: str, value: object) -> None:
# Allow user defined subclasses to have custom attributes.
if issubclass(self.__class__, BasePersistence) and self.__class__.__name__ not in {
'DictPersistence',
'PicklePersistence',
}:
object.__setattr__(self, key, value)
return
set_new_attribute_deprecated(self, key, value)
def set_bot(self, bot: Bot) -> None:
"""Set the Bot to be used by this persistence instance.
Args:
bot (:class:`telegram.Bot`): The bot.
"""
if self.store_callback_data and not isinstance(bot, telegram.ext.extbot.ExtBot):
raise TypeError('store_callback_data can only be used with telegram.ext.ExtBot.')
self.bot = bot
@classmethod
@@ -137,7 +211,9 @@ class BasePersistence(ABC):
Replaces all instances of :class:`telegram.Bot` that occur within the passed object with
:attr:`REPLACED_BOT`. Currently, this handles objects of type ``list``, ``tuple``, ``set``,
``frozenset``, ``dict``, ``defaultdict`` and objects that have a ``__dict__`` or
``__slot__`` attribute, excluding objects that can't be copied with `copy.copy`.
``__slots__`` attribute, excluding classes and objects that can't be copied with
``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
@@ -168,6 +244,14 @@ class BasePersistence(ABC):
new_immutable = obj.__class__(cls._replace_bot(item, memo) for item in obj)
memo[obj_id] = new_immutable
return new_immutable
if isinstance(obj, type):
# classes usually do have a __dict__, but it's not writable
warnings.warn(
'BasePersistence.replace_bot does not handle classes. See '
'the docs of BasePersistence.replace_bot for more information.',
RuntimeWarning,
)
return obj
try:
new_obj = copy(obj)
@@ -193,21 +277,34 @@ class BasePersistence(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:
@@ -215,7 +312,9 @@ class BasePersistence(ABC):
Replaces all instances of :attr:`REPLACED_BOT` that occur within the passed object with
:attr:`bot`. Currently, this handles objects of type ``list``, ``tuple``, ``set``,
``frozenset``, ``dict``, ``defaultdict`` and objects that have a ``__dict__`` or
``__slot__`` attribute, excluding objects that can't be copied with `copy.copy`.
``__slots__`` attribute, excluding classes and objects that can't be copied with
``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
@@ -248,6 +347,14 @@ class BasePersistence(ABC):
new_immutable = obj.__class__(self._insert_bot(item, memo) for item in obj)
memo[obj_id] = new_immutable
return new_immutable
if isinstance(obj, type):
# classes usually do have a __dict__, but it's not writable
warnings.warn(
'BasePersistence.insert_bot does not handle classes. See '
'the docs of BasePersistence.insert_bot for more information.',
RuntimeWarning,
)
return obj
try:
new_obj = copy(obj)
@@ -272,53 +379,78 @@ class BasePersistence(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
def get_user_data(self) -> DefaultDict[int, Dict[object, object]]:
def get_user_data(self) -> DefaultDict[int, UD]:
"""Will be called by :class:`telegram.ext.Dispatcher` upon creation with a
persistence object. It should return the ``user_data`` if stored, or an empty
``defaultdict(dict)``.
:obj:`defaultdict(telegram.ext.utils.types.UD)` with integer keys.
Returns:
:obj:`defaultdict`: The restored user data.
DefaultDict[:obj:`int`, :class:`telegram.ext.utils.types.UD`]: The restored user data.
"""
@abstractmethod
def get_chat_data(self) -> DefaultDict[int, Dict[object, object]]:
def get_chat_data(self) -> DefaultDict[int, CD]:
"""Will be called by :class:`telegram.ext.Dispatcher` upon creation with a
persistence object. It should return the ``chat_data`` if stored, or an empty
``defaultdict(dict)``.
:obj:`defaultdict(telegram.ext.utils.types.CD)` with integer keys.
Returns:
:obj:`defaultdict`: The restored chat data.
DefaultDict[:obj:`int`, :class:`telegram.ext.utils.types.CD`]: The restored chat data.
"""
@abstractmethod
def get_bot_data(self) -> Dict[object, object]:
def get_bot_data(self) -> BD:
"""Will be called by :class:`telegram.ext.Dispatcher` upon creation with a
persistence object. It should return the ``bot_data`` if stored, or an empty
:obj:`dict`.
:class:`telegram.ext.utils.types.BD`.
Returns:
:obj:`dict`: The restored bot data.
:class:`telegram.ext.utils.types.BD`: The restored bot data.
"""
def get_callback_data(self) -> Optional[CDCData]:
"""Will be called by :class:`telegram.ext.Dispatcher` upon creation with a
persistence object. If callback data was stored, it should be returned.
.. versionadded:: 13.6
Returns:
Optional[:class:`telegram.ext.utils.types.CDCData`]: The restored meta data or
:obj:`None`, if no data was stored.
"""
raise NotImplementedError
@abstractmethod
def get_conversations(self, name: str) -> ConversationDict:
"""Will be called by :class:`telegram.ext.Dispatcher` when a
@@ -337,8 +469,8 @@ class BasePersistence(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.
@@ -347,34 +479,84 @@ class BasePersistence(ABC):
"""
@abstractmethod
def update_user_data(self, user_id: int, data: Dict) -> None:
def update_user_data(self, user_id: int, data: UD) -> None:
"""Will be called by the :class:`telegram.ext.Dispatcher` after a handler has
handled an update.
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 (:class:`telegram.ext.utils.types.UD`): The
:attr:`telegram.ext.Dispatcher.user_data` ``[user_id]``.
"""
@abstractmethod
def update_chat_data(self, chat_id: int, data: Dict) -> None:
def update_chat_data(self, chat_id: int, data: CD) -> None:
"""Will be called by the :class:`telegram.ext.Dispatcher` after a handler has
handled an update.
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 (:class:`telegram.ext.utils.types.CD`): The
:attr:`telegram.ext.Dispatcher.chat_data` ``[chat_id]``.
"""
@abstractmethod
def update_bot_data(self, data: Dict) -> None:
def update_bot_data(self, data: BD) -> None:
"""Will be called by the :class:`telegram.ext.Dispatcher` after a handler has
handled an update.
Args:
data (:obj:`dict`): The :attr:`telegram.ext.dispatcher.bot_data` .
data (:class:`telegram.ext.utils.types.BD`): The
:attr:`telegram.ext.Dispatcher.bot_data`.
"""
def refresh_user_data(self, user_id: int, user_data: UD) -> None:
"""Will be called by the :class:`telegram.ext.Dispatcher` before passing the
:attr:`user_data` to a callback. Can be used to update data stored in :attr:`user_data`
from an external source.
.. versionadded:: 13.6
Args:
user_id (:obj:`int`): The user ID this :attr:`user_data` is associated with.
user_data (:class:`telegram.ext.utils.types.UD`): The ``user_data`` of a single user.
"""
def refresh_chat_data(self, chat_id: int, chat_data: CD) -> None:
"""Will be called by the :class:`telegram.ext.Dispatcher` before passing the
:attr:`chat_data` to a callback. Can be used to update data stored in :attr:`chat_data`
from an external source.
.. versionadded:: 13.6
Args:
chat_id (:obj:`int`): The chat ID this :attr:`chat_data` is associated with.
chat_data (:class:`telegram.ext.utils.types.CD`): The ``chat_data`` of a single chat.
"""
def refresh_bot_data(self, bot_data: BD) -> None:
"""Will be called by the :class:`telegram.ext.Dispatcher` before passing the
:attr:`bot_data` to a callback. Can be used to update data stored in :attr:`bot_data`
from an external source.
.. versionadded:: 13.6
Args:
bot_data (:class:`telegram.ext.utils.types.BD`): The ``bot_data``.
"""
def update_callback_data(self, data: CDCData) -> None:
"""Will be called by the :class:`telegram.ext.Dispatcher` after a handler has
handled an update.
.. versionadded:: 13.6
Args:
data (:class:`telegram.ext.utils.types.CDCData`): The relevant data to restore
:class:`telegram.ext.CallbackDataCache`.
"""
raise NotImplementedError
def flush(self) -> None:
"""Will be called by :class:`telegram.ext.Updater` upon receiving a stop signal. Gives the
persistence a chance to finish up saving or close a database connection gracefully.
+176 -32
View File
@@ -19,16 +19,32 @@
# pylint: disable=R0201
"""This module contains the CallbackContext class."""
from queue import Queue
from typing import TYPE_CHECKING, Dict, List, Match, NoReturn, Optional, Tuple, Union
from typing import (
TYPE_CHECKING,
Dict,
List,
Match,
NoReturn,
Optional,
Tuple,
Union,
Generic,
Type,
TypeVar,
)
from telegram import Update
from telegram import Update, CallbackQuery
from telegram.ext import ExtBot
from telegram.ext.utils.types import UD, CD, BD
if TYPE_CHECKING:
from telegram import Bot
from telegram.ext import Dispatcher, Job, JobQueue
CC = TypeVar('CC', bound='CallbackContext')
class CallbackContext:
class CallbackContext(Generic[UD, CD, BD]):
"""
This is a context object passed to the callback called by :class:`telegram.ext.Handler`
or by the :class:`telegram.ext.Dispatcher` in an error handler added by
@@ -50,20 +66,10 @@ class CallbackContext:
almost certainly execute the callbacks for an update out of order, and the attributes
that you think you added will not be present.
Args:
dispatcher (:class:`telegram.ext.Dispatcher`): The dispatcher associated with this context.
Attributes:
bot_data (:obj:`dict`): Optional. A dict that can be used to keep any data in. For each
update it will be the same ``dict``.
chat_data (:obj:`dict`): Optional. A dict that can be used to keep any data in. For each
update from the same chat id it will be the same ``dict``.
Warning:
When a group chat migrates to a supergroup, its chat id will change and the
``chat_data`` needs to be transferred. For details see our `wiki page
<https://github.com/python-telegram-bot/python-telegram-bot/wiki/
Storing-user--and-chat-related-data#chat-migration>`_.
user_data (:obj:`dict`): Optional. A dict that can be used to keep any data in. For each
update from the same user it will be the same ``dict``.
matches (List[:obj:`re match object`]): Optional. If the associated update originated from
a regex-supported handler or had a :class:`Filters.regex`, this will contain a list of
match objects for every pattern where ``re.search(pattern, string)`` returned a match.
@@ -86,6 +92,19 @@ class CallbackContext:
"""
__slots__ = (
'_dispatcher',
'_chat_id_and_data',
'_user_id_and_data',
'args',
'matches',
'error',
'job',
'async_args',
'async_kwargs',
'__dict__',
)
def __init__(self, dispatcher: 'Dispatcher'):
"""
Args:
@@ -96,9 +115,8 @@ class CallbackContext:
'CallbackContext should not be used with a non context aware ' 'dispatcher!'
)
self._dispatcher = dispatcher
self._bot_data = dispatcher.bot_data
self._chat_data: Optional[Dict[object, object]] = None
self._user_data: Optional[Dict[object, object]] = None
self._chat_id_and_data: Optional[Tuple[int, CD]] = None
self._user_id_and_data: Optional[Tuple[int, UD]] = None
self.args: Optional[List[str]] = None
self.matches: Optional[List[Match]] = None
self.error: Optional[Exception] = None
@@ -112,8 +130,11 @@ class CallbackContext:
return self._dispatcher
@property
def bot_data(self) -> Dict:
return self._bot_data
def bot_data(self) -> BD:
""":obj:`dict`: Optional. A dict that can be used to keep any data in. For each
update it will be the same ``dict``.
"""
return self.dispatcher.bot_data
@bot_data.setter
def bot_data(self, value: object) -> NoReturn:
@@ -122,8 +143,19 @@ class CallbackContext:
)
@property
def chat_data(self) -> Optional[Dict]:
return self._chat_data
def chat_data(self) -> Optional[CD]:
""":obj:`dict`: Optional. A dict that can be used to keep any data in. For each
update from the same chat id it will be the same ``dict``.
Warning:
When a group chat migrates to a supergroup, its chat id will change and the
``chat_data`` needs to be transferred. For details see our `wiki page
<https://github.com/python-telegram-bot/python-telegram-bot/wiki/
Storing-bot,-user-and-chat-related-data#chat-migration>`_.
"""
if self._chat_id_and_data:
return self._chat_id_and_data[1]
return None
@chat_data.setter
def chat_data(self, value: object) -> NoReturn:
@@ -132,8 +164,13 @@ class CallbackContext:
)
@property
def user_data(self) -> Optional[Dict]:
return self._user_data
def user_data(self) -> Optional[UD]:
""":obj:`dict`: Optional. A dict that can be used to keep any data in. For each
update from the same user it will be the same ``dict``.
"""
if self._user_id_and_data:
return self._user_id_and_data[1]
return None
@user_data.setter
def user_data(self, value: object) -> NoReturn:
@@ -141,15 +178,82 @@ class CallbackContext:
"You can not assign a new value to user_data, see https://git.io/Jt6ic"
)
def refresh_data(self) -> None:
"""If :attr:`dispatcher` uses persistence, calls
:meth:`telegram.ext.BasePersistence.refresh_bot_data` on :attr:`bot_data`,
:meth:`telegram.ext.BasePersistence.refresh_chat_data` on :attr:`chat_data` and
:meth:`telegram.ext.BasePersistence.refresh_user_data` on :attr:`user_data`, if
appropriate.
.. versionadded:: 13.6
"""
if self.dispatcher.persistence:
if self.dispatcher.persistence.store_bot_data:
self.dispatcher.persistence.refresh_bot_data(self.bot_data)
if self.dispatcher.persistence.store_chat_data and self._chat_id_and_data is not None:
self.dispatcher.persistence.refresh_chat_data(*self._chat_id_and_data)
if self.dispatcher.persistence.store_user_data and self._user_id_and_data is not None:
self.dispatcher.persistence.refresh_user_data(*self._user_id_and_data)
def drop_callback_data(self, callback_query: CallbackQuery) -> None:
"""
Deletes the cached data for the specified callback query.
.. versionadded:: 13.6
Note:
Will *not* raise exceptions in case the data is not found in the cache.
*Will* raise :class:`KeyError` in case the callback query can not be found in the
cache.
Args:
callback_query (:class:`telegram.CallbackQuery`): The callback query.
Raises:
KeyError | RuntimeError: :class:`KeyError`, if the callback query can not be found in
the cache and :class:`RuntimeError`, if the bot doesn't allow for arbitrary
callback data.
"""
if isinstance(self.bot, ExtBot):
if not self.bot.arbitrary_callback_data:
raise RuntimeError(
'This telegram.ext.ExtBot instance does not use arbitrary callback data.'
)
self.bot.callback_data_cache.drop_data(callback_query)
else:
raise RuntimeError('telegram.Bot does not allow for arbitrary callback data.')
@classmethod
def from_error(
cls,
cls: Type[CC],
update: object,
error: Exception,
dispatcher: 'Dispatcher',
async_args: Union[List, Tuple] = None,
async_kwargs: Dict[str, object] = None,
) -> 'CallbackContext':
) -> CC:
"""
Constructs an instance of :class:`telegram.ext.CallbackContext` to be passed to the error
handlers.
.. seealso:: :meth:`telegram.ext.Dispatcher.add_error_handler`
Args:
update (:obj:`object` | :class:`telegram.Update`): The update associated with the
error. May be :obj:`None`, e.g. for errors in job callbacks.
error (:obj:`Exception`): The error.
dispatcher (:class:`telegram.ext.Dispatcher`): The dispatcher associated with this
context.
async_args (List[:obj:`object`]): Optional. Positional arguments of the function that
raised the error. Pass only when the raising function was run asynchronously using
:meth:`telegram.ext.Dispatcher.run_async`.
async_kwargs (Dict[:obj:`str`, :obj:`object`]): Optional. Keyword arguments of the
function that raised the error. Pass only when the raising function was run
asynchronously using :meth:`telegram.ext.Dispatcher.run_async`.
Returns:
:class:`telegram.ext.CallbackContext`
"""
self = cls.from_update(update, dispatcher)
self.error = error
self.async_args = async_args
@@ -157,7 +261,21 @@ class CallbackContext:
return self
@classmethod
def from_update(cls, update: object, dispatcher: 'Dispatcher') -> 'CallbackContext':
def from_update(cls: Type[CC], update: object, dispatcher: 'Dispatcher') -> CC:
"""
Constructs an instance of :class:`telegram.ext.CallbackContext` to be passed to the
handlers.
.. seealso:: :meth:`telegram.ext.Dispatcher.add_handler`
Args:
update (:obj:`object` | :class:`telegram.Update`): The update.
dispatcher (:class:`telegram.ext.Dispatcher`): The dispatcher associated with this
context.
Returns:
:class:`telegram.ext.CallbackContext`
"""
self = cls(dispatcher)
if update is not None and isinstance(update, Update):
@@ -165,19 +283,45 @@ class CallbackContext:
user = update.effective_user
if chat:
self._chat_data = dispatcher.chat_data[chat.id] # pylint: disable=W0212
self._chat_id_and_data = (
chat.id,
dispatcher.chat_data[chat.id], # pylint: disable=W0212
)
if user:
self._user_data = dispatcher.user_data[user.id] # pylint: disable=W0212
self._user_id_and_data = (
user.id,
dispatcher.user_data[user.id], # pylint: disable=W0212
)
return self
@classmethod
def from_job(cls, job: 'Job', dispatcher: 'Dispatcher') -> 'CallbackContext':
def from_job(cls: Type[CC], job: 'Job', dispatcher: 'Dispatcher') -> CC:
"""
Constructs an instance of :class:`telegram.ext.CallbackContext` to be passed to a
job callback.
.. seealso:: :meth:`telegram.ext.JobQueue`
Args:
job (:class:`telegram.ext.Job`): The job.
dispatcher (:class:`telegram.ext.Dispatcher`): The dispatcher associated with this
context.
Returns:
:class:`telegram.ext.CallbackContext`
"""
self = cls(dispatcher)
self.job = job
return self
def update(self, data: Dict[str, object]) -> None:
self.__dict__.update(data)
"""Updates ``self.__slots__`` with the passed data.
Args:
data (Dict[:obj:`str`, :obj:`object`]): The data.
"""
for key, value in data.items():
setattr(self, key, value)
@property
def bot(self) -> 'Bot':
+427
View File
@@ -0,0 +1,427 @@
#!/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/].
#
# 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/].
"""This module contains the CallbackDataCache class."""
import logging
import time
from datetime import datetime
from threading import Lock
from typing import Dict, Tuple, Union, Optional, MutableMapping, TYPE_CHECKING, cast
from uuid import uuid4
from cachetools import LRUCache # pylint: disable=E0401
from telegram import (
InlineKeyboardMarkup,
InlineKeyboardButton,
TelegramError,
CallbackQuery,
Message,
User,
)
from telegram.utils.helpers import to_float_timestamp
from telegram.ext.utils.types import CDCData
if TYPE_CHECKING:
from telegram.ext import ExtBot
class InvalidCallbackData(TelegramError):
"""
Raised when the received callback data has been tempered with or deleted from cache.
.. versionadded:: 13.6
Args:
callback_data (:obj:`int`, optional): The button data of which the callback data could not
be found.
Attributes:
callback_data (:obj:`int`): Optional. The button data of which the callback data could not
be found.
"""
__slots__ = ('callback_data',)
def __init__(self, callback_data: str = None) -> None:
super().__init__(
'The object belonging to this callback_data was deleted or the callback_data was '
'manipulated.'
)
self.callback_data = callback_data
def __reduce__(self) -> Tuple[type, Tuple[Optional[str]]]: # type: ignore[override]
return self.__class__, (self.callback_data,)
class _KeyboardData:
__slots__ = ('keyboard_uuid', 'button_data', 'access_time')
def __init__(
self, keyboard_uuid: str, access_time: float = None, button_data: Dict[str, object] = None
):
self.keyboard_uuid = keyboard_uuid
self.button_data = button_data or {}
self.access_time = access_time or time.time()
def update_access_time(self) -> None:
"""Updates the access time with the current time."""
self.access_time = time.time()
def to_tuple(self) -> Tuple[str, float, Dict[str, object]]:
"""Gives a tuple representation consisting of the keyboard uuid, the access time and the
button data.
"""
return self.keyboard_uuid, self.access_time, self.button_data
class CallbackDataCache:
"""A custom cache for storing the callback data of a :class:`telegram.ext.ExtBot`. Internally,
it keeps two mappings with fixed maximum size:
* One for mapping the data received in callback queries to the cached objects
* One for mapping the IDs of received callback queries to the cached objects
The second mapping allows to manually drop data that has been cached for keyboards of messages
sent via inline mode.
If necessary, will drop the least recently used items.
.. versionadded:: 13.6
Args:
bot (:class:`telegram.ext.ExtBot`): The bot this cache is for.
maxsize (:obj:`int`, optional): Maximum number of items in each of the internal mappings.
Defaults to 1024.
persistent_data (:obj:`telegram.ext.utils.types.CDCData`, optional): Data to initialize
the cache with, as returned by :meth:`telegram.ext.BasePersistence.get_callback_data`.
Attributes:
bot (:class:`telegram.ext.ExtBot`): The bot this cache is for.
maxsize (:obj:`int`): maximum size of the cache.
"""
__slots__ = ('bot', 'maxsize', '_keyboard_data', '_callback_queries', '__lock', 'logger')
def __init__(
self,
bot: 'ExtBot',
maxsize: int = 1024,
persistent_data: CDCData = None,
):
self.logger = logging.getLogger(__name__)
self.bot = bot
self.maxsize = maxsize
self._keyboard_data: MutableMapping[str, _KeyboardData] = LRUCache(maxsize=maxsize)
self._callback_queries: MutableMapping[str, str] = LRUCache(maxsize=maxsize)
self.__lock = Lock()
if persistent_data:
keyboard_data, callback_queries = persistent_data
for key, value in callback_queries.items():
self._callback_queries[key] = value
for uuid, access_time, data in keyboard_data:
self._keyboard_data[uuid] = _KeyboardData(
keyboard_uuid=uuid, access_time=access_time, button_data=data
)
@property
def persistence_data(self) -> CDCData:
""":obj:`telegram.ext.utils.types.CDCData`: The data that needs to be persisted to allow
caching callback data across bot reboots.
"""
# While building a list/dict from the LRUCaches has linear runtime (in the number of
# entries), the runtime is bounded by maxsize and it has the big upside of not throwing a
# highly customized data structure at users trying to implement a custom persistence class
with self.__lock:
return [data.to_tuple() for data in self._keyboard_data.values()], dict(
self._callback_queries.items()
)
def process_keyboard(self, reply_markup: InlineKeyboardMarkup) -> InlineKeyboardMarkup:
"""Registers the reply markup to the cache. If any of the buttons have
:attr:`callback_data`, stores that data and builds a new keyboard with the correspondingly
replaced buttons. Otherwise does nothing and returns the original reply markup.
Args:
reply_markup (:class:`telegram.InlineKeyboardMarkup`): The keyboard.
Returns:
:class:`telegram.InlineKeyboardMarkup`: The keyboard to be passed to Telegram.
"""
with self.__lock:
return self.__process_keyboard(reply_markup)
def __process_keyboard(self, reply_markup: InlineKeyboardMarkup) -> InlineKeyboardMarkup:
keyboard_uuid = uuid4().hex
keyboard_data = _KeyboardData(keyboard_uuid)
# Built a new nested list of buttons by replacing the callback data if needed
buttons = [
[
# We create a new button instead of replacing callback_data in case the
# same object is used elsewhere
InlineKeyboardButton(
btn.text,
callback_data=self.__put_button(btn.callback_data, keyboard_data),
)
if btn.callback_data
else btn
for btn in column
]
for column in reply_markup.inline_keyboard
]
if not keyboard_data.button_data:
# If we arrive here, no data had to be replaced and we can return the input
return reply_markup
self._keyboard_data[keyboard_uuid] = keyboard_data
return InlineKeyboardMarkup(buttons)
@staticmethod
def __put_button(callback_data: object, keyboard_data: _KeyboardData) -> str:
"""Stores the data for a single button in :attr:`keyboard_data`.
Returns the string that should be passed instead of the callback_data, which is
``keyboard_uuid + button_uuids``.
"""
uuid = uuid4().hex
keyboard_data.button_data[uuid] = callback_data
return f'{keyboard_data.keyboard_uuid}{uuid}'
def __get_keyboard_uuid_and_button_data(
self, callback_data: str
) -> Union[Tuple[str, object], Tuple[None, InvalidCallbackData]]:
keyboard, button = self.extract_uuids(callback_data)
try:
# we get the values before calling update() in case KeyErrors are raised
# we don't want to update in that case
keyboard_data = self._keyboard_data[keyboard]
button_data = keyboard_data.button_data[button]
# Update the timestamp for the LRU
keyboard_data.update_access_time()
return keyboard, button_data
except KeyError:
return None, InvalidCallbackData(callback_data)
@staticmethod
def extract_uuids(callback_data: str) -> Tuple[str, str]:
"""Extracts the keyboard uuid and the button uuid from the given ``callback_data``.
Args:
callback_data (:obj:`str`): The ``callback_data`` as present in the button.
Returns:
(:obj:`str`, :obj:`str`): Tuple of keyboard and button uuid
"""
# Extract the uuids as put in __put_button
return callback_data[:32], callback_data[32:]
def process_message(self, message: Message) -> None:
"""Replaces the data in the inline keyboard attached to the message with the cached
objects, if necessary. If the data could not be found,
:class:`telegram.ext.InvalidCallbackData` will be inserted.
Note:
Checks :attr:`telegram.Message.via_bot` and :attr:`telegram.Message.from_user` to check
if the reply markup (if any) was actually sent by this caches bot. If it was not, the
message will be returned unchanged.
Note that this will fail for channel posts, as :attr:`telegram.Message.from_user` is
:obj:`None` for those! In the corresponding reply markups the callback data will be
replaced by :class:`telegram.ext.InvalidCallbackData`.
Warning:
* Does *not* consider :attr:`telegram.Message.reply_to_message` and
:attr:`telegram.Message.pinned_message`. Pass them to these method separately.
* *In place*, i.e. the passed :class:`telegram.Message` will be changed!
Args:
message (:class:`telegram.Message`): The message.
"""
with self.__lock:
self.__process_message(message)
def __process_message(self, message: Message) -> Optional[str]:
"""As documented in process_message, but returns the uuid of the attached keyboard, if any,
which is relevant for process_callback_query.
**IN PLACE**
"""
if not message.reply_markup:
return None
if message.via_bot:
sender: Optional[User] = message.via_bot
elif message.from_user:
sender = message.from_user
else:
sender = None
if sender is not None and sender != self.bot.bot:
return None
keyboard_uuid = None
for row in message.reply_markup.inline_keyboard:
for button in row:
if button.callback_data:
button_data = cast(str, button.callback_data)
keyboard_id, callback_data = self.__get_keyboard_uuid_and_button_data(
button_data
)
# update_callback_data makes sure that the _id_attrs are updated
button.update_callback_data(callback_data)
# This is lazy loaded. The firsts time we find a button
# we load the associated keyboard - afterwards, there is
if not keyboard_uuid and not isinstance(callback_data, InvalidCallbackData):
keyboard_uuid = keyboard_id
return keyboard_uuid
def process_callback_query(self, callback_query: CallbackQuery) -> None:
"""Replaces the data in the callback query and the attached messages keyboard with the
cached objects, if necessary. If the data could not be found,
:class:`telegram.ext.InvalidCallbackData` will be inserted.
If :attr:`callback_query.data` or :attr:`callback_query.message` is present, this also
saves the callback queries ID in order to be able to resolve it to the stored data.
Note:
Also considers inserts data into the buttons of
:attr:`telegram.Message.reply_to_message` and :attr:`telegram.Message.pinned_message`
if necessary.
Warning:
*In place*, i.e. the passed :class:`telegram.CallbackQuery` will be changed!
Args:
callback_query (:class:`telegram.CallbackQuery`): The callback query.
"""
with self.__lock:
mapped = False
if callback_query.data:
data = callback_query.data
# Get the cached callback data for the CallbackQuery
keyboard_uuid, button_data = self.__get_keyboard_uuid_and_button_data(data)
callback_query.data = button_data # type: ignore[assignment]
# Map the callback queries ID to the keyboards UUID for later use
if not mapped and not isinstance(button_data, InvalidCallbackData):
self._callback_queries[callback_query.id] = keyboard_uuid # type: ignore
mapped = True
# Get the cached callback data for the inline keyboard attached to the
# CallbackQuery.
if callback_query.message:
self.__process_message(callback_query.message)
for message in (
callback_query.message.pinned_message,
callback_query.message.reply_to_message,
):
if message:
self.__process_message(message)
def drop_data(self, callback_query: CallbackQuery) -> None:
"""Deletes the data for the specified callback query.
Note:
Will *not* raise exceptions in case the callback data is not found in the cache.
*Will* raise :class:`KeyError` in case the callback query can not be found in the
cache.
Args:
callback_query (:class:`telegram.CallbackQuery`): The callback query.
Raises:
KeyError: If the callback query can not be found in the cache
"""
with self.__lock:
try:
keyboard_uuid = self._callback_queries.pop(callback_query.id)
self.__drop_keyboard(keyboard_uuid)
except KeyError as exc:
raise KeyError('CallbackQuery was not found in cache.') from exc
def __drop_keyboard(self, keyboard_uuid: str) -> None:
try:
self._keyboard_data.pop(keyboard_uuid)
except KeyError:
return
def clear_callback_data(self, time_cutoff: Union[float, datetime] = None) -> None:
"""Clears the stored callback data.
Args:
time_cutoff (:obj:`float` | :obj:`datetime.datetime`, optional): Pass a UNIX timestamp
or a :obj:`datetime.datetime` to clear only entries which are older.
For timezone naive :obj:`datetime.datetime` objects, the default timezone of the
bot will be used.
"""
with self.__lock:
self.__clear(self._keyboard_data, time_cutoff=time_cutoff)
def clear_callback_queries(self) -> None:
"""Clears the stored callback query IDs."""
with self.__lock:
self.__clear(self._callback_queries)
def __clear(self, mapping: MutableMapping, time_cutoff: Union[float, datetime] = None) -> None:
if not time_cutoff:
mapping.clear()
return
if isinstance(time_cutoff, datetime):
effective_cutoff = to_float_timestamp(
time_cutoff, tzinfo=self.bot.defaults.tzinfo if self.bot.defaults else None
)
else:
effective_cutoff = time_cutoff
# We need a list instead of a generator here, as the list doesn't change it's size
# during the iteration
to_drop = [key for key, data in mapping.items() if data.access_time < effective_cutoff]
for key in to_drop:
mapping.pop(key)
+63 -21
View File
@@ -35,26 +35,35 @@ from telegram import Update
from telegram.utils.helpers import DefaultValue, DEFAULT_FALSE
from .handler import Handler
from .utils.types import CCT
if TYPE_CHECKING:
from telegram.ext import CallbackContext, Dispatcher
from telegram.ext import Dispatcher
RT = TypeVar('RT')
class CallbackQueryHandler(Handler[Update]):
class CallbackQueryHandler(Handler[Update, CCT]):
"""Handler class to handle Telegram callback queries. Optionally based on a regex.
Read the documentation of the ``re`` module for more information.
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``.
* :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.
Note that this is DEPRECATED, and you should use context based callbacks. See
https://git.io/fxJuV for more info.
* If your bot allows arbitrary objects as ``callback_data``, it may happen that the
original ``callback_data`` for the incoming :class:`telegram.CallbackQuery`` can not be
found. This is the case when either a malicious client tempered with the
``callback_data`` or the data was simply dropped from cache or not persisted. In these
cases, an instance of :class:`telegram.ext.InvalidCallbackData` will be set as
``callback_data``.
.. versionadded:: 13.6
Warning:
When setting ``run_async`` to :obj:`True`, you cannot rely on adding custom
@@ -79,9 +88,24 @@ class CallbackQueryHandler(Handler[Update]):
: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.
pattern (:obj:`str` | `Pattern`, optional): Regex pattern. If not :obj:`None`, ``re.match``
is used on :attr:`telegram.CallbackQuery.data` to determine if an update should be
handled by this handler.
pattern (:obj:`str` | `Pattern` | :obj:`callable` | :obj:`type`, optional):
Pattern to test :attr:`telegram.CallbackQuery.data` against. If a string or a regex
pattern is passed, :meth:`re.match` is used on :attr:`telegram.CallbackQuery.data` to
determine if an update should be handled by this handler. If your bot allows arbitrary
objects as ``callback_data``, non-strings will be accepted. To filter arbitrary
objects you may pass
* a callable, accepting exactly one argument, namely the
:attr:`telegram.CallbackQuery.data`. It must return :obj:`True` or
:obj:`False`/:obj:`None` to indicate, whether the update should be handled.
* a :obj:`type`. If :attr:`telegram.CallbackQuery.data` is an instance of that type
(or a subclass), the update will be handled.
If :attr:`telegram.CallbackQuery.data` is :obj:`None`, the
:class:`telegram.CallbackQuery` update will not be handled.
.. versionchanged:: 13.6
Added support for arbitrary callback data.
pass_groups (:obj:`bool`, optional): If the callback should be passed the result of
``re.match(pattern, data).groups()`` as a keyword argument called ``groups``.
Default is :obj:`False`
@@ -105,8 +129,11 @@ class CallbackQueryHandler(Handler[Update]):
passed to the callback function.
pass_job_queue (:obj:`bool`): Determines whether ``job_queue`` will be passed to
the callback function.
pattern (:obj:`str` | `Pattern`): Optional. Regex pattern to test
:attr:`telegram.CallbackQuery.data` against.
pattern (`Pattern` | :obj:`callable` | :obj:`type`): Optional. Regex pattern, callback or
type to test :attr:`telegram.CallbackQuery.data` against.
.. versionchanged:: 13.6
Added support for arbitrary callback data.
pass_groups (:obj:`bool`): Determines whether ``groups`` will be passed to the
callback function.
pass_groupdict (:obj:`bool`): Determines whether ``groupdict``. will be passed to
@@ -119,12 +146,14 @@ class CallbackQueryHandler(Handler[Update]):
"""
__slots__ = ('pattern', 'pass_groups', 'pass_groupdict')
def __init__(
self,
callback: Callable[[Update, 'CallbackContext'], RT],
callback: Callable[[Update, CCT], RT],
pass_update_queue: bool = False,
pass_job_queue: bool = False,
pattern: Union[str, Pattern] = None,
pattern: Union[str, Pattern, type, Callable[[object], Optional[bool]]] = None,
pass_groups: bool = False,
pass_groupdict: bool = False,
pass_user_data: bool = False,
@@ -158,11 +187,17 @@ class CallbackQueryHandler(Handler[Update]):
"""
if isinstance(update, Update) and update.callback_query:
callback_data = update.callback_query.data
if self.pattern:
if update.callback_query.data:
match = re.match(self.pattern, update.callback_query.data)
if match:
return match
if callback_data is None:
return False
if isinstance(self.pattern, type):
return isinstance(callback_data, self.pattern)
if callable(self.pattern):
return self.pattern(callback_data)
match = re.match(self.pattern, callback_data)
if match:
return match
else:
return True
return None
@@ -173,8 +208,12 @@ class CallbackQueryHandler(Handler[Update]):
update: Update = None,
check_result: Union[bool, Match] = None,
) -> Dict[str, object]:
"""Pass the results of ``re.match(pattern, data).{groups(), groupdict()}`` to the
callback as a keyword arguments called ``groups`` and ``groupdict``, respectively, if
needed.
"""
optional_args = super().collect_optional_args(dispatcher, update, check_result)
if self.pattern:
if self.pattern and not callable(self.pattern):
check_result = cast(Match, check_result)
if self.pass_groups:
optional_args['groups'] = check_result.groups()
@@ -184,11 +223,14 @@ class CallbackQueryHandler(Handler[Update]):
def collect_additional_context(
self,
context: 'CallbackContext',
context: CCT,
update: Update,
dispatcher: 'Dispatcher',
check_result: Union[bool, Match],
) -> None:
"""Add the result of ``re.match(pattern, update.callback_query.data)`` to
:attr:`CallbackContext.matches` as list with one element.
"""
if self.pattern:
check_result = cast(Match, check_result)
context.matches = [check_result]
+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)
+5 -6
View File
@@ -17,19 +17,17 @@
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains the ChatMemberHandler classes."""
from typing import ClassVar, TypeVar, Union, Callable, TYPE_CHECKING
from typing import ClassVar, TypeVar, Union, Callable
from telegram import Update
from telegram.utils.helpers import DefaultValue, DEFAULT_FALSE
from .handler import Handler
if TYPE_CHECKING:
from telegram.ext import CallbackContext
from .utils.types import CCT
RT = TypeVar('RT')
class ChatMemberHandler(Handler[Update]):
class ChatMemberHandler(Handler[Update, CCT]):
"""Handler class to handle Telegram updates that contain a chat member update.
.. versionadded:: 13.4
@@ -96,6 +94,7 @@ class ChatMemberHandler(Handler[Update]):
"""
__slots__ = ('chat_member_types',)
MY_CHAT_MEMBER: ClassVar[int] = -1
""":obj:`int`: Used as a constant to handle only :attr:`telegram.Update.my_chat_member`."""
CHAT_MEMBER: ClassVar[int] = 0
@@ -106,7 +105,7 @@ class ChatMemberHandler(Handler[Update]):
def __init__(
self,
callback: Callable[[Update, 'CallbackContext'], RT],
callback: Callable[[Update, CCT], RT],
chat_member_types: int = MY_CHAT_MEMBER,
pass_update_queue: bool = False,
pass_job_queue: bool = False,
+66 -4
View File
@@ -17,17 +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/].
"""This module contains the ChosenInlineResultHandler class."""
from typing import Optional, TypeVar, Union
import re
from typing import Optional, TypeVar, Union, Callable, TYPE_CHECKING, Pattern, Match, cast
from telegram import Update
from telegram.utils.helpers import DefaultValue, DEFAULT_FALSE
from .handler import Handler
from .utils.types import CCT
RT = TypeVar('RT')
if TYPE_CHECKING:
from telegram.ext import CallbackContext, Dispatcher
class ChosenInlineResultHandler(Handler[Update]):
class ChosenInlineResultHandler(Handler[Update, CCT]):
"""Handler class to handle Telegram updates that contain a chosen inline result.
Note:
@@ -70,6 +75,12 @@ class ChosenInlineResultHandler(Handler[Update]):
DEPRECATED: Please switch to context based callbacks.
run_async (:obj:`bool`): Determines whether the callback will run asynchronously.
Defaults to :obj:`False`.
pattern (:obj:`str` | `Pattern`, optional): Regex pattern. If not :obj:`None`, ``re.match``
is used on :attr:`telegram.ChosenInlineResult.result_id` to determine if an update
should be handled by this handler. This is accessible in the callback as
:attr:`telegram.ext.CallbackContext.matches`.
.. versionadded:: 13.6
Attributes:
callback (:obj:`callable`): The callback function for this handler.
@@ -82,9 +93,39 @@ class ChosenInlineResultHandler(Handler[Update]):
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.
pattern (`Pattern`): Optional. Regex pattern to test
:attr:`telegram.ChosenInlineResult.result_id` against.
.. versionadded:: 13.6
"""
__slots__ = ('pattern',)
def __init__(
self,
callback: Callable[[Update, 'CallbackContext'], RT],
pass_update_queue: bool = False,
pass_job_queue: bool = False,
pass_user_data: bool = False,
pass_chat_data: bool = False,
run_async: Union[bool, DefaultValue] = DEFAULT_FALSE,
pattern: Union[str, Pattern] = None,
):
super().__init__(
callback,
pass_update_queue=pass_update_queue,
pass_job_queue=pass_job_queue,
pass_user_data=pass_user_data,
pass_chat_data=pass_chat_data,
run_async=run_async,
)
if isinstance(pattern, str):
pattern = re.compile(pattern)
self.pattern = pattern
def check_update(self, update: object) -> Optional[Union[bool, object]]:
"""Determines whether an update should be passed to this handlers :attr:`callback`.
@@ -95,4 +136,25 @@ class ChosenInlineResultHandler(Handler[Update]):
:obj:`bool`
"""
return isinstance(update, Update) and update.chosen_inline_result
if isinstance(update, Update) and update.chosen_inline_result:
if self.pattern:
match = re.match(self.pattern, update.chosen_inline_result.result_id)
if match:
return match
else:
return True
return None
def collect_additional_context(
self,
context: 'CallbackContext',
update: Update,
dispatcher: 'Dispatcher',
check_result: Union[bool, Match],
) -> None:
"""This function adds the matched regex pattern result to
:attr:`telegram.ext.CallbackContext.matches`.
"""
if self.pattern:
check_result = cast(Match, check_result)
context.matches = [check_result]
+19 -19
View File
@@ -27,15 +27,16 @@ from telegram.utils.deprecate import TelegramDeprecationWarning
from telegram.utils.types import SLT
from telegram.utils.helpers import DefaultValue, DEFAULT_FALSE
from .utils.types import CCT
from .handler import Handler
if TYPE_CHECKING:
from telegram.ext import CallbackContext, Dispatcher
from telegram.ext import Dispatcher
RT = TypeVar('RT')
class CommandHandler(Handler[Update]):
class CommandHandler(Handler[Update, CCT]):
"""Handler class to handle Telegram commands.
Commands are Telegram messages that start with ``/``, optionally followed by an ``@`` and the
@@ -129,10 +130,12 @@ class CommandHandler(Handler[Update]):
run_async (:obj:`bool`): Determines whether the callback will run asynchronously.
"""
__slots__ = ('command', 'filters', 'pass_args')
def __init__(
self,
command: SLT[str],
callback: Callable[[Update, 'CallbackContext'], RT],
callback: Callable[[Update, CCT], RT],
filters: BaseFilter = None,
allow_edited: bool = None,
pass_args: bool = False,
@@ -219,6 +222,9 @@ class CommandHandler(Handler[Update]):
update: Update = None,
check_result: Optional[Union[bool, Tuple[List[str], Optional[bool]]]] = None,
) -> Dict[str, object]:
"""Provide text after the command to the callback the ``args`` argument as list, split on
single whitespaces.
"""
optional_args = super().collect_optional_args(dispatcher, update)
if self.pass_args and isinstance(check_result, tuple):
optional_args['args'] = check_result[0]
@@ -226,11 +232,14 @@ class CommandHandler(Handler[Update]):
def collect_additional_context(
self,
context: 'CallbackContext',
context: CCT,
update: Update,
dispatcher: 'Dispatcher',
check_result: Optional[Union[bool, Tuple[List[str], Optional[bool]]]],
) -> None:
"""Add text after the command to :attr:`CallbackContext.args` as list, split on single
whitespaces and add output of data filters to :attr:`CallbackContext` as well.
"""
if isinstance(check_result, tuple):
context.args = check_result[0]
if isinstance(check_result[1], dict):
@@ -238,7 +247,7 @@ class CommandHandler(Handler[Update]):
class PrefixHandler(CommandHandler):
"""Handler class to handle custom prefix commands
"""Handler class to handle custom prefix commands.
This is a intermediate handler between :class:`MessageHandler` and :class:`CommandHandler`.
It supports configurable commands with the same options as CommandHandler. It will respond to
@@ -265,7 +274,7 @@ class PrefixHandler(CommandHandler):
.. code:: python
PrefixHandler(['!', '#'], ['test', 'help'], callback) # will respond to '!test', \
'#test', '!help' and '#help'.
'#test', '!help' and '#help'.
By default the handler listens to messages as well as edited messages. To change this behavior
@@ -344,11 +353,14 @@ class PrefixHandler(CommandHandler):
"""
# 'prefix' is a class property, & 'command' is included in the superclass, so they're left out.
__slots__ = ('_prefix', '_command', '_commands')
def __init__(
self,
prefix: SLT[str],
command: SLT[str],
callback: Callable[[Update, 'CallbackContext'], RT],
callback: Callable[[Update, CCT], RT],
filters: BaseFilter = None,
pass_args: bool = False,
pass_update_queue: bool = False,
@@ -442,15 +454,3 @@ class PrefixHandler(CommandHandler):
return text_list[1:], filter_result
return False
return None
def collect_additional_context(
self,
context: 'CallbackContext',
update: Update,
dispatcher: 'Dispatcher',
check_result: Optional[Union[bool, Tuple[List[str], Optional[bool]]]],
) -> None:
if isinstance(check_result, tuple):
context.args = check_result[0]
if isinstance(check_result[1], dict):
context.update(check_result[1])
+202
View File
@@ -0,0 +1,202 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
# Copyright (C) 2020
# 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=R0201
"""This module contains the auxiliary class ContextTypes."""
from typing import Type, Generic, overload, Dict # pylint: disable=W0611
from telegram.ext.callbackcontext import CallbackContext
from telegram.ext.utils.types import CCT, UD, CD, BD
class ContextTypes(Generic[CCT, UD, CD, BD]):
"""
Convenience class to gather customizable types of the :class:`telegram.ext.CallbackContext`
interface.
.. versionadded:: 13.6
Args:
context (:obj:`type`, optional): Determines the type of the ``context`` argument of all
(error-)handler callbacks and job callbacks. Must be a subclass of
:class:`telegram.ext.CallbackContext`. Defaults to
:class:`telegram.ext.CallbackContext`.
bot_data (:obj:`type`, optional): Determines the type of ``context.bot_data`` of all
(error-)handler callbacks and job callbacks. Defaults to :obj:`dict`. Must support
instantiating without arguments.
chat_data (:obj:`type`, optional): Determines the type of ``context.chat_data`` of all
(error-)handler callbacks and job callbacks. Defaults to :obj:`dict`. Must support
instantiating without arguments.
user_data (:obj:`type`, optional): Determines the type of ``context.user_data`` of all
(error-)handler callbacks and job callbacks. Defaults to :obj:`dict`. Must support
instantiating without arguments.
"""
__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], Dict, Dict, Dict]',
):
...
@overload
def __init__(self: 'ContextTypes[CCT, Dict, Dict, Dict]', context: Type[CCT]):
...
@overload
def __init__(
self: 'ContextTypes[CallbackContext[UD, Dict, Dict], UD, Dict, Dict]', user_data: Type[UD]
):
...
@overload
def __init__(
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]
):
...
@overload
def __init__(
self: 'ContextTypes[CCT, Dict, CD, Dict]', context: Type[CCT], chat_data: Type[CD]
):
...
@overload
def __init__(
self: 'ContextTypes[CCT, Dict, Dict, BD]', context: Type[CCT], bot_data: Type[BD]
):
...
@overload
def __init__(
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], UD, Dict, BD]',
user_data: Type[UD],
bot_data: Type[BD],
):
...
@overload
def __init__(
self: 'ContextTypes[CallbackContext[Dict, CD, BD], Dict, CD, BD]',
chat_data: Type[CD],
bot_data: Type[BD],
):
...
@overload
def __init__(
self: 'ContextTypes[CCT, UD, CD, Dict]',
context: Type[CCT],
user_data: Type[UD],
chat_data: Type[CD],
):
...
@overload
def __init__(
self: 'ContextTypes[CCT, UD, Dict, BD]',
context: Type[CCT],
user_data: Type[UD],
bot_data: Type[BD],
):
...
@overload
def __init__(
self: 'ContextTypes[CCT, Dict, CD, BD]',
context: Type[CCT],
chat_data: Type[CD],
bot_data: Type[BD],
):
...
@overload
def __init__(
self: 'ContextTypes[CallbackContext[UD, CD, BD], UD, CD, BD]',
user_data: Type[UD],
chat_data: Type[CD],
bot_data: Type[BD],
):
...
@overload
def __init__(
self: 'ContextTypes[CCT, UD, CD, BD]',
context: Type[CCT],
user_data: Type[UD],
chat_data: Type[CD],
bot_data: Type[BD],
):
...
def __init__( # type: ignore[no-untyped-def]
self,
context=CallbackContext,
bot_data=dict,
chat_data=dict,
user_data=dict,
):
if not issubclass(context, CallbackContext):
raise ValueError('context must be a subclass of CallbackContext.')
# We make all those only accessible via properties because we don't currently support
# changing this at runtime, so overriding the attributes doesn't make sense
self._context = context
self._bot_data = bot_data
self._chat_data = chat_data
self._user_data = user_data
@property
def context(self) -> Type[CCT]:
return self._context
@property
def bot_data(self) -> Type[BD]:
return self._bot_data
@property
def chat_data(self) -> Type[CD]:
return self._chat_data
@property
def user_data(self) -> Type[UD]:
return self._user_data
+61 -37
View File
@@ -37,7 +37,8 @@ from telegram.ext import (
InlineQueryHandler,
)
from telegram.ext.utils.promise import Promise
from telegram.utils.types import ConversationDict
from telegram.ext.utils.types import ConversationDict
from telegram.ext.utils.types import CCT
if TYPE_CHECKING:
from telegram.ext import Dispatcher, Job
@@ -45,6 +46,9 @@ CheckUpdateType = Optional[Tuple[Tuple[int, ...], Handler, object]]
class _ConversationTimeoutContext:
# '__dict__' is not included since this a private class
__slots__ = ('conversation_key', 'update', 'dispatcher', 'callback_context')
def __init__(
self,
conversation_key: Tuple[int, ...],
@@ -58,7 +62,7 @@ class _ConversationTimeoutContext:
self.callback_context = callback_context
class ConversationHandler(Handler[Update]):
class ConversationHandler(Handler[Update, CCT]):
"""
A handler to hold a conversation with a single or multiple users through Telegram updates by
managing four collections of other handlers.
@@ -173,33 +177,8 @@ class ConversationHandler(Handler[Update]):
ValueError
Attributes:
entry_points (List[:class:`telegram.ext.Handler`]): A list of ``Handler`` objects that can
trigger the start of the conversation.
states (Dict[:obj:`object`, List[:class:`telegram.ext.Handler`]]): A :obj:`dict` that
defines the different states of conversation a user can be in and one or more
associated ``Handler`` objects that should be used in that state.
fallbacks (List[:class:`telegram.ext.Handler`]): A list of handlers that might be used if
the user is in a conversation, but every handler for their current state returned
:obj:`False` on :attr:`check_update`.
allow_reentry (:obj:`bool`): Determines if a user can restart a conversation with
an entry point.
per_chat (:obj:`bool`): If the conversationkey should contain the Chat's ID.
per_user (:obj:`bool`): If the conversationkey should contain the User's ID.
per_message (:obj:`bool`): If the conversationkey should contain the Message's
ID.
conversation_timeout (:obj:`float` | :obj:`datetime.timedelta`): Optional. When this
handler is inactive more than this timeout (in seconds), it will be automatically
ended. If this value is 0 (default), there will be no timeout. When it's triggered, the
last received update and the corresponding ``context`` will be handled by ALL the
handler's who's :attr:`check_update` method returns :obj:`True` that are in the state
:attr:`ConversationHandler.TIMEOUT`.
name (:obj:`str`): Optional. The name for this conversationhandler. Required for
persistence
persistent (:obj:`bool`): Optional. If the conversations dict for this handler should be
saved. Name is required and persistence has to be set in :class:`telegram.ext.Updater`
map_to_parent (Dict[:obj:`object`, :obj:`object`]): Optional. A :obj:`dict` that can be
used to instruct a nested conversationhandler to transition into a mapped state on
its parent conversationhandler in place of a specified nested state.
run_async (:obj:`bool`): If :obj:`True`, will override the
:attr:`Handler.run_async` setting of all internal handlers on initialization.
@@ -207,6 +186,26 @@ class ConversationHandler(Handler[Update]):
"""
__slots__ = (
'_entry_points',
'_states',
'_fallbacks',
'_allow_reentry',
'_per_user',
'_per_chat',
'_per_message',
'_conversation_timeout',
'_name',
'persistent',
'_persistence',
'_map_to_parent',
'timeout_jobs',
'_timeout_jobs_lock',
'_conversations',
'_conversations_lock',
'logger',
)
END: ClassVar[int] = -1
""":obj:`int`: Used as a constant to return when a conversation is ended."""
TIMEOUT: ClassVar[int] = -2
@@ -217,9 +216,9 @@ class ConversationHandler(Handler[Update]):
# pylint: disable=W0231
def __init__(
self,
entry_points: List[Handler],
states: Dict[object, List[Handler]],
fallbacks: List[Handler],
entry_points: List[Handler[Update, CCT]],
states: Dict[object, List[Handler[Update, CCT]]],
fallbacks: List[Handler[Update, CCT]],
allow_reentry: bool = False,
per_chat: bool = True,
per_user: bool = True,
@@ -316,6 +315,9 @@ class ConversationHandler(Handler[Update]):
@property
def entry_points(self) -> List[Handler]:
"""List[:class:`telegram.ext.Handler`]: A list of ``Handler`` objects that can trigger the
start of the conversation.
"""
return self._entry_points
@entry_points.setter
@@ -324,6 +326,10 @@ class ConversationHandler(Handler[Update]):
@property
def states(self) -> Dict[object, List[Handler]]:
"""Dict[:obj:`object`, List[:class:`telegram.ext.Handler`]]: A :obj:`dict` that
defines the different states of conversation a user can be in and one or more
associated ``Handler`` objects that should be used in that state.
"""
return self._states
@states.setter
@@ -332,6 +338,10 @@ class ConversationHandler(Handler[Update]):
@property
def fallbacks(self) -> List[Handler]:
"""List[:class:`telegram.ext.Handler`]: A list of handlers that might be used if
the user is in a conversation, but every handler for their current state returned
:obj:`False` on :attr:`check_update`.
"""
return self._fallbacks
@fallbacks.setter
@@ -340,6 +350,7 @@ class ConversationHandler(Handler[Update]):
@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
@@ -348,6 +359,7 @@ class ConversationHandler(Handler[Update]):
@property
def per_user(self) -> bool:
""":obj:`bool`: If the conversation key should contain the User's ID."""
return self._per_user
@per_user.setter
@@ -356,6 +368,7 @@ class ConversationHandler(Handler[Update]):
@property
def per_chat(self) -> bool:
""":obj:`bool`: If the conversation key should contain the Chat's ID."""
return self._per_chat
@per_chat.setter
@@ -364,6 +377,7 @@ class ConversationHandler(Handler[Update]):
@property
def per_message(self) -> bool:
""":obj:`bool`: If the conversation key should contain the message's ID."""
return self._per_message
@per_message.setter
@@ -374,16 +388,21 @@ class ConversationHandler(Handler[Update]):
def conversation_timeout(
self,
) -> Optional[Union[float, datetime.timedelta]]:
""":obj:`float` | :obj:`datetime.timedelta`: Optional. When this
handler is inactive more than this timeout (in seconds), it will be automatically
ended.
"""
return self._conversation_timeout
@conversation_timeout.setter
def conversation_timeout(self, value: object) -> NoReturn:
raise ValueError(
'You can not assign a new value to conversation_timeout after ' 'initialization.'
'You can not assign a new value to conversation_timeout after initialization.'
)
@property
def name(self) -> Optional[str]:
""":obj:`str`: Optional. The name for this :class:`ConversationHandler`."""
return self._name
@name.setter
@@ -392,6 +411,10 @@ class ConversationHandler(Handler[Update]):
@property
def map_to_parent(self) -> Optional[Dict[object, object]]:
"""Dict[:obj:`object`, :obj:`object`]: Optional. A :obj:`dict` that can be
used to instruct a nested :class:`ConversationHandler` to transition into a mapped state on
its parent :class:`ConversationHandler` in place of a specified nested state.
"""
return self._map_to_parent
@map_to_parent.setter
@@ -400,6 +423,7 @@ class ConversationHandler(Handler[Update]):
@property
def persistence(self) -> Optional[BasePersistence]:
"""The persistence class as provided by the :class:`Dispatcher`."""
return self._persistence
@persistence.setter
@@ -412,7 +436,7 @@ class ConversationHandler(Handler[Update]):
handler.persistence = self.persistence
@property
def conversations(self) -> ConversationDict:
def conversations(self) -> ConversationDict: # skipcq: PY-D0003
return self._conversations
@conversations.setter
@@ -518,7 +542,7 @@ class ConversationHandler(Handler[Update]):
# check if promise is finished or not
if state[1].done.wait(0):
res = self._resolve_promise(state)
self.update_state(res, key)
self._update_state(res, key)
with self._conversations_lock:
state = self.conversations.get(key)
@@ -627,19 +651,19 @@ class ConversationHandler(Handler[Update]):
)
if isinstance(self.map_to_parent, dict) and new_state in self.map_to_parent:
self.update_state(self.END, conversation_key)
self._update_state(self.END, conversation_key)
if raise_dp_handler_stop:
raise DispatcherHandlerStop(self.map_to_parent.get(new_state))
return self.map_to_parent.get(new_state)
self.update_state(new_state, conversation_key)
self._update_state(new_state, conversation_key)
if raise_dp_handler_stop:
# Don't pass the new state here. If we're in a nested conversation, the parent is
# expecting None as return value.
raise DispatcherHandlerStop()
return None
def update_state(self, new_state: object, key: Tuple[int, ...]) -> None:
def _update_state(self, new_state: object, key: Tuple[int, ...]) -> None:
if new_state == self.END:
with self._conversations_lock:
if key in self.conversations:
@@ -698,4 +722,4 @@ class ConversationHandler(Handler[Update]):
'ConversationHandler has no effect. Ignoring.'
)
self.update_state(self.END, ctxt.conversation_key)
self._update_state(self.END, ctxt.conversation_key)
+54 -30
View File
@@ -22,6 +22,7 @@ from typing import NoReturn, Optional, Dict, Any
import pytz
from telegram.utils.deprecate import set_new_attribute_deprecated
from telegram.utils.helpers import DEFAULT_NONE
from telegram.utils.types import ODVInput
@@ -41,6 +42,9 @@ class Defaults:
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).
Note:
Will *not* be used for :meth:`telegram.Bot.get_updates`!
quote (:obj:`bool`, optional): If set to :obj:`True`, the reply is sent as an actual reply
to the message. If ``reply_to_message_id`` is passed in ``kwargs``, this parameter will
be ignored. Default: :obj:`True` in group chats and :obj:`False` in private chats.
@@ -48,37 +52,24 @@ class Defaults:
appearing throughout PTB, i.e. if a timezone naive date(time) object is passed
somewhere, it will be assumed to be in ``tzinfo``. Must be a timezone provided by the
``pytz`` module. Defaults to UTC.
Note:
Will *not* be used for :meth:`telegram.Bot.get_updates`!
run_async (:obj:`bool`, optional): Default setting for the ``run_async`` parameter of
handlers and error handlers registered through :meth:`Dispatcher.add_handler` and
:meth:`Dispatcher.add_error_handler`. Defaults to :obj:`False`.
Attributes:
parse_mode (:obj:`str`): Optional. Send Markdown or HTML, if you want Telegram apps to show
bold, italic, fixed-width text or URLs in your bot's message.
explanation_parse_mode (:obj:`str`): Optional. Alias for :attr:`parse_mode`, used for
the corresponding parameter of :meth:`telegram.Bot.send_poll`.
disable_notification (:obj:`bool`): Optional. Sends the message silently. Users will
receive a notification with no sound.
disable_web_page_preview (:obj:`bool`): Optional. Disables link previews for links in this
message.
allow_sending_without_reply (:obj:`bool`): Optional. Pass :obj:`True`, if the message
should be sent even if the specified replied-to message is not found.
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).
quote (:obj:`bool`): Optional. If set to :obj:`True`, the reply is sent as an actual reply
to the message. If ``reply_to_message_id`` is passed in ``kwargs``, this parameter will
be ignored. Default: :obj:`True` in group chats and :obj:`False` in private chats.
tzinfo (:obj:`tzinfo`): A timezone to be used for all date(time) objects appearing
throughout PTB.
run_async (:obj:`bool`): Optional. Default setting for the ``run_async`` parameter of
handlers and error handlers registered through :meth:`Dispatcher.add_handler` and
:meth:`Dispatcher.add_error_handler`.
"""
__slots__ = (
'_timeout',
'_tzinfo',
'_disable_web_page_preview',
'_run_async',
'_quote',
'_disable_notification',
'_allow_sending_without_reply',
'_parse_mode',
'_api_defaults',
'__dict__',
)
def __init__(
self,
parse_mode: str = None,
@@ -114,15 +105,21 @@ class Defaults:
if value not in [None, DEFAULT_NONE]:
self._api_defaults[kwarg] = value
# Special casing, as None is a valid default value
if self.timeout != DEFAULT_NONE:
self._api_defaults['timeout'] = self.timeout
if self._timeout != DEFAULT_NONE:
self._api_defaults['timeout'] = self._timeout
def __setattr__(self, key: str, value: object) -> None:
set_new_attribute_deprecated(self, key, value)
@property
def api_defaults(self) -> Dict[str, Any]:
def api_defaults(self) -> Dict[str, Any]: # skip-cq: PY-D0003
return self._api_defaults
@property
def parse_mode(self) -> Optional[str]:
""":obj:`str`: Optional. Send Markdown or HTML, if you want Telegram apps to show
bold, italic, fixed-width text or URLs in your bot's message.
"""
return self._parse_mode
@parse_mode.setter
@@ -134,6 +131,9 @@ class Defaults:
@property
def explanation_parse_mode(self) -> Optional[str]:
""":obj:`str`: Optional. Alias for :attr:`parse_mode`, used for
the corresponding parameter of :meth:`telegram.Bot.send_poll`.
"""
return self._parse_mode
@explanation_parse_mode.setter
@@ -145,6 +145,9 @@ class Defaults:
@property
def disable_notification(self) -> Optional[bool]:
""":obj:`bool`: Optional. Sends the message silently. Users will
receive a notification with no sound.
"""
return self._disable_notification
@disable_notification.setter
@@ -156,6 +159,9 @@ class Defaults:
@property
def disable_web_page_preview(self) -> Optional[bool]:
""":obj:`bool`: Optional. Disables link previews for links in this
message.
"""
return self._disable_web_page_preview
@disable_web_page_preview.setter
@@ -167,6 +173,9 @@ class Defaults:
@property
def allow_sending_without_reply(self) -> Optional[bool]:
""":obj:`bool`: Optional. Pass :obj:`True`, if the message
should be sent even if the specified replied-to message is not found.
"""
return self._allow_sending_without_reply
@allow_sending_without_reply.setter
@@ -178,6 +187,10 @@ class Defaults:
@property
def timeout(self) -> ODVInput[float]:
""":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).
"""
return self._timeout
@timeout.setter
@@ -189,6 +202,10 @@ class Defaults:
@property
def quote(self) -> Optional[bool]:
""":obj:`bool`: Optional. If set to :obj:`True`, the reply is sent as an actual reply
to the message. If ``reply_to_message_id`` is passed in ``kwargs``, this parameter will
be ignored. Default: :obj:`True` in group chats and :obj:`False` in private chats.
"""
return self._quote
@quote.setter
@@ -200,6 +217,9 @@ class Defaults:
@property
def tzinfo(self) -> pytz.BaseTzInfo:
""":obj:`tzinfo`: A timezone to be used for all date(time) objects appearing
throughout PTB.
"""
return self._tzinfo
@tzinfo.setter
@@ -211,6 +231,10 @@ class Defaults:
@property
def run_async(self) -> bool:
""":obj:`bool`: Optional. Default setting for the ``run_async`` parameter of
handlers and error handlers registered through :meth:`Dispatcher.add_handler` and
:meth:`Dispatcher.add_error_handler`.
"""
return self._run_async
@run_async.setter
@@ -236,7 +260,7 @@ class Defaults:
def __eq__(self, other: object) -> bool:
if isinstance(other, Defaults):
return self.__dict__ == other.__dict__
return all(getattr(self, attr) == getattr(other, attr) for attr in self.__slots__)
return False
def __ne__(self, other: object) -> bool:
+146 -30
View File
@@ -17,9 +17,8 @@
# 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 DictPersistence class."""
from copy import deepcopy
from typing import DefaultDict, Dict, Optional, Tuple
from typing import DefaultDict, Dict, Optional, Tuple, cast
from collections import defaultdict
from telegram.utils.helpers import (
@@ -28,7 +27,7 @@ from telegram.utils.helpers import (
encode_conversations_to_json,
)
from telegram.ext import BasePersistence
from telegram.utils.types import ConversationDict
from telegram.ext.utils.types import ConversationDict, CDCData
try:
import ujson as json
@@ -37,7 +36,7 @@ except ImportError:
class DictPersistence(BasePersistence):
"""Using python's dicts and json for making your bot persistent.
"""Using Python's :obj:`dict` and ``json`` for making your bot persistent.
Note:
This class does *not* implement a :meth:`flush` method, meaning that data managed by
@@ -57,17 +56,25 @@ class DictPersistence(BasePersistence):
Args:
store_user_data (:obj:`bool`, optional): Whether user_data should be saved by this
persistence class. Default is :obj:`True`.
store_chat_data (:obj:`bool`, optional): Whether user_data should be saved by this
store_chat_data (:obj:`bool`, optional): Whether chat_data should be saved by this
persistence class. Default is :obj:`True`.
store_bot_data (:obj:`bool`, optional): Whether bot_data should be saved by this
persistence class. Default is :obj:`True` .
user_data_json (:obj:`str`, optional): Json string that will be used to reconstruct
persistence class. Default is :obj:`True`.
store_callback_data (:obj:`bool`, optional): Whether callback_data should be saved by this
persistence class. Default is :obj:`False`.
.. versionadded:: 13.6
user_data_json (:obj:`str`, optional): JSON string that will be used to reconstruct
user_data on creating this persistence. Default is ``""``.
chat_data_json (:obj:`str`, optional): Json string that will be used to reconstruct
chat_data_json (:obj:`str`, optional): JSON string that will be used to reconstruct
chat_data on creating this persistence. Default is ``""``.
bot_data_json (:obj:`str`, optional): Json string that will be used to reconstruct
bot_data_json (:obj:`str`, optional): JSON string that will be used to reconstruct
bot_data on creating this persistence. Default is ``""``.
conversations_json (:obj:`str`, optional): Json string that will be used to reconstruct
callback_data_json (:obj:`str`, optional): Json string that will be used to reconstruct
callback_data on creating this persistence. Default is ``""``.
.. versionadded:: 13.6
conversations_json (:obj:`str`, optional): JSON string that will be used to reconstruct
conversation on creating this persistence. Default is ``""``.
Attributes:
@@ -77,8 +84,25 @@ class DictPersistence(BasePersistence):
persistence class.
store_bot_data (:obj:`bool`): Whether bot_data should be saved by this
persistence class.
store_callback_data (:obj:`bool`): Whether callback_data be saved by this
persistence class.
.. versionadded:: 13.6
"""
__slots__ = (
'_user_data',
'_chat_data',
'_bot_data',
'_callback_data',
'_conversations',
'_user_data_json',
'_chat_data_json',
'_bot_data_json',
'_callback_data_json',
'_conversations_json',
)
def __init__(
self,
store_user_data: bool = True,
@@ -88,19 +112,24 @@ class DictPersistence(BasePersistence):
chat_data_json: str = '',
bot_data_json: str = '',
conversations_json: str = '',
store_callback_data: bool = False,
callback_data_json: str = '',
):
super().__init__(
store_user_data=store_user_data,
store_chat_data=store_chat_data,
store_bot_data=store_bot_data,
store_callback_data=store_callback_data,
)
self._user_data = None
self._chat_data = None
self._bot_data = None
self._callback_data = None
self._conversations = None
self._user_data_json = None
self._chat_data_json = None
self._bot_data_json = None
self._callback_data_json = None
self._conversations_json = None
if user_data_json:
try:
@@ -122,6 +151,34 @@ class DictPersistence(BasePersistence):
raise TypeError("Unable to deserialize bot_data_json. Not valid JSON") from exc
if not isinstance(self._bot_data, dict):
raise TypeError("bot_data_json must be serialized dict")
if callback_data_json:
try:
data = json.loads(callback_data_json)
except (ValueError, AttributeError) as exc:
raise TypeError(
"Unable to deserialize callback_data_json. Not valid JSON"
) from exc
# We are a bit more thorough with the checking of the format here, because it's
# more complicated than for the other things
try:
if data is None:
self._callback_data = None
else:
self._callback_data = cast(
CDCData,
([(one, float(two), three) for one, two, three in data[0]], data[1]),
)
self._callback_data_json = callback_data_json
except (ValueError, IndexError) as exc:
raise TypeError("callback_data_json is not in the required format") from exc
if self._callback_data is not None and (
not all(
isinstance(entry[2], dict) and isinstance(entry[0], str)
for entry in self._callback_data[0]
)
or not isinstance(self._callback_data[1], dict)
):
raise TypeError("callback_data_json is not in the required format")
if conversations_json:
try:
@@ -169,7 +226,25 @@ class DictPersistence(BasePersistence):
return json.dumps(self.bot_data)
@property
def conversations(self) -> Optional[Dict[str, Dict[Tuple, object]]]:
def callback_data(self) -> Optional[CDCData]:
""":class:`telegram.ext.utils.types.CDCData`: The meta data on the stored callback data.
.. versionadded:: 13.6
"""
return self._callback_data
@property
def callback_data_json(self) -> str:
""":obj:`str`: The meta data on the stored callback data as a JSON-string.
.. versionadded:: 13.6
"""
if self._callback_data_json:
return self._callback_data_json
return json.dumps(self.callback_data)
@property
def conversations(self) -> Optional[Dict[str, ConversationDict]]:
""":obj:`dict`: The conversations as a dict."""
return self._conversations
@@ -187,11 +262,9 @@ class DictPersistence(BasePersistence):
Returns:
:obj:`defaultdict`: The restored user data.
"""
if self.user_data:
pass
else:
if self.user_data is None:
self._user_data = defaultdict(dict)
return deepcopy(self.user_data) # type: ignore[arg-type]
return self.user_data # type: ignore[return-value]
def get_chat_data(self) -> DefaultDict[int, Dict[object, object]]:
"""Returns the chat_data created from the ``chat_data_json`` or an empty
@@ -200,11 +273,9 @@ class DictPersistence(BasePersistence):
Returns:
:obj:`defaultdict`: The restored chat data.
"""
if self.chat_data:
pass
else:
if self.chat_data is None:
self._chat_data = defaultdict(dict)
return deepcopy(self.chat_data) # type: ignore[arg-type]
return self.chat_data # type: ignore[return-value]
def get_bot_data(self) -> Dict[object, object]:
"""Returns the bot_data created from the ``bot_data_json`` or an empty :obj:`dict`.
@@ -212,11 +283,23 @@ class DictPersistence(BasePersistence):
Returns:
:obj:`dict`: The restored bot data.
"""
if self.bot_data:
pass
else:
if self.bot_data is None:
self._bot_data = {}
return deepcopy(self.bot_data) # type: ignore[arg-type]
return self.bot_data # type: ignore[return-value]
def get_callback_data(self) -> Optional[CDCData]:
"""Returns the callback_data created from the ``callback_data_json`` or :obj:`None`.
.. versionadded:: 13.6
Returns:
Optional[:class:`telegram.ext.utils.types.CDCData`]: The restored meta data or
:obj:`None`, if no data was stored.
"""
if self.callback_data is None:
self._callback_data = None
return None
return self.callback_data[0], self.callback_data[1].copy()
def get_conversations(self, name: str) -> ConversationDict:
"""Returns the conversations created from the ``conversations_json`` or an empty
@@ -225,9 +308,7 @@ class DictPersistence(BasePersistence):
Returns:
:obj:`dict`: The restored conversations data.
"""
if self.conversations:
pass
else:
if self.conversations is None:
self._conversations = {}
return self.conversations.get(name, {}).copy() # type: ignore[union-attr]
@@ -253,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)
@@ -267,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)
@@ -280,9 +361,44 @@ 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
self._bot_data = data.copy()
self._bot_data = data
self._bot_data_json = None
def update_callback_data(self, data: CDCData) -> None:
"""Will update the callback_data (if changed).
.. versionadded:: 13.6
Args:
data (:class:`telegram.ext.utils.types.CDCData`): The relevant data to restore
:class:`telegram.ext.CallbackDataCache`.
"""
if self._callback_data == data:
return
self._callback_data = (data[0], data[1].copy())
self._callback_data_json = None
def refresh_user_data(self, user_id: int, user_data: Dict) -> None:
"""Does nothing.
.. versionadded:: 13.6
.. seealso:: :meth:`telegram.ext.BasePersistence.refresh_user_data`
"""
def refresh_chat_data(self, chat_id: int, chat_data: Dict) -> None:
"""Does nothing.
.. versionadded:: 13.6
.. seealso:: :meth:`telegram.ext.BasePersistence.refresh_chat_data`
"""
def refresh_bot_data(self, bot_data: Dict) -> None:
"""Does nothing.
.. versionadded:: 13.6
.. seealso:: :meth:`telegram.ext.BasePersistence.refresh_bot_data`
"""
+157 -26
View File
@@ -26,16 +26,32 @@ from functools import wraps
from queue import Empty, Queue
from threading import BoundedSemaphore, Event, Lock, Thread, current_thread
from time import sleep
from typing import TYPE_CHECKING, Callable, DefaultDict, Dict, List, Optional, Set, Union
from typing import (
TYPE_CHECKING,
Callable,
DefaultDict,
Dict,
List,
Optional,
Set,
Union,
Generic,
TypeVar,
overload,
cast,
)
from uuid import uuid4
from telegram import TelegramError, Update
from telegram.ext import BasePersistence
from telegram.ext import BasePersistence, ContextTypes
from telegram.ext.callbackcontext import CallbackContext
from telegram.ext.handler import Handler
from telegram.utils.deprecate import TelegramDeprecationWarning
import telegram.ext.extbot
from telegram.ext.callbackdatacache import CallbackDataCache
from telegram.utils.deprecate import TelegramDeprecationWarning, set_new_attribute_deprecated
from telegram.ext.utils.promise import Promise
from telegram.utils.helpers import DefaultValue, DEFAULT_FALSE
from telegram.ext.utils.types import CCT, UD, CD, BD
if TYPE_CHECKING:
from telegram import Bot
@@ -43,6 +59,8 @@ if TYPE_CHECKING:
DEFAULT_GROUP: int = 0
UT = TypeVar('UT')
def run_async(
func: Callable[[Update, CallbackContext], object]
@@ -98,12 +116,14 @@ class DispatcherHandlerStop(Exception):
state (:obj:`object`, optional): The next state of the conversation.
"""
__slots__ = ('state',)
def __init__(self, state: object = None) -> None:
super().__init__()
self.state = state
class Dispatcher:
class Dispatcher(Generic[CCT, UD, CD, BD]):
"""This class dispatches all kinds of updates to its registered handlers.
Args:
@@ -118,6 +138,12 @@ class Dispatcher:
use_context (:obj:`bool`, optional): If set to :obj:`True` uses the context based callback
API (ignored if `dispatcher` argument is used). Defaults to :obj:`True`.
**New users**: set this to :obj:`True`.
context_types (:class:`telegram.ext.ContextTypes`, optional): Pass an instance
of :class:`telegram.ext.ContextTypes` to customize the types used in the
``context`` interface. If not passed, the defaults documented in
:class:`telegram.ext.ContextTypes` will be used.
.. versionadded:: 13.6
Attributes:
bot (:class:`telegram.Bot`): The bot object that should be passed to the handlers.
@@ -131,14 +157,70 @@ class Dispatcher:
bot_data (:obj:`dict`): A dictionary handlers can use to store data for the bot.
persistence (:class:`telegram.ext.BasePersistence`): Optional. The persistence class to
store data that should be persistent over restarts.
context_types (:class:`telegram.ext.ContextTypes`): Container for the types used
in the ``context`` interface.
.. versionadded:: 13.6
"""
# Allowing '__weakref__' creation here since we need it for the singleton
__slots__ = (
'workers',
'persistence',
'use_context',
'update_queue',
'job_queue',
'user_data',
'chat_data',
'bot_data',
'_update_persistence_lock',
'handlers',
'groups',
'error_handlers',
'running',
'__stop_event',
'__exception_event',
'__async_queue',
'__async_threads',
'bot',
'__dict__',
'__weakref__',
'context_types',
)
__singleton_lock = Lock()
__singleton_semaphore = BoundedSemaphore()
__singleton = None
logger = logging.getLogger(__name__)
@overload
def __init__(
self: 'Dispatcher[CallbackContext[Dict, Dict, Dict], Dict, Dict, Dict]',
bot: 'Bot',
update_queue: Queue,
workers: int = 4,
exception_event: Event = None,
job_queue: 'JobQueue' = None,
persistence: BasePersistence = None,
use_context: bool = True,
):
...
@overload
def __init__(
self: 'Dispatcher[CCT, UD, CD, BD]',
bot: 'Bot',
update_queue: Queue,
workers: int = 4,
exception_event: Event = None,
job_queue: 'JobQueue' = None,
persistence: BasePersistence = None,
use_context: bool = True,
context_types: ContextTypes[CCT, UD, CD, BD] = None,
):
...
def __init__(
self,
bot: 'Bot',
@@ -148,12 +230,14 @@ class Dispatcher:
job_queue: 'JobQueue' = None,
persistence: BasePersistence = None,
use_context: bool = True,
context_types: ContextTypes[CCT, UD, CD, BD] = None,
):
self.bot = bot
self.update_queue = update_queue
self.job_queue = job_queue
self.workers = workers
self.use_context = use_context
self.context_types = cast(ContextTypes[CCT, UD, CD, BD], context_types or ContextTypes())
if not use_context:
warnings.warn(
@@ -167,9 +251,9 @@ class Dispatcher:
'Asynchronous callbacks can not be processed without at least one worker thread.'
)
self.user_data: DefaultDict[int, Dict[object, object]] = defaultdict(dict)
self.chat_data: DefaultDict[int, Dict[object, object]] = defaultdict(dict)
self.bot_data = {}
self.user_data: DefaultDict[int, UD] = defaultdict(self.context_types.user_data)
self.chat_data: DefaultDict[int, CD] = defaultdict(self.context_types.chat_data)
self.bot_data = self.context_types.bot_data()
self.persistence: Optional[BasePersistence] = None
self._update_persistence_lock = Lock()
if persistence:
@@ -187,8 +271,21 @@ class Dispatcher:
raise ValueError("chat_data must be of type defaultdict")
if self.persistence.store_bot_data:
self.bot_data = self.persistence.get_bot_data()
if not isinstance(self.bot_data, dict):
raise ValueError("bot_data must be of type dict")
if not isinstance(self.bot_data, self.context_types.bot_data):
raise ValueError(
f"bot_data must be of type {self.context_types.bot_data.__name__}"
)
if self.persistence.store_callback_data:
self.bot = cast(telegram.ext.extbot.ExtBot, self.bot)
persistent_data = self.persistence.get_callback_data()
if persistent_data is not None:
if not isinstance(persistent_data, tuple) and len(persistent_data) != 2:
raise ValueError('callback_data must be a 2-tuple')
self.bot.callback_data_cache = CallbackDataCache(
self.bot,
self.bot.callback_data_cache.maxsize,
persistent_data=persistent_data,
)
else:
self.persistence = None
@@ -215,8 +312,19 @@ class Dispatcher:
else:
self._set_singleton(None)
def __setattr__(self, key: str, value: object) -> None:
# Mangled names don't automatically apply in __setattr__ (see
# https://docs.python.org/3/tutorial/classes.html#private-variables), so we have to make
# it mangled so they don't raise TelegramDeprecationWarning unnecessarily
if key.startswith('__'):
key = f"_{self.__class__.__name__}{key}"
if issubclass(self.__class__, Dispatcher) and self.__class__ is not Dispatcher:
object.__setattr__(self, key, value)
return
set_new_attribute_deprecated(self, key, value)
@property
def exception_event(self) -> Event:
def exception_event(self) -> Event: # skipcq: PY-D0003
return self.__exception_event
def _init_async_threads(self, base_name: str, workers: int) -> None:
@@ -404,7 +512,7 @@ class Dispatcher:
self.logger.debug('async thread %s/%s has ended', i + 1, total)
@property
def has_running_threads(self) -> bool:
def has_running_threads(self) -> bool: # skipcq: PY-D0003
return self.running or bool(self.__async_threads)
def process_update(self, update: object) -> None:
@@ -422,7 +530,6 @@ class Dispatcher:
The update to process.
"""
# An error happened while polling
if isinstance(update, TelegramError):
try:
@@ -441,7 +548,8 @@ class Dispatcher:
check = handler.check_update(update)
if check is not None and check is not False:
if not context and self.use_context:
context = CallbackContext.from_update(update, self)
context = self.context_types.context.from_update(update, self)
context.refresh_data()
handled = True
sync_modes.append(handler.run_async)
handler.handle_update(update, self, check, context)
@@ -474,7 +582,7 @@ class Dispatcher:
if not handled_only_async:
self.update_persistence(update=update)
def add_handler(self, handler: Handler, group: int = DEFAULT_GROUP) -> None:
def add_handler(self, handler: Handler[UT, CCT], group: int = DEFAULT_GROUP) -> None:
"""Register a handler.
TL;DR: Order and priority counts. 0 or 1 handlers per group will be used. End handling of
@@ -506,14 +614,22 @@ class Dispatcher:
raise TypeError(f'handler is not an instance of {Handler.__name__}')
if not isinstance(group, int):
raise TypeError('group is not int')
if isinstance(handler, ConversationHandler) and handler.persistent and handler.name:
# For some reason MyPy infers the type of handler is <nothing> here,
# so for now we just ignore all the errors
if (
isinstance(handler, ConversationHandler)
and handler.persistent # type: ignore[attr-defined]
and handler.name # type: ignore[attr-defined]
):
if not self.persistence:
raise ValueError(
f"ConversationHandler {handler.name} can not be persistent if dispatcher has "
f"no persistence"
f"ConversationHandler {handler.name} " # type: ignore[attr-defined]
f"can not be persistent if dispatcher has no persistence"
)
handler.persistence = self.persistence
handler.conversations = self.persistence.get_conversations(handler.name)
handler.persistence = self.persistence # type: ignore[attr-defined]
handler.conversations = ( # type: ignore[attr-defined]
self.persistence.get_conversations(handler.name) # type: ignore[attr-defined]
)
if group not in self.handlers:
self.handlers[group] = []
@@ -541,7 +657,7 @@ class Dispatcher:
Args:
update (:class:`telegram.Update`, optional): The update to process. If passed, only the
corresponding ``user_data`` and ``chat_data`` will be updated.
corresponding ``user_data`` and ``chat_data`` will be updated.
"""
with self._update_persistence_lock:
self.__update_persistence(update)
@@ -563,6 +679,22 @@ class Dispatcher:
else:
user_ids = []
if self.persistence.store_callback_data:
self.bot = cast(telegram.ext.extbot.ExtBot, self.bot)
try:
self.persistence.update_callback_data(
self.bot.callback_data_cache.persistence_data
)
except Exception as exc:
try:
self.dispatch_error(update, exc)
except Exception:
message = (
'Saving callback data raised an error and an '
'uncaught error was raised while handling '
'the error with an error_handler'
)
self.logger.exception(message)
if self.persistence.store_bot_data:
try:
self.persistence.update_bot_data(self.bot_data)
@@ -607,7 +739,7 @@ class Dispatcher:
def add_error_handler(
self,
callback: Callable[[object, CallbackContext], None],
callback: Callable[[object, CCT], None],
run_async: Union[bool, DefaultValue] = DEFAULT_FALSE, # pylint: disable=W0621
) -> None:
"""Registers an error handler in the Dispatcher. This handler will receive every error
@@ -637,13 +769,12 @@ class Dispatcher:
self.logger.debug('The callback is already registered as an error handler. Ignoring.')
return
if run_async is DEFAULT_FALSE and self.bot.defaults:
if self.bot.defaults.run_async:
run_async = True
if run_async is DEFAULT_FALSE and self.bot.defaults and self.bot.defaults.run_async:
run_async = True
self.error_handlers[callback] = run_async
def remove_error_handler(self, callback: Callable[[object, CallbackContext], None]) -> None:
def remove_error_handler(self, callback: Callable[[object, CCT], None]) -> None:
"""Removes an error handler.
Args:
@@ -670,7 +801,7 @@ class Dispatcher:
if self.error_handlers:
for callback, run_async in self.error_handlers.items(): # pylint: disable=W0621
if self.use_context:
context = CallbackContext.from_error(
context = self.context_types.context.from_error(
update, error, self, async_args=async_args, async_kwargs=async_kwargs
)
if run_async:
+337
View File
@@ -0,0 +1,337 @@
#!/usr/bin/env python
# pylint: disable=E0611,E0213,E1102,C0103,E1101,R0913,R0904
#
# 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/].
"""This module contains an object that represents a Telegram Bot with convenience extensions."""
from copy import copy
from typing import Union, cast, List, Callable, Optional, Tuple, TypeVar, TYPE_CHECKING, Sequence
import telegram.bot
from telegram import (
ReplyMarkup,
Message,
InlineKeyboardMarkup,
Poll,
MessageId,
Update,
Chat,
CallbackQuery,
)
from telegram.ext.callbackdatacache import CallbackDataCache
from telegram.utils.types import JSONDict, ODVInput, DVInput
from ..utils.helpers import DEFAULT_NONE
if TYPE_CHECKING:
from telegram import InlineQueryResult, MessageEntity
from telegram.utils.request import Request
from .defaults import Defaults
HandledTypes = TypeVar('HandledTypes', bound=Union[Message, CallbackQuery, Chat])
class ExtBot(telegram.bot.Bot):
"""This object represents a Telegram Bot with convenience extensions.
Warning:
Not to be confused with :class:`telegram.Bot`.
For the documentation of the arguments, methods and attributes, please see
:class:`telegram.Bot`.
.. versionadded:: 13.6
Args:
defaults (:class:`telegram.ext.Defaults`, optional): An object containing default values to
be used if not set explicitly in the bot methods.
arbitrary_callback_data (:obj:`bool` | :obj:`int`, optional): Whether to
allow arbitrary objects as callback data for :class:`telegram.InlineKeyboardButton`.
Pass an integer to specify the maximum number of objects cached in memory. For more
details, please see our `wiki <https://git.io/JGBDI>`_. Defaults to :obj:`False`.
Attributes:
arbitrary_callback_data (:obj:`bool` | :obj:`int`): Whether this bot instance
allows to use arbitrary objects as callback data for
:class:`telegram.InlineKeyboardButton`.
callback_data_cache (:class:`telegram.ext.CallbackDataCache`): The cache for objects passed
as callback data for :class:`telegram.InlineKeyboardButton`.
"""
__slots__ = ('arbitrary_callback_data', 'callback_data_cache')
# The ext_bot argument is a little hack to get warnings handled correctly.
# It's not very clean, but the warnings will be dropped at some point anyway.
def __setattr__(self, key: str, value: object, ext_bot: bool = True) -> None:
if issubclass(self.__class__, ExtBot) and self.__class__ is not ExtBot:
object.__setattr__(self, key, value)
return
super().__setattr__(key, value, ext_bot=ext_bot) # type: ignore[call-arg]
def __init__(
self,
token: str,
base_url: str = None,
base_file_url: str = None,
request: 'Request' = None,
private_key: bytes = None,
private_key_password: bytes = None,
defaults: 'Defaults' = None,
arbitrary_callback_data: Union[bool, int] = False,
):
super().__init__(
token=token,
base_url=base_url,
base_file_url=base_file_url,
request=request,
private_key=private_key,
private_key_password=private_key_password,
)
# 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):
maxsize = cast(int, arbitrary_callback_data)
self.arbitrary_callback_data = True
else:
maxsize = 1024
self.arbitrary_callback_data = arbitrary_callback_data
self.callback_data_cache: CallbackDataCache = CallbackDataCache(bot=self, maxsize=maxsize)
def _replace_keyboard(self, reply_markup: Optional[ReplyMarkup]) -> Optional[ReplyMarkup]:
# If the reply_markup is an inline keyboard and we allow arbitrary callback data, let the
# CallbackDataCache build a new keyboard with the data replaced. Otherwise return the input
if isinstance(reply_markup, InlineKeyboardMarkup) and self.arbitrary_callback_data:
return self.callback_data_cache.process_keyboard(reply_markup)
return reply_markup
def insert_callback_data(self, update: Update) -> None:
"""If this bot allows for arbitrary callback data, this inserts the cached data into all
corresponding buttons within this update.
Note:
Checks :attr:`telegram.Message.via_bot` and :attr:`telegram.Message.from_user` to check
if the reply markup (if any) was actually sent by this caches bot. If it was not, the
message will be returned unchanged.
Note that this will fail for channel posts, as :attr:`telegram.Message.from_user` is
:obj:`None` for those! In the corresponding reply markups the callback data will be
replaced by :class:`telegram.ext.InvalidCallbackData`.
Warning:
*In place*, i.e. the passed :class:`telegram.Message` will be changed!
Args:
update (:class`telegram.Update`): The update.
"""
# The only incoming updates that can directly contain a message sent by the bot itself are:
# * CallbackQueries
# * Messages where the pinned_message is sent by the bot
# * Messages where the reply_to_message is sent by the bot
# * Messages where via_bot is the bot
# Finally there is effective_chat.pinned message, but that's only returned in get_chat
if update.callback_query:
self._insert_callback_data(update.callback_query)
# elif instead of if, as effective_message includes callback_query.message
# and that has already been processed
elif update.effective_message:
self._insert_callback_data(update.effective_message)
def _insert_callback_data(self, obj: HandledTypes) -> HandledTypes:
if not self.arbitrary_callback_data:
return obj
if isinstance(obj, CallbackQuery):
self.callback_data_cache.process_callback_query(obj)
return obj # type: ignore[return-value]
if isinstance(obj, Message):
if obj.reply_to_message:
# reply_to_message can't contain further reply_to_messages, so no need to check
self.callback_data_cache.process_message(obj.reply_to_message)
if obj.reply_to_message.pinned_message:
# pinned messages can't contain reply_to_message, no need to check
self.callback_data_cache.process_message(obj.reply_to_message.pinned_message)
if obj.pinned_message:
# pinned messages can't contain reply_to_message, no need to check
self.callback_data_cache.process_message(obj.pinned_message)
# Finally, handle the message itself
self.callback_data_cache.process_message(message=obj)
return obj # type: ignore[return-value]
if isinstance(obj, Chat) and obj.pinned_message:
self.callback_data_cache.process_message(obj.pinned_message)
return obj
def _message(
self,
endpoint: str,
data: JSONDict,
reply_to_message_id: int = None,
disable_notification: ODVInput[bool] = DEFAULT_NONE,
reply_markup: ReplyMarkup = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
timeout: ODVInput[float] = DEFAULT_NONE,
api_kwargs: JSONDict = None,
) -> Union[bool, Message]:
# We override this method to call self._replace_keyboard and self._insert_callback_data.
# This covers most methods that have a reply_markup
result = super()._message(
endpoint=endpoint,
data=data,
reply_to_message_id=reply_to_message_id,
disable_notification=disable_notification,
reply_markup=self._replace_keyboard(reply_markup),
allow_sending_without_reply=allow_sending_without_reply,
timeout=timeout,
api_kwargs=api_kwargs,
)
if isinstance(result, Message):
self._insert_callback_data(result)
return result
def get_updates(
self,
offset: int = None,
limit: int = 100,
timeout: float = 0,
read_latency: float = 2.0,
allowed_updates: List[str] = None,
api_kwargs: JSONDict = None,
) -> List[Update]:
updates = super().get_updates(
offset=offset,
limit=limit,
timeout=timeout,
read_latency=read_latency,
allowed_updates=allowed_updates,
api_kwargs=api_kwargs,
)
for update in updates:
self.insert_callback_data(update)
return updates
def _effective_inline_results( # pylint: disable=R0201
self,
results: Union[
Sequence['InlineQueryResult'], Callable[[int], Optional[Sequence['InlineQueryResult']]]
],
next_offset: str = None,
current_offset: str = None,
) -> Tuple[Sequence['InlineQueryResult'], Optional[str]]:
"""
This method is called by Bot.answer_inline_query to build the actual results list.
Overriding this to call self._replace_keyboard suffices
"""
effective_results, next_offset = super()._effective_inline_results(
results=results, next_offset=next_offset, current_offset=current_offset
)
# Process arbitrary callback
if not self.arbitrary_callback_data:
return effective_results, next_offset
results = []
for result in effective_results:
# All currently existingInlineQueryResults have a reply_markup, but future ones
# might not have. Better be save than sorry
if not hasattr(result, 'reply_markup'):
results.append(result)
else:
# We build a new result in case the user wants to use the same object in
# different places
new_result = copy(result)
markup = self._replace_keyboard(result.reply_markup) # type: ignore[attr-defined]
new_result.reply_markup = markup
results.append(new_result)
return results, next_offset
def stop_poll(
self,
chat_id: Union[int, str],
message_id: int,
reply_markup: InlineKeyboardMarkup = None,
timeout: ODVInput[float] = DEFAULT_NONE,
api_kwargs: JSONDict = None,
) -> Poll:
# We override this method to call self._replace_keyboard
return super().stop_poll(
chat_id=chat_id,
message_id=message_id,
reply_markup=self._replace_keyboard(reply_markup),
timeout=timeout,
api_kwargs=api_kwargs,
)
def copy_message(
self,
chat_id: Union[int, str],
from_chat_id: Union[str, int],
message_id: int,
caption: str = None,
parse_mode: ODVInput[str] = DEFAULT_NONE,
caption_entities: Union[Tuple['MessageEntity', ...], List['MessageEntity']] = None,
disable_notification: DVInput[bool] = DEFAULT_NONE,
reply_to_message_id: int = None,
allow_sending_without_reply: DVInput[bool] = DEFAULT_NONE,
reply_markup: ReplyMarkup = None,
timeout: ODVInput[float] = DEFAULT_NONE,
api_kwargs: JSONDict = None,
) -> MessageId:
# We override this method to call self._replace_keyboard
return super().copy_message(
chat_id=chat_id,
from_chat_id=from_chat_id,
message_id=message_id,
caption=caption,
parse_mode=parse_mode,
caption_entities=caption_entities,
disable_notification=disable_notification,
reply_to_message_id=reply_to_message_id,
allow_sending_without_reply=allow_sending_without_reply,
reply_markup=self._replace_keyboard(reply_markup),
timeout=timeout,
api_kwargs=api_kwargs,
)
def get_chat(
self,
chat_id: Union[str, int],
timeout: ODVInput[float] = DEFAULT_NONE,
api_kwargs: JSONDict = None,
) -> Chat:
# 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`"""
+190 -27
View File
@@ -23,6 +23,7 @@ import re
import warnings
from abc import ABC, abstractmethod
from sys import version_info as py_ver
from threading import Lock
from typing import (
Dict,
@@ -50,7 +51,7 @@ __all__ = [
'XORFilter',
]
from telegram.utils.deprecate import TelegramDeprecationWarning
from telegram.utils.deprecate import TelegramDeprecationWarning, set_new_attribute_deprecated
from telegram.utils.types import SLT
DataDict = Dict[str, list]
@@ -112,12 +113,21 @@ class BaseFilter(ABC):
(depends on the handler).
"""
_name = None
data_filter = False
if py_ver < (3, 7):
__slots__ = ('_name', '_data_filter')
else:
__slots__ = ('_name', '_data_filter', '__dict__') # type: ignore[assignment]
def __new__(cls, *args: object, **kwargs: object) -> 'BaseFilter': # pylint: disable=W0613
instance = super().__new__(cls)
instance._name = None
instance._data_filter = False
return instance
@abstractmethod
def __call__(self, update: Update) -> Optional[Union[bool, DataDict]]:
pass
...
def __and__(self, other: 'BaseFilter') -> 'BaseFilter':
return MergedFilter(self, and_filter=other)
@@ -131,13 +141,33 @@ class BaseFilter(ABC):
def __invert__(self) -> 'BaseFilter':
return InvertedFilter(self)
def __setattr__(self, key: str, value: object) -> None:
# Allow setting custom attributes w/o warning for user defined custom filters.
# To differentiate between a custom and a PTB filter, we use this hacky but
# simple way of checking the module name where the class is defined from.
if (
issubclass(self.__class__, (UpdateFilter, MessageFilter))
and self.__class__.__module__ != __name__
): # __name__ is telegram.ext.filters
object.__setattr__(self, key, value)
return
set_new_attribute_deprecated(self, key, value)
@property
def data_filter(self) -> bool:
return self._data_filter
@data_filter.setter
def data_filter(self, value: bool) -> None:
self._data_filter = value
@property
def name(self) -> Optional[str]:
return self._name
@name.setter
def name(self, name: Optional[str]) -> None:
self._name = name
self._name = name # pylint: disable=E0237
def __repr__(self) -> str:
# We do this here instead of in a __init__ so filter don't have to call __init__ or super()
@@ -146,7 +176,7 @@ class BaseFilter(ABC):
return self.name
class MessageFilter(BaseFilter, ABC):
class MessageFilter(BaseFilter):
"""Base class for all Message Filters. In contrast to :class:`UpdateFilter`, the object passed
to :meth:`filter` is ``update.effective_message``.
@@ -162,6 +192,8 @@ class MessageFilter(BaseFilter, ABC):
"""
__slots__ = ()
def __call__(self, update: Update) -> Optional[Union[bool, DataDict]]:
return self.filter(update.effective_message)
@@ -178,7 +210,7 @@ class MessageFilter(BaseFilter, ABC):
"""
class UpdateFilter(BaseFilter, ABC):
class UpdateFilter(BaseFilter):
"""Base class for all Update Filters. In contrast to :class:`MessageFilter`, the object
passed to :meth:`filter` is ``update``, which allows to create filters like
:attr:`Filters.update.edited_message`.
@@ -195,6 +227,8 @@ class UpdateFilter(BaseFilter, ABC):
"""
__slots__ = ()
def __call__(self, update: Update) -> Optional[Union[bool, DataDict]]:
return self.filter(update)
@@ -219,6 +253,8 @@ class InvertedFilter(UpdateFilter):
"""
__slots__ = ('f',)
def __init__(self, f: BaseFilter):
self.f = f
@@ -244,6 +280,8 @@ class MergedFilter(UpdateFilter):
"""
__slots__ = ('base_filter', 'and_filter', 'or_filter')
def __init__(
self, base_filter: BaseFilter, and_filter: BaseFilter = None, or_filter: BaseFilter = None
):
@@ -328,6 +366,8 @@ class XORFilter(UpdateFilter):
"""
__slots__ = ('base_filter', 'xor_filter', 'merged_filter')
def __init__(self, base_filter: BaseFilter, xor_filter: BaseFilter):
self.base_filter = base_filter
self.xor_filter = xor_filter
@@ -346,11 +386,15 @@ class XORFilter(UpdateFilter):
class _DiceEmoji(MessageFilter):
__slots__ = ('emoji',)
def __init__(self, emoji: str = None, name: str = None):
self.name = f'Filters.dice.{name}' if name else 'Filters.dice'
self.emoji = emoji
class _DiceValues(MessageFilter):
__slots__ = ('values', 'emoji')
def __init__(
self,
values: SLT[int],
@@ -393,7 +437,13 @@ class Filters:
"""
__slots__ = ('__dict__',)
def __setattr__(self, key: str, value: object) -> None:
set_new_attribute_deprecated(self, key, value)
class _All(MessageFilter):
__slots__ = ()
name = 'Filters.all'
def filter(self, message: Message) -> bool:
@@ -403,9 +453,12 @@ class Filters:
"""All Messages."""
class _Text(MessageFilter):
__slots__ = ()
name = 'Filters.text'
class _TextStrings(MessageFilter):
__slots__ = ('strings',)
def __init__(self, strings: Union[List[str], Tuple[str]]):
self.strings = strings
self.name = f'Filters.text({strings})'
@@ -454,9 +507,12 @@ class Filters:
"""
class _Caption(MessageFilter):
__slots__ = ()
name = 'Filters.caption'
class _CaptionStrings(MessageFilter):
__slots__ = ('strings',)
def __init__(self, strings: Union[List[str], Tuple[str]]):
self.strings = strings
self.name = f'Filters.caption({strings})'
@@ -489,9 +545,12 @@ class Filters:
"""
class _Command(MessageFilter):
__slots__ = ()
name = 'Filters.command'
class _CommandOnlyStart(MessageFilter):
__slots__ = ('only_start',)
def __init__(self, only_start: bool):
self.only_start = only_start
self.name = f'Filters.command({only_start})'
@@ -564,6 +623,7 @@ class Filters:
pattern (:obj:`str` | :obj:`Pattern`): The regex pattern.
"""
__slots__ = ('pattern',)
data_filter = True
def __init__(self, pattern: Union[str, Pattern]):
@@ -599,6 +659,7 @@ class Filters:
pattern (:obj:`str` | :obj:`Pattern`): The regex pattern.
"""
__slots__ = ('pattern',)
data_filter = True
def __init__(self, pattern: Union[str, Pattern]):
@@ -617,6 +678,7 @@ class Filters:
return {}
class _Reply(MessageFilter):
__slots__ = ()
name = 'Filters.reply'
def filter(self, message: Message) -> bool:
@@ -626,6 +688,7 @@ class Filters:
"""Messages that are a reply to another message."""
class _Audio(MessageFilter):
__slots__ = ()
name = 'Filters.audio'
def filter(self, message: Message) -> bool:
@@ -635,6 +698,7 @@ class Filters:
"""Messages that contain :class:`telegram.Audio`."""
class _Document(MessageFilter):
__slots__ = ()
name = 'Filters.document'
class category(MessageFilter):
@@ -651,18 +715,21 @@ class Filters:
of audio sent as file, for example 'audio/mpeg' or 'audio/x-wav'.
"""
__slots__ = ('_category',)
def __init__(self, category: Optional[str]):
"""Initialize the category you want to filter
Args:
category (str, optional): category of the media you want to filter"""
self.category = category
self.name = f"Filters.document.category('{self.category}')"
category (str, optional): category of the media you want to filter
"""
self._category = category
self.name = f"Filters.document.category('{self._category}')"
def filter(self, message: Message) -> bool:
"""""" # remove method from docs
if message.document:
return message.document.mime_type.startswith(self.category)
return message.document.mime_type.startswith(self._category)
return False
application = category('application/')
@@ -684,11 +751,9 @@ class Filters:
``Filters.document.mime_type('audio/mpeg')`` filters all audio in mp3 format.
"""
def __init__(self, mimetype: Optional[str]):
"""Initialize the category you want to filter
__slots__ = ('mimetype',)
Args:
mimetype (str, optional): mime_type of the media you want to filter"""
def __init__(self, mimetype: Optional[str]):
self.mimetype = mimetype
self.name = f"Filters.document.mime_type('{self.mimetype}')"
@@ -740,6 +805,8 @@ class Filters:
filters files without a dot in the filename.
"""
__slots__ = ('_file_extension', 'is_case_sensitive')
def __init__(self, file_extension: Optional[str], case_sensitive: bool = False):
"""Initialize the extension you want to filter.
@@ -752,29 +819,29 @@ class Filters:
"""
self.is_case_sensitive = case_sensitive
if file_extension is None:
self.file_extension = None
self._file_extension = None
self.name = "Filters.document.file_extension(None)"
elif case_sensitive:
self.file_extension = f".{file_extension}"
elif self.is_case_sensitive:
self._file_extension = f".{file_extension}"
self.name = (
f"Filters.document.file_extension({file_extension!r},"
" case_sensitive=True)"
)
else:
self.file_extension = f".{file_extension}".lower()
self._file_extension = f".{file_extension}".lower()
self.name = f"Filters.document.file_extension({file_extension.lower()!r})"
def filter(self, message: Message) -> bool:
"""""" # remove method from docs
if message.document is None:
return False
if self.file_extension is None:
if self._file_extension is None:
return "." not in message.document.file_name
if self.is_case_sensitive:
filename = message.document.file_name
else:
filename = message.document.file_name.lower()
return filename.endswith(self.file_extension)
return filename.endswith(self._file_extension)
def filter(self, message: Message) -> bool:
return bool(message.document)
@@ -858,6 +925,7 @@ officedocument.wordprocessingml.document")``.
"""
class _Animation(MessageFilter):
__slots__ = ()
name = 'Filters.animation'
def filter(self, message: Message) -> bool:
@@ -867,6 +935,7 @@ officedocument.wordprocessingml.document")``.
"""Messages that contain :class:`telegram.Animation`."""
class _Photo(MessageFilter):
__slots__ = ()
name = 'Filters.photo'
def filter(self, message: Message) -> bool:
@@ -876,6 +945,7 @@ officedocument.wordprocessingml.document")``.
"""Messages that contain :class:`telegram.PhotoSize`."""
class _Sticker(MessageFilter):
__slots__ = ()
name = 'Filters.sticker'
def filter(self, message: Message) -> bool:
@@ -885,6 +955,7 @@ officedocument.wordprocessingml.document")``.
"""Messages that contain :class:`telegram.Sticker`."""
class _Video(MessageFilter):
__slots__ = ()
name = 'Filters.video'
def filter(self, message: Message) -> bool:
@@ -894,6 +965,7 @@ officedocument.wordprocessingml.document")``.
"""Messages that contain :class:`telegram.Video`."""
class _Voice(MessageFilter):
__slots__ = ()
name = 'Filters.voice'
def filter(self, message: Message) -> bool:
@@ -903,6 +975,7 @@ officedocument.wordprocessingml.document")``.
"""Messages that contain :class:`telegram.Voice`."""
class _VideoNote(MessageFilter):
__slots__ = ()
name = 'Filters.video_note'
def filter(self, message: Message) -> bool:
@@ -912,6 +985,7 @@ officedocument.wordprocessingml.document")``.
"""Messages that contain :class:`telegram.VideoNote`."""
class _Contact(MessageFilter):
__slots__ = ()
name = 'Filters.contact'
def filter(self, message: Message) -> bool:
@@ -921,6 +995,7 @@ officedocument.wordprocessingml.document")``.
"""Messages that contain :class:`telegram.Contact`."""
class _Location(MessageFilter):
__slots__ = ()
name = 'Filters.location'
def filter(self, message: Message) -> bool:
@@ -930,6 +1005,7 @@ officedocument.wordprocessingml.document")``.
"""Messages that contain :class:`telegram.Location`."""
class _Venue(MessageFilter):
__slots__ = ()
name = 'Filters.venue'
def filter(self, message: Message) -> bool:
@@ -947,7 +1023,10 @@ officedocument.wordprocessingml.document")``.
"""
__slots__ = ()
class _NewChatMembers(MessageFilter):
__slots__ = ()
name = 'Filters.status_update.new_chat_members'
def filter(self, message: Message) -> bool:
@@ -957,6 +1036,7 @@ officedocument.wordprocessingml.document")``.
"""Messages that contain :attr:`telegram.Message.new_chat_members`."""
class _LeftChatMember(MessageFilter):
__slots__ = ()
name = 'Filters.status_update.left_chat_member'
def filter(self, message: Message) -> bool:
@@ -966,6 +1046,7 @@ officedocument.wordprocessingml.document")``.
"""Messages that contain :attr:`telegram.Message.left_chat_member`."""
class _NewChatTitle(MessageFilter):
__slots__ = ()
name = 'Filters.status_update.new_chat_title'
def filter(self, message: Message) -> bool:
@@ -975,6 +1056,7 @@ officedocument.wordprocessingml.document")``.
"""Messages that contain :attr:`telegram.Message.new_chat_title`."""
class _NewChatPhoto(MessageFilter):
__slots__ = ()
name = 'Filters.status_update.new_chat_photo'
def filter(self, message: Message) -> bool:
@@ -984,6 +1066,7 @@ officedocument.wordprocessingml.document")``.
"""Messages that contain :attr:`telegram.Message.new_chat_photo`."""
class _DeleteChatPhoto(MessageFilter):
__slots__ = ()
name = 'Filters.status_update.delete_chat_photo'
def filter(self, message: Message) -> bool:
@@ -993,6 +1076,7 @@ officedocument.wordprocessingml.document")``.
"""Messages that contain :attr:`telegram.Message.delete_chat_photo`."""
class _ChatCreated(MessageFilter):
__slots__ = ()
name = 'Filters.status_update.chat_created'
def filter(self, message: Message) -> bool:
@@ -1008,6 +1092,7 @@ officedocument.wordprocessingml.document")``.
:attr: `telegram.Message.channel_chat_created`."""
class _MessageAutoDeleteTimerChanged(MessageFilter):
__slots__ = ()
name = 'MessageAutoDeleteTimerChanged'
def filter(self, message: Message) -> bool:
@@ -1017,6 +1102,7 @@ officedocument.wordprocessingml.document")``.
"""Messages that contain :attr:`message_auto_delete_timer_changed`"""
class _Migrate(MessageFilter):
__slots__ = ()
name = 'Filters.status_update.migrate'
def filter(self, message: Message) -> bool:
@@ -1027,6 +1113,7 @@ officedocument.wordprocessingml.document")``.
:attr:`telegram.Message.migrate_to_chat_id`."""
class _PinnedMessage(MessageFilter):
__slots__ = ()
name = 'Filters.status_update.pinned_message'
def filter(self, message: Message) -> bool:
@@ -1036,6 +1123,7 @@ officedocument.wordprocessingml.document")``.
"""Messages that contain :attr:`telegram.Message.pinned_message`."""
class _ConnectedWebsite(MessageFilter):
__slots__ = ()
name = 'Filters.status_update.connected_website'
def filter(self, message: Message) -> bool:
@@ -1045,6 +1133,7 @@ officedocument.wordprocessingml.document")``.
"""Messages that contain :attr:`telegram.Message.connected_website`."""
class _ProximityAlertTriggered(MessageFilter):
__slots__ = ()
name = 'Filters.status_update.proximity_alert_triggered'
def filter(self, message: Message) -> bool:
@@ -1054,6 +1143,7 @@ officedocument.wordprocessingml.document")``.
"""Messages that contain :attr:`telegram.Message.proximity_alert_triggered`."""
class _VoiceChatScheduled(MessageFilter):
__slots__ = ()
name = 'Filters.status_update.voice_chat_scheduled'
def filter(self, message: Message) -> bool:
@@ -1063,6 +1153,7 @@ officedocument.wordprocessingml.document")``.
"""Messages that contain :attr:`telegram.Message.voice_chat_scheduled`."""
class _VoiceChatStarted(MessageFilter):
__slots__ = ()
name = 'Filters.status_update.voice_chat_started'
def filter(self, message: Message) -> bool:
@@ -1072,6 +1163,7 @@ officedocument.wordprocessingml.document")``.
"""Messages that contain :attr:`telegram.Message.voice_chat_started`."""
class _VoiceChatEnded(MessageFilter):
__slots__ = ()
name = 'Filters.status_update.voice_chat_ended'
def filter(self, message: Message) -> bool:
@@ -1081,6 +1173,7 @@ officedocument.wordprocessingml.document")``.
"""Messages that contain :attr:`telegram.Message.voice_chat_ended`."""
class _VoiceChatParticipantsInvited(MessageFilter):
__slots__ = ()
name = 'Filters.status_update.voice_chat_participants_invited'
def filter(self, message: Message) -> bool:
@@ -1165,6 +1258,7 @@ officedocument.wordprocessingml.document")``.
"""
class _Forwarded(MessageFilter):
__slots__ = ()
name = 'Filters.forwarded'
def filter(self, message: Message) -> bool:
@@ -1174,6 +1268,7 @@ officedocument.wordprocessingml.document")``.
"""Messages that are forwarded."""
class _Game(MessageFilter):
__slots__ = ()
name = 'Filters.game'
def filter(self, message: Message) -> bool:
@@ -1196,6 +1291,8 @@ officedocument.wordprocessingml.document")``.
"""
__slots__ = ('entity_type',)
def __init__(self, entity_type: str):
self.entity_type = entity_type
self.name = f'Filters.entity({self.entity_type})'
@@ -1218,6 +1315,8 @@ officedocument.wordprocessingml.document")``.
"""
__slots__ = ('entity_type',)
def __init__(self, entity_type: str):
self.entity_type = entity_type
self.name = f'Filters.caption_entity({self.entity_type})'
@@ -1227,6 +1326,7 @@ officedocument.wordprocessingml.document")``.
return any(entity.type == self.entity_type for entity in message.caption_entities)
class _Private(MessageFilter):
__slots__ = ()
name = 'Filters.private'
def filter(self, message: Message) -> bool:
@@ -1247,6 +1347,7 @@ officedocument.wordprocessingml.document")``.
"""
class _Group(MessageFilter):
__slots__ = ()
name = 'Filters.group'
def filter(self, message: Message) -> bool:
@@ -1267,9 +1368,11 @@ officedocument.wordprocessingml.document")``.
"""
class _ChatType(MessageFilter):
__slots__ = ()
name = 'Filters.chat_type'
class _Channel(MessageFilter):
__slots__ = ()
name = 'Filters.chat_type.channel'
def filter(self, message: Message) -> bool:
@@ -1278,6 +1381,7 @@ officedocument.wordprocessingml.document")``.
channel = _Channel()
class _Group(MessageFilter):
__slots__ = ()
name = 'Filters.chat_type.group'
def filter(self, message: Message) -> bool:
@@ -1286,6 +1390,7 @@ officedocument.wordprocessingml.document")``.
group = _Group()
class _SuperGroup(MessageFilter):
__slots__ = ()
name = 'Filters.chat_type.supergroup'
def filter(self, message: Message) -> bool:
@@ -1294,6 +1399,7 @@ officedocument.wordprocessingml.document")``.
supergroup = _SuperGroup()
class _Groups(MessageFilter):
__slots__ = ()
name = 'Filters.chat_type.groups'
def filter(self, message: Message) -> bool:
@@ -1302,6 +1408,7 @@ officedocument.wordprocessingml.document")``.
groups = _Groups()
class _Private(MessageFilter):
__slots__ = ()
name = 'Filters.chat_type.private'
def filter(self, message: Message) -> bool:
@@ -1328,7 +1435,16 @@ officedocument.wordprocessingml.document")``.
private: Updates sent in private chat
"""
class _ChatUserBaseFilter(MessageFilter):
class _ChatUserBaseFilter(MessageFilter, ABC):
__slots__ = (
'chat_id_name',
'username_name',
'allow_empty',
'__lock',
'_chat_ids',
'_usernames',
)
def __init__(
self,
chat_id: SLT[int] = None,
@@ -1348,7 +1464,7 @@ officedocument.wordprocessingml.document")``.
@abstractmethod
def get_chat_or_user(self, message: Message) -> Union[Chat, User, None]:
pass
...
@staticmethod
def _parse_chat_id(chat_id: SLT[int]) -> Set[int]:
@@ -1505,6 +1621,8 @@ officedocument.wordprocessingml.document")``.
"""
__slots__ = ()
def __init__(
self,
user_id: SLT[int] = None,
@@ -1604,6 +1722,8 @@ officedocument.wordprocessingml.document")``.
"""
__slots__ = ()
def __init__(
self,
bot_id: SLT[int] = None,
@@ -1703,6 +1823,8 @@ officedocument.wordprocessingml.document")``.
"""
__slots__ = ()
def get_chat_or_user(self, message: Message) -> Optional[Chat]:
return message.chat
@@ -1794,6 +1916,8 @@ officedocument.wordprocessingml.document")``.
is specified in :attr:`chat_ids` and :attr:`usernames`.
"""
__slots__ = ()
def get_chat_or_user(self, message: Message) -> Union[User, Chat, None]:
return message.forward_from or message.forward_from_chat
@@ -1899,6 +2023,8 @@ officedocument.wordprocessingml.document")``.
"""
__slots__ = ()
def get_chat_or_user(self, message: Message) -> Optional[Chat]:
return message.sender_chat
@@ -1945,12 +2071,16 @@ officedocument.wordprocessingml.document")``.
return super().remove_chat_ids(chat_id)
class _SuperGroup(MessageFilter):
__slots__ = ()
def filter(self, message: Message) -> bool:
if message.sender_chat:
return message.sender_chat.type == Chat.SUPERGROUP
return False
class _Channel(MessageFilter):
__slots__ = ()
def filter(self, message: Message) -> bool:
if message.sender_chat:
return message.sender_chat.type == Chat.CHANNEL
@@ -1960,6 +2090,7 @@ officedocument.wordprocessingml.document")``.
channel = _Channel()
class _Invoice(MessageFilter):
__slots__ = ()
name = 'Filters.invoice'
def filter(self, message: Message) -> bool:
@@ -1969,6 +2100,7 @@ officedocument.wordprocessingml.document")``.
"""Messages that contain :class:`telegram.Invoice`."""
class _SuccessfulPayment(MessageFilter):
__slots__ = ()
name = 'Filters.successful_payment'
def filter(self, message: Message) -> bool:
@@ -1978,6 +2110,7 @@ officedocument.wordprocessingml.document")``.
"""Messages that confirm a :class:`telegram.SuccessfulPayment`."""
class _PassportData(MessageFilter):
__slots__ = ()
name = 'Filters.passport_data'
def filter(self, message: Message) -> bool:
@@ -1987,6 +2120,7 @@ officedocument.wordprocessingml.document")``.
"""Messages that contain a :class:`telegram.PassportData`"""
class _Poll(MessageFilter):
__slots__ = ()
name = 'Filters.poll'
def filter(self, message: Message) -> bool:
@@ -1996,6 +2130,7 @@ officedocument.wordprocessingml.document")``.
"""Messages that contain a :class:`telegram.Poll`."""
class _Dice(_DiceEmoji):
__slots__ = ()
dice = _DiceEmoji('🎲', 'dice')
darts = _DiceEmoji('🎯', 'darts')
basketball = _DiceEmoji('🏀', 'basketball')
@@ -2010,10 +2145,15 @@ officedocument.wordprocessingml.document")``.
Examples:
To allow any dice message, simply use
``MessageHandler(Filters.dice, callback_method)``.
To allow only dice with value 6, use
``MessageHandler(Filters.dice(6), callback_method)``.
To allow only dice with value 5 `or` 6, use
``MessageHandler(Filters.dice([5, 6]), callback_method)``.
To allow only dice messages with the emoji 🎲, but any value, use
``MessageHandler(Filters.dice.dice, callback_method)``.
To allow only dice messages with the emoji 🎯 and with value 6, use
``MessageHandler(Filters.dice.darts(6), callback_method)``.
To allow only dice messages with the emoji and with value 5 `or` 6, use
``MessageHandler(Filters.dice.football([5, 6]), callback_method)``.
Note:
Dice messages don't have text. If you want to filter either text or dice messages, use
@@ -2059,6 +2199,8 @@ officedocument.wordprocessingml.document")``.
"""
__slots__ = ('lang',)
def __init__(self, lang: SLT[str]):
if isinstance(lang, str):
lang = cast(str, lang)
@@ -2075,10 +2217,26 @@ officedocument.wordprocessingml.document")``.
and any(message.from_user.language_code.startswith(x) for x in self.lang)
)
class _Attachment(MessageFilter):
__slots__ = ()
name = 'Filters.attachment'
def filter(self, message: Message) -> bool:
return bool(message.effective_attachment)
attachment = _Attachment()
"""Messages that contain :meth:`telegram.Message.effective_attachment`.
.. versionadded:: 13.6"""
class _UpdateType(UpdateFilter):
__slots__ = ()
name = 'Filters.update'
class _Message(UpdateFilter):
__slots__ = ()
name = 'Filters.update.message'
def filter(self, update: Update) -> bool:
@@ -2087,6 +2245,7 @@ officedocument.wordprocessingml.document")``.
message = _Message()
class _EditedMessage(UpdateFilter):
__slots__ = ()
name = 'Filters.update.edited_message'
def filter(self, update: Update) -> bool:
@@ -2095,6 +2254,7 @@ officedocument.wordprocessingml.document")``.
edited_message = _EditedMessage()
class _Messages(UpdateFilter):
__slots__ = ()
name = 'Filters.update.messages'
def filter(self, update: Update) -> bool:
@@ -2103,6 +2263,7 @@ officedocument.wordprocessingml.document")``.
messages = _Messages()
class _ChannelPost(UpdateFilter):
__slots__ = ()
name = 'Filters.update.channel_post'
def filter(self, update: Update) -> bool:
@@ -2111,6 +2272,7 @@ officedocument.wordprocessingml.document")``.
channel_post = _ChannelPost()
class _EditedChannelPost(UpdateFilter):
__slots__ = ()
name = 'Filters.update.edited_channel_post'
def filter(self, update: Update) -> bool:
@@ -2119,6 +2281,7 @@ officedocument.wordprocessingml.document")``.
edited_channel_post = _EditedChannelPost()
class _ChannelPosts(UpdateFilter):
__slots__ = ()
name = 'Filters.update.channel_posts'
def filter(self, update: Update) -> bool:
+47 -9
View File
@@ -17,22 +17,25 @@
# 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 base class for handlers as used by the Dispatcher."""
from abc import ABC, abstractmethod
from typing import TYPE_CHECKING, Any, Callable, Dict, Optional, TypeVar, Union, Generic
from sys import version_info as py_ver
from telegram.utils.deprecate import set_new_attribute_deprecated
from telegram import Update
from telegram.ext.utils.promise import Promise
from telegram.utils.helpers import DefaultValue, DEFAULT_FALSE
from telegram.ext.utils.types import CCT
if TYPE_CHECKING:
from telegram.ext import CallbackContext, Dispatcher
from telegram.ext import Dispatcher
RT = TypeVar('RT')
UT = TypeVar('UT')
class Handler(Generic[UT], ABC):
class Handler(Generic[UT, CCT], ABC):
"""The base class for all update handlers. Create custom handlers by inheriting from it.
Note:
@@ -90,9 +93,30 @@ class Handler(Generic[UT], ABC):
"""
# Apparently Py 3.7 and below have '__dict__' in ABC
if py_ver < (3, 7):
__slots__ = (
'callback',
'pass_update_queue',
'pass_job_queue',
'pass_user_data',
'pass_chat_data',
'run_async',
)
else:
__slots__ = (
'callback', # type: ignore[assignment]
'pass_update_queue',
'pass_job_queue',
'pass_user_data',
'pass_chat_data',
'run_async',
'__dict__',
)
def __init__(
self,
callback: Callable[[UT, 'CallbackContext'], RT],
callback: Callable[[UT, CCT], RT],
pass_update_queue: bool = False,
pass_job_queue: bool = False,
pass_user_data: bool = False,
@@ -106,6 +130,17 @@ class Handler(Generic[UT], ABC):
self.pass_chat_data = pass_chat_data
self.run_async = run_async
def __setattr__(self, key: str, value: object) -> None:
# See comment on BaseFilter to know why this was done.
if key.startswith('__'):
key = f"_{self.__class__.__name__}{key}"
if issubclass(self.__class__, Handler) and not self.__class__.__module__.startswith(
'telegram.ext.'
):
object.__setattr__(self, key, value)
return
set_new_attribute_deprecated(self, key, value)
@abstractmethod
def check_update(self, update: object) -> Optional[Union[bool, object]]:
"""
@@ -131,7 +166,7 @@ class Handler(Generic[UT], ABC):
update: UT,
dispatcher: 'Dispatcher',
check_result: object,
context: 'CallbackContext' = None,
context: CCT = None,
) -> Union[RT, Promise]:
"""
This method is called if it was determined that an update should indeed
@@ -149,9 +184,12 @@ class Handler(Generic[UT], ABC):
"""
run_async = self.run_async
if self.run_async is DEFAULT_FALSE and dispatcher.bot.defaults:
if dispatcher.bot.defaults.run_async:
run_async = True
if (
self.run_async is DEFAULT_FALSE
and dispatcher.bot.defaults
and dispatcher.bot.defaults.run_async
):
run_async = True
if context:
self.collect_additional_context(context, update, dispatcher, check_result)
@@ -168,7 +206,7 @@ class Handler(Generic[UT], ABC):
def collect_additional_context(
self,
context: 'CallbackContext',
context: CCT,
update: UT,
dispatcher: 'Dispatcher',
check_result: Any,
+15 -6
View File
@@ -16,7 +16,7 @@
#
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
""" This module contains the InlineQueryHandler class """
"""This module contains the InlineQueryHandler class."""
import re
from typing import (
TYPE_CHECKING,
@@ -35,14 +35,15 @@ from telegram import Update
from telegram.utils.helpers import DefaultValue, DEFAULT_FALSE
from .handler import Handler
from .utils.types import CCT
if TYPE_CHECKING:
from telegram.ext import CallbackContext, Dispatcher
from telegram.ext import Dispatcher
RT = TypeVar('RT')
class InlineQueryHandler(Handler[Update]):
class InlineQueryHandler(Handler[Update, CCT]):
"""
Handler class to handle Telegram inline queries. Optionally based on a regex. Read the
documentation of the ``re`` module for more information.
@@ -129,9 +130,11 @@ class InlineQueryHandler(Handler[Update]):
"""
__slots__ = ('pattern', 'chat_types', 'pass_groups', 'pass_groupdict')
def __init__(
self,
callback: Callable[[Update, 'CallbackContext'], RT],
callback: Callable[[Update, CCT], RT],
pass_update_queue: bool = False,
pass_job_queue: bool = False,
pattern: Union[str, Pattern] = None,
@@ -170,7 +173,6 @@ class InlineQueryHandler(Handler[Update]):
:obj:`bool`
"""
if isinstance(update, Update) and update.inline_query:
if (self.chat_types is not None) and (
update.inline_query.chat_type not in self.chat_types
@@ -191,6 +193,10 @@ class InlineQueryHandler(Handler[Update]):
update: Update = None,
check_result: Optional[Union[bool, Match]] = None,
) -> Dict[str, object]:
"""Pass the results of ``re.match(pattern, query).{groups(), groupdict()}`` to the
callback as a keyword arguments called ``groups`` and ``groupdict``, respectively, if
needed.
"""
optional_args = super().collect_optional_args(dispatcher, update, check_result)
if self.pattern:
check_result = cast(Match, check_result)
@@ -202,11 +208,14 @@ class InlineQueryHandler(Handler[Update]):
def collect_additional_context(
self,
context: 'CallbackContext',
context: CCT,
update: Update,
dispatcher: 'Dispatcher',
check_result: Optional[Union[bool, Match]],
) -> None:
"""Add the result of ``re.match(pattern, update.inline_query.query)`` to
:attr:`CallbackContext.matches` as list with one element.
"""
if self.pattern:
check_result = cast(Match, check_result)
context.matches = [check_result]
+49 -29
View File
@@ -31,6 +31,7 @@ from apscheduler.job import Job as APSJob
from telegram.ext.callbackcontext import CallbackContext
from telegram.utils.types import JSONDict
from telegram.utils.deprecate import set_new_attribute_deprecated
if TYPE_CHECKING:
from telegram import Bot
@@ -38,11 +39,6 @@ if TYPE_CHECKING:
import apscheduler.job # noqa: F401
class Days:
MON, TUE, WED, THU, FRI, SAT, SUN = range(7)
EVERY_DAY = tuple(range(7))
class JobQueue:
"""This class allows you to periodically perform tasks with the bot. It is a convenience
wrapper for the APScheduler library.
@@ -54,6 +50,8 @@ class JobQueue:
"""
__slots__ = ('_dispatcher', 'logger', 'scheduler', '__dict__')
def __init__(self) -> None:
self._dispatcher: 'Dispatcher' = None # type: ignore[assignment]
self.logger = logging.getLogger(self.__class__.__name__)
@@ -69,15 +67,18 @@ class JobQueue:
logging.getLogger('apscheduler.executors.default').addFilter(aps_log_filter)
self.scheduler.add_listener(self._dispatch_error, EVENT_JOB_ERROR)
def __setattr__(self, key: str, value: object) -> None:
set_new_attribute_deprecated(self, key, value)
def _build_args(self, job: 'Job') -> List[Union[CallbackContext, 'Bot', 'Job']]:
if self._dispatcher.use_context:
return [CallbackContext.from_job(job, self._dispatcher)]
return [self._dispatcher.context_types.context.from_job(job, self._dispatcher)]
return [self._dispatcher.bot, job]
def _tz_now(self) -> datetime.datetime:
return datetime.datetime.now(self.scheduler.timezone)
def _update_persistence(self, event: JobEvent) -> None: # pylint: disable=W0613
def _update_persistence(self, _: JobEvent) -> None:
self._dispatcher.update_persistence()
def _dispatch_error(self, event: JobEvent) -> None:
@@ -136,8 +137,7 @@ class JobQueue:
"""
self._dispatcher = dispatcher
if dispatcher.bot.defaults:
if dispatcher.bot.defaults:
self.scheduler.configure(timezone=dispatcher.bot.defaults.tzinfo or pytz.utc)
self.scheduler.configure(timezone=dispatcher.bot.defaults.tzinfo or pytz.utc)
def run_once(
self,
@@ -218,6 +218,12 @@ class JobQueue:
) -> 'Job':
"""Creates a new ``Job`` that runs at specified intervals and adds it to the queue.
Note:
For a note about DST, please see the documentation of `APScheduler`_.
.. _`APScheduler`: https://apscheduler.readthedocs.io/en/stable/modules/triggers/cron.html
#daylight-saving-time-behavior
Args:
callback (:obj:`callable`): The callback function that should be executed by the new
job. Callback signature for context based API:
@@ -268,11 +274,6 @@ class JobQueue:
:class:`telegram.ext.Job`: The new ``Job`` instance that has been added to the job
queue.
Note:
`interval` is always respected "as-is". That means that if DST changes during that
interval, the job might not run at the time one would expect. It is always recommended
to pin servers to UTC time, then time related behaviour can always be expected.
"""
if not job_kwargs:
job_kwargs = {}
@@ -392,13 +393,19 @@ class JobQueue:
self,
callback: Callable[['CallbackContext'], None],
time: datetime.time,
days: Tuple[int, ...] = Days.EVERY_DAY,
days: Tuple[int, ...] = tuple(range(7)),
context: object = None,
name: str = None,
job_kwargs: JSONDict = None,
) -> 'Job':
"""Creates a new ``Job`` that runs on a daily basis and adds it to the queue.
Note:
For a note about DST, please see the documentation of `APScheduler`_.
.. _`APScheduler`: https://apscheduler.readthedocs.io/en/stable/modules/triggers/cron.html
#daylight-saving-time-behavior
Args:
callback (:obj:`callable`): The callback function that should be executed by the new
job. Callback signature for context based API:
@@ -422,12 +429,6 @@ class JobQueue:
:class:`telegram.ext.Job`: The new ``Job`` instance that has been added to the job
queue.
Note:
For a note about DST, please see the documentation of `APScheduler`_.
.. _`APScheduler`: https://apscheduler.readthedocs.io/en/stable/modules/triggers/cron.html
#daylight-saving-time-behavior
"""
if not job_kwargs:
job_kwargs = {}
@@ -499,14 +500,16 @@ class JobQueue:
self.scheduler.shutdown()
def jobs(self) -> Tuple['Job', ...]:
"""
Returns a tuple of all *pending/scheduled* jobs that are currently in the ``JobQueue``.
"""
return tuple(Job.from_aps_job(job, self) for job in self.scheduler.get_jobs())
"""Returns a tuple of all *scheduled* jobs that are currently in the ``JobQueue``."""
return tuple(
Job._from_aps_job(job, self) # pylint: disable=W0212
for job in self.scheduler.get_jobs()
)
def get_jobs_by_name(self, name: str) -> Tuple['Job', ...]:
"""Returns a tuple of all *pending/scheduled* jobs with the given name that are currently
in the ``JobQueue``"""
in the ``JobQueue``.
"""
return tuple(job for job in self.jobs() if job.name == name)
@@ -515,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.
@@ -546,6 +552,17 @@ class Job:
job (:class:`apscheduler.job.Job`): Optional. The APS Job this job is a wrapper for.
"""
__slots__ = (
'callback',
'context',
'name',
'job_queue',
'_removed',
'_enabled',
'job',
'__dict__',
)
def __init__(
self,
callback: Callable[['CallbackContext'], None],
@@ -563,13 +580,16 @@ class Job:
self._removed = False
self._enabled = False
self.job = cast(APSJob, job)
self.job = cast(APSJob, job) # skipcq: PTC-W0052
def __setattr__(self, key: str, value: object) -> None:
set_new_attribute_deprecated(self, key, value)
def run(self, dispatcher: 'Dispatcher') -> None:
"""Executes the callback function independently of the jobs schedule."""
try:
if dispatcher.use_context:
self.callback(CallbackContext.from_job(self, dispatcher))
self.callback(dispatcher.context_types.context.from_job(self, dispatcher))
else:
self.callback(dispatcher.bot, self) # type: ignore[arg-type,call-arg]
except Exception as exc:
@@ -619,7 +639,7 @@ class Job:
return self.job.next_run_time
@classmethod
def from_aps_job(cls, job: APSJob, job_queue: JobQueue) -> 'Job':
def _from_aps_job(cls, job: APSJob, job_queue: JobQueue) -> 'Job':
# context based callbacks
if len(job.args) == 1:
context = job.args[0].job.context

Some files were not shown because too many files have changed in this diff Show More