mirror of
https://github.com/python-telegram-bot/python-telegram-bot.git
synced 2026-06-22 17:34:11 +00:00
Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2642ecc737 | |||
| abfcf72a56 | |||
| 0e044804d2 | |||
| 5b9afd5329 | |||
| a983a89964 | |||
| 741a50ab97 | |||
| cf6c298b82 | |||
| 90c0fe948b | |||
| 2c84122654 | |||
| 143db5fc9d | |||
| c28ad86214 | |||
| 15f153474a | |||
| 55d66a9ea3 | |||
| 14c86daf23 | |||
| 460aaf8bb6 | |||
| 1e703a0be5 | |||
| 142e3c0177 | |||
| d4b7a2b3e9 | |||
| dac6d03666 | |||
| 3bfd58dfd9 | |||
| 2d6459b290 | |||
| 1f0f6a8d3d | |||
| 2ecb8d5413 | |||
| f1d03393de |
@@ -26,7 +26,7 @@ Setting things up
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ pip install -r requirements.txt -r requirements-dev.txt
|
||||
$ pip install -r requirements-all.txt
|
||||
|
||||
|
||||
5. Install pre-commit hooks:
|
||||
@@ -82,7 +82,7 @@ Here's how to make a one-off code change.
|
||||
|
||||
- Documenting types of global variables and complex types of class members can be done using the Sphinx docstring convention.
|
||||
|
||||
- In addition, PTB uses the `Black`_ coder formatting. Plugins for Black exist for some `popular editors`_. You can use those instead of manually formatting everything.
|
||||
- In addition, PTB uses some formatting/styling and linting tools in the pre-commit setup. Some of those tools also have command line tools that can help to run these tools outside of the pre-commit step. If you'd like to leverage that, please have a look at the `pre-commit config file`_ for an overview of which tools (and which versions of them) are used. For example, we use `Black`_ for code formatting. Plugins for Black exist for some `popular editors`_. You can use those instead of manually formatting everything.
|
||||
|
||||
- Please ensure that the code you write is well-tested.
|
||||
|
||||
@@ -273,6 +273,7 @@ break the API classes. For example:
|
||||
.. _AUTHORS.rst: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/AUTHORS.rst
|
||||
.. _`MyPy`: https://mypy.readthedocs.io/en/stable/index.html
|
||||
.. _`here`: https://mypy.readthedocs.io/en/stable/cheat_sheet_py3.html
|
||||
.. _`pre-commit config file`: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/.pre-commit-config.yaml
|
||||
.. _`Black`: https://black.readthedocs.io/en/stable/index.html
|
||||
.. _`popular editors`: https://black.readthedocs.io/en/stable/integrations/editors.html
|
||||
.. _`RTD`: https://docs.python-telegram-bot.org/
|
||||
|
||||
@@ -16,14 +16,12 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v3
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -W ignore -m pip install --upgrade pip
|
||||
python -W ignore -m pip install -r requirements.txt
|
||||
python -W ignore -m pip install -r requirements-dev.txt
|
||||
python -W ignore -m pip install -r docs/requirements-docs.txt
|
||||
python -W ignore -m pip install -r requirements-all.txt
|
||||
- name: Check Links
|
||||
run: sphinx-build docs/source docs/build/html -W --keep-going -j auto -b linkcheck
|
||||
|
||||
@@ -21,14 +21,12 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v3
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -W ignore -m pip install --upgrade pip
|
||||
python -W ignore -m pip install -r requirements.txt
|
||||
python -W ignore -m pip install -r requirements-dev.txt
|
||||
python -W ignore -m pip install -r docs/requirements-docs.txt
|
||||
python -W ignore -m pip install -r requirements-all.txt
|
||||
- name: Build docs
|
||||
run: sphinx-build docs/source docs/build/html -W --keep-going -j auto
|
||||
|
||||
@@ -3,7 +3,7 @@ on:
|
||||
pull_request_target:
|
||||
paths:
|
||||
- requirements.txt
|
||||
- requirements-dev.txt
|
||||
- requirements-opts.txt
|
||||
- .pre-commit-config.yaml
|
||||
permissions:
|
||||
pull-requests: write
|
||||
@@ -15,5 +15,5 @@ jobs:
|
||||
- name: running the check
|
||||
uses: Poolitzer/notifier-action@master
|
||||
with:
|
||||
notify-message: Hey! Looks like you edited the (dev) requirements or the pre-commit hooks. I'm just a friendly reminder to keep the pre-commit hook versions in sync with the dev requirements and the additional dependencies for the hooks in sync with the requirements :)
|
||||
notify-message: Hey! Looks like you edited the (optional) requirements or the pre-commit hooks. I'm just a friendly reminder to keep the additional dependencies for the hooks in sync with the requirements :)
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -4,7 +4,7 @@ on:
|
||||
branches:
|
||||
- master
|
||||
push:
|
||||
branches:
|
||||
branches:
|
||||
- master
|
||||
schedule:
|
||||
# Run monday and friday morning at 03:07 - odd time to spread load on GitHub Actions
|
||||
@@ -14,15 +14,27 @@ jobs:
|
||||
pytest:
|
||||
name: pytest
|
||||
runs-on: ${{matrix.os}}
|
||||
continue-on-error: ${{ matrix.experimental }}
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ['3.7', '3.8', '3.9', '3.10']
|
||||
os: [ubuntu-latest, windows-latest, macos-latest]
|
||||
experimental: [false]
|
||||
include:
|
||||
- python-version: 3.11.0-rc.1
|
||||
os: ubuntu-latest
|
||||
experimental: true
|
||||
- python-version: 3.11.0-rc.1
|
||||
os: windows-latest
|
||||
experimental: true
|
||||
- python-version: 3.11.0-rc.1
|
||||
os: macos-latest
|
||||
experimental: true
|
||||
fail-fast: False
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v3
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: Install dependencies
|
||||
@@ -30,32 +42,41 @@ jobs:
|
||||
python -W ignore -m pip install --upgrade pip
|
||||
python -W ignore -m pip install -U codecov pytest-cov
|
||||
python -W ignore -m pip install -r requirements.txt
|
||||
python -W ignore -m pip install -r requirements-opts.txt
|
||||
python -W ignore -m pip install -r requirements-dev.txt
|
||||
|
||||
- name: Test with pytest
|
||||
# We run 3 different suites here
|
||||
# We run 4 different suites here
|
||||
# 1. Test just utils.datetime.py without pytz being installed
|
||||
# 2. Test just test_no_passport.py without passport dependencies being installed
|
||||
# 3. Test everything else
|
||||
# 3. Test just test_rate_limiter.py without passport dependencies being installed
|
||||
# 4. Test everything else
|
||||
# The first & second one are achieved by mocking the corresponding import
|
||||
# See test_helpers.py & test_no_passport.py for details
|
||||
run: |
|
||||
pytest -v --cov -k test_no_passport.py
|
||||
no_passport_exit=$?
|
||||
export TEST_NO_PASSPORT='false'
|
||||
export TEST_PASSPORT='true'
|
||||
pytest -v --cov --cov-append -k test_helpers.py
|
||||
no_pytz_exit=$?
|
||||
export TEST_NO_PYTZ='false'
|
||||
export TEST_PYTZ='true'
|
||||
pip uninstall aiolimiter -y
|
||||
pytest -v --cov --cov-append -k test_ratelimiter.py
|
||||
no_rate_limiter_exit=$?
|
||||
export TEST_RATE_LIMITER='true'
|
||||
pip install -r requirements-opts.txt
|
||||
pytest -v --cov --cov-append
|
||||
full_exit=$?
|
||||
special_exit=$(( no_pytz_exit > no_passport_exit ? no_pytz_exit : no_passport_exit ))
|
||||
special_exit=$(( special_exit > no_rate_limiter_exit ? special_exit : no_rate_limiter_exit ))
|
||||
global_exit=$(( special_exit > full_exit ? special_exit : full_exit ))
|
||||
exit ${global_exit}
|
||||
env:
|
||||
JOB_INDEX: ${{ strategy.job-index }}
|
||||
BOTS: W3sidG9rZW4iOiAiNjk2MTg4NzMyOkFBR1Z3RUtmSEhsTmpzY3hFRE5LQXdraEdzdFpfa28xbUMwIiwgInBheW1lbnRfcHJvdmlkZXJfdG9rZW4iOiAiMjg0Njg1MDYzOlRFU1Q6WldGaU1UUmxNbVF5TnpNeSIsICJib3RfbmFtZSI6ICJQVEIgdGVzdHMgb24gVHJhdmlzIHVzaW5nIENQeXRob24gMi43IiwgInN1cGVyX2dyb3VwX2lkIjogIi0xMDAxMzkwOTgzOTk3IiwgImJvdF91c2VybmFtZSI6ICJAcHRiX3RyYXZpc19jcHl0aG9uXzI3X2JvdCJ9LCB7InRva2VuIjogIjY3MTQ2ODg4NjpBQUdQR2ZjaVJJQlVORmU4MjR1SVZkcTdKZTNfWW5BVE5HdyIsICJwYXltZW50X3Byb3ZpZGVyX3Rva2VuIjogIjI4NDY4NTA2MzpURVNUOlpHWXdPVGxrTXpNeE4yWTIiLCAiYm90X25hbWUiOiAiUFRCIHRlc3RzIG9uIFRyYXZpcyB1c2luZyBDUHl0aG9uIDMuNCIsICJzdXBlcl9ncm91cF9pZCI6ICItMTAwMTQ0NjAyMjUyMiIsICJib3RfdXNlcm5hbWUiOiAiQHB0Yl90cmF2aXNfY3B5dGhvbl8zNF9ib3QifSwgeyJ0b2tlbiI6ICI2MjkzMjY1Mzg6QUFGUnJaSnJCN29CM211ekdzR0pYVXZHRTVDUXpNNUNVNG8iLCAicGF5bWVudF9wcm92aWRlcl90b2tlbiI6ICIyODQ2ODUwNjM6VEVTVDpNbU01WVdKaFl6a3hNMlUxIiwgImJvdF9uYW1lIjogIlBUQiB0ZXN0cyBvbiBUcmF2aXMgdXNpbmcgQ1B5dGhvbiAzLjUiLCAic3VwZXJfZ3JvdXBfaWQiOiAiLTEwMDE0OTY5MTc3NTAiLCAiYm90X3VzZXJuYW1lIjogIkBwdGJfdHJhdmlzX2NweXRob25fMzVfYm90In0sIHsidG9rZW4iOiAiNjQwMjA4OTQzOkFBRmhCalFwOXFtM1JUeFN6VXBZekJRakNsZS1Kano1aGNrIiwgInBheW1lbnRfcHJvdmlkZXJfdG9rZW4iOiAiMjg0Njg1MDYzOlRFU1Q6WXpoa1pUZzFOamMxWXpWbCIsICJib3RfbmFtZSI6ICJQVEIgdGVzdHMgb24gVHJhdmlzIHVzaW5nIENQeXRob24gMy42IiwgInN1cGVyX2dyb3VwX2lkIjogIi0xMDAxMzMzODcxNDYxIiwgImJvdF91c2VybmFtZSI6ICJAcHRiX3RyYXZpc19jcHl0aG9uXzM2X2JvdCJ9LCB7InRva2VuIjogIjY5NTEwNDA4ODpBQUhmenlsSU9qU0lJUy1lT25JMjB5MkUyMEhvZEhzZnotMCIsICJwYXltZW50X3Byb3ZpZGVyX3Rva2VuIjogIjI4NDY4NTA2MzpURVNUOk9HUTFNRGd3WmpJd1pqRmwiLCAiYm90X25hbWUiOiAiUFRCIHRlc3RzIG9uIFRyYXZpcyB1c2luZyBDUHl0aG9uIDMuNyIsICJzdXBlcl9ncm91cF9pZCI6ICItMTAwMTQ3ODI5MzcxNCIsICJib3RfdXNlcm5hbWUiOiAiQHB0Yl90cmF2aXNfY3B5dGhvbl8zN19ib3QifSwgeyJ0b2tlbiI6ICI2OTE0MjM1NTQ6QUFGOFdrakNaYm5IcVBfaTZHaFRZaXJGRWxackdhWU9oWDAiLCAicGF5bWVudF9wcm92aWRlcl90b2tlbiI6ICIyODQ2ODUwNjM6VEVTVDpZamM1TlRoaU1tUXlNV1ZoIiwgImJvdF9uYW1lIjogIlBUQiB0ZXN0cyBvbiBUcmF2aXMgdXNpbmcgUHlQeSAyLjciLCAic3VwZXJfZ3JvdXBfaWQiOiAiLTEwMDEzNjM5MzI1NzMiLCAiYm90X3VzZXJuYW1lIjogIkBwdGJfdHJhdmlzX3B5cHlfMjdfYm90In0sIHsidG9rZW4iOiAiNjg0MzM5OTg0OkFBRk1nRUVqcDAxcjVyQjAwN3lDZFZOc2c4QWxOc2FVLWNjIiwgInBheW1lbnRfcHJvdmlkZXJfdG9rZW4iOiAiMjg0Njg1MDYzOlRFU1Q6TVRBek1UWTNNR1V5TmpnMCIsICJib3RfbmFtZSI6ICJQVEIgdGVzdHMgb24gVHJhdmlzIHVzaW5nIFB5UHkgMy41IiwgInN1cGVyX2dyb3VwX2lkIjogIi0xMDAxNDA3ODM2NjA1IiwgImJvdF91c2VybmFtZSI6ICJAcHRiX3RyYXZpc19weXB5XzM1X2JvdCJ9LCB7InRva2VuIjogIjY5MDA5MTM0NzpBQUZMbVI1cEFCNVljcGVfbU9oN3pNNEpGQk9oMHozVDBUbyIsICJwYXltZW50X3Byb3ZpZGVyX3Rva2VuIjogIjI4NDY4NTA2MzpURVNUOlpEaGxOekU1TURrd1lXSmkiLCAiYm90X25hbWUiOiAiUFRCIHRlc3RzIG9uIEFwcFZleW9yIHVzaW5nIENQeXRob24gMy40IiwgInN1cGVyX2dyb3VwX2lkIjogIi0xMDAxMjc5NjAwMDI2IiwgImJvdF91c2VybmFtZSI6ICJAcHRiX2FwcHZleW9yX2NweXRob25fMzRfYm90In0sIHsidG9rZW4iOiAiNjk0MzA4MDUyOkFBRUIyX3NvbkNrNTVMWTlCRzlBTy1IOGp4aVBTNTVvb0JBIiwgInBheW1lbnRfcHJvdmlkZXJfdG9rZW4iOiAiMjg0Njg1MDYzOlRFU1Q6WW1aaVlXWm1NakpoWkdNeSIsICJib3RfbmFtZSI6ICJQVEIgdGVzdHMgb24gQXBwVmV5b3IgdXNpbmcgQ1B5dGhvbiAyLjciLCAic3VwZXJfZ3JvdXBfaWQiOiAiLTEwMDEyOTMwNzkxNjUiLCAiYm90X3VzZXJuYW1lIjogIkBwdGJfYXBwdmV5b3JfY3B5dGhvbl8yN19ib3QifSwgeyJ0b2tlbiI6ICIxMDU1Mzk3NDcxOkFBRzE4bkJfUzJXQXd1SjNnN29oS0JWZ1hYY2VNbklPeVNjIiwgInBheW1lbnRfcHJvdmlkZXJfdG9rZW4iOiAiMjg0Njg1MDYzOlRFU1Q6TmpBd056QXpZalZpTkdOayIsICJuYW1lIjogIlBUQiB0ZXN0cyBbMF0iLCAic3VwZXJfZ3JvdXBfaWQiOiAiLTEwMDExODU1MDk2MzYiLCAidXNlcm5hbWUiOiAicHRiXzBfYm90In0sIHsidG9rZW4iOiAiMTA0NzMyNjc3MTpBQUY4bk90ODFGcFg4bGJidno4VWV3UVF2UmZUYkZmQnZ1SSIsICJwYXltZW50X3Byb3ZpZGVyX3Rva2VuIjogIjI4NDY4NTA2MzpURVNUOllUVTFOVEk0WkdSallqbGkiLCAibmFtZSI6ICJQVEIgdGVzdHMgWzFdIiwgInN1cGVyX2dyb3VwX2lkIjogIi0xMDAxNDg0Nzk3NjEyIiwgInVzZXJuYW1lIjogInB0Yl8xX2JvdCJ9LCB7InRva2VuIjogIjk3MTk5Mjc0NTpBQUdPa09hVzBOSGpnSXY1LTlqUWJPajR2R3FkaFNGLVV1cyIsICJwYXltZW50X3Byb3ZpZGVyX3Rva2VuIjogIjI4NDY4NTA2MzpURVNUOk5XWmtNV1ZoWWpsallqVTUiLCAibmFtZSI6ICJQVEIgdGVzdHMgWzJdIiwgInN1cGVyX2dyb3VwX2lkIjogIi0xMDAxNDAyMjU1MDcwIiwgInVzZXJuYW1lIjogInB0Yl8yX2JvdCJ9XQ==
|
||||
TEST_NO_PYTZ : "true"
|
||||
TEST_NO_PASSPORT: "true"
|
||||
TEST_PYTZ : "false"
|
||||
TEST_PASSPORT: "false"
|
||||
TEST_RATE_LIMITER: "false"
|
||||
TEST_BUILD: "true"
|
||||
shell: bash --noprofile --norc {0}
|
||||
|
||||
@@ -76,13 +97,14 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v3
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -W ignore -m pip install --upgrade pip
|
||||
python -W ignore -m pip install -r requirements.txt
|
||||
python -W ignore -m pip install -r requirements-opts.txt
|
||||
python -W ignore -m pip install -r requirements-dev.txt
|
||||
- name: Compare to official api
|
||||
run: |
|
||||
|
||||
+11
-12
@@ -1,6 +1,4 @@
|
||||
# Make sure that
|
||||
# * the revs specified here match requirements-dev.txt
|
||||
# * the additional_dependencies here match requirements.txt
|
||||
# Make sure that the additional_dependencies here match requirements.txt
|
||||
|
||||
ci:
|
||||
autofix_prs: false
|
||||
@@ -11,14 +9,14 @@ ci:
|
||||
|
||||
repos:
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 22.3.0
|
||||
rev: 22.6.0
|
||||
hooks:
|
||||
- id: black
|
||||
args:
|
||||
- --diff
|
||||
- --check
|
||||
- repo: https://gitlab.com/pycqa/flake8
|
||||
rev: 4.0.1
|
||||
- repo: https://github.com/PyCQA/flake8
|
||||
rev: 5.0.2
|
||||
hooks:
|
||||
- id: flake8
|
||||
- repo: https://github.com/PyCQA/pylint
|
||||
@@ -34,25 +32,26 @@ repos:
|
||||
|
||||
additional_dependencies:
|
||||
- httpx~=0.23.0
|
||||
- tornado~=6.1
|
||||
- tornado~=6.2
|
||||
- APScheduler~=3.9.1
|
||||
- cachetools~=5.2.0
|
||||
- aiolimiter~=1.0.0
|
||||
- . # this basically does `pip install -e .`
|
||||
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||
rev: v0.961
|
||||
rev: v0.971
|
||||
hooks:
|
||||
- id: mypy
|
||||
name: mypy-ptb
|
||||
files: ^telegram/.*\.py$
|
||||
additional_dependencies:
|
||||
- types-ujson
|
||||
- types-pytz
|
||||
- types-cryptography
|
||||
- types-cachetools
|
||||
- httpx~=0.23.0
|
||||
- tornado~=6.1
|
||||
- tornado~=6.2
|
||||
- APScheduler~=3.9.1
|
||||
- cachetools~=5.2.0
|
||||
- aiolimiter~=1.0.0
|
||||
- . # this basically does `pip install -e .`
|
||||
- id: mypy
|
||||
name: mypy-examples
|
||||
@@ -61,12 +60,12 @@ repos:
|
||||
- --no-strict-optional
|
||||
- --follow-imports=silent
|
||||
additional_dependencies:
|
||||
- tornado~=6.1
|
||||
- tornado~=6.2
|
||||
- APScheduler~=3.9.1
|
||||
- cachetools~=5.2.0
|
||||
- . # this basically does `pip install -e .`
|
||||
- repo: https://github.com/asottile/pyupgrade
|
||||
rev: v2.34.0
|
||||
rev: v2.37.3
|
||||
hooks:
|
||||
- id: pyupgrade
|
||||
files: ^(telegram|examples|tests)/.*\.py$
|
||||
|
||||
@@ -86,6 +86,7 @@ The following wonderful people contributed directly or indirectly to this projec
|
||||
- `Paradox <https://github.com/paradox70>`_
|
||||
- `Patrick Hofmann <https://github.com/PH89>`_
|
||||
- `Paul Larsen <https://github.com/PaulSonOfLars>`_
|
||||
- `Pawan <https://github.com/pawanrai9999>`_
|
||||
- `Pieter Schutz <https://github.com/eldinnie>`_
|
||||
- `Piraty <https://github.com/piraty>`_
|
||||
- `Poolitzer <https://github.com/Poolitzer>`_
|
||||
|
||||
+84
@@ -2,6 +2,90 @@
|
||||
Changelog
|
||||
=========
|
||||
|
||||
Version 20.0a4
|
||||
==============
|
||||
*Released 2022-08-27*
|
||||
|
||||
This is the technical changelog for version 20.0a4. More elaborate release notes can be found in the news channel `@pythontelegrambotchannel <https://t.me/pythontelegrambotchannel>`_.
|
||||
|
||||
Hot Fixes
|
||||
---------
|
||||
|
||||
* Fix a Bug in ``setup.py`` Regarding Optional Dependencies (`#3209`_)
|
||||
|
||||
.. _`#3209`: https://github.com/python-telegram-bot/python-telegram-bot/pull/3209
|
||||
|
||||
Version 20.0a3
|
||||
==============
|
||||
*Released 2022-08-27*
|
||||
|
||||
This is the technical changelog for version 20.0a3. More elaborate release notes can be found in the news channel `@pythontelegrambotchannel <https://t.me/pythontelegrambotchannel>`_.
|
||||
|
||||
Major Changes
|
||||
-------------
|
||||
|
||||
- Full Support for API 6.2 (`#3195`_)
|
||||
|
||||
New Features
|
||||
------------
|
||||
|
||||
- New Rate Limiting Mechanism (`#3148`_)
|
||||
- Make ``chat/user_data`` Available in Error Handler for Errors in Jobs (`#3152`_)
|
||||
- Add ``Application.post_shutdown`` (`#3126`_)
|
||||
|
||||
Bug Fixes
|
||||
---------
|
||||
|
||||
- Fix ``helpers.mention_markdown`` for Markdown V1 and Improve Related Unit Tests (`#3155`_)
|
||||
- Add ``api_kwargs`` Parameter to ``Bot.log_out`` and Improve Related Unit Tests (`#3147`_)
|
||||
- Make ``Bot.delete_my_commands`` a Coroutine Function (`#3136`_)
|
||||
- Fix ``ConversationHandler.check_update`` not respecting ``per_user`` (`#3128`_)
|
||||
|
||||
Minor Changes, Documentation Improvements and CI
|
||||
------------------------------------------------
|
||||
|
||||
- Add Python 3.11 to Test Suite & Adapt Enum Behaviour (`#3168`_)
|
||||
- Drop Manual Token Validation (`#3167`_)
|
||||
- Simplify Unit Tests for ``Bot.send_chat_action`` (`#3151`_)
|
||||
- Drop ``pre-commit`` Dependencies from ``requirements-dev.txt`` (`#3120`_)
|
||||
- Change Default Values for ``concurrent_updates`` and ``connection_pool_size`` (`#3127`_)
|
||||
- Documentation Improvements (`#3139`_, `#3153`_, `#3135`_)
|
||||
- Type Hinting Fixes (`#3202`_)
|
||||
|
||||
Dependencies
|
||||
------------
|
||||
|
||||
- Bump ``sphinx`` from 5.0.2 to 5.1.1 (`#3177`_)
|
||||
- Update ``pre-commit`` Dependencies (`#3085`_)
|
||||
- Bump ``pytest-asyncio`` from 0.18.3 to 0.19.0 (`#3158`_)
|
||||
- Update ``tornado`` requirement from ~=6.1 to ~=6.2 (`#3149`_)
|
||||
- Bump ``black`` from 22.3.0 to 22.6.0 (`#3132`_)
|
||||
- Bump ``actions/setup-python`` from 3 to 4 (`#3131`_)
|
||||
|
||||
.. _`#3195`: https://github.com/python-telegram-bot/python-telegram-bot/pull/3195
|
||||
.. _`#3148`: https://github.com/python-telegram-bot/python-telegram-bot/pull/3148
|
||||
.. _`#3152`: https://github.com/python-telegram-bot/python-telegram-bot/pull/3152
|
||||
.. _`#3126`: https://github.com/python-telegram-bot/python-telegram-bot/pull/3126
|
||||
.. _`#3155`: https://github.com/python-telegram-bot/python-telegram-bot/pull/3155
|
||||
.. _`#3147`: https://github.com/python-telegram-bot/python-telegram-bot/pull/3147
|
||||
.. _`#3136`: https://github.com/python-telegram-bot/python-telegram-bot/pull/3136
|
||||
.. _`#3128`: https://github.com/python-telegram-bot/python-telegram-bot/pull/3128
|
||||
.. _`#3168`: https://github.com/python-telegram-bot/python-telegram-bot/pull/3168
|
||||
.. _`#3167`: https://github.com/python-telegram-bot/python-telegram-bot/pull/3167
|
||||
.. _`#3151`: https://github.com/python-telegram-bot/python-telegram-bot/pull/3151
|
||||
.. _`#3120`: https://github.com/python-telegram-bot/python-telegram-bot/pull/3120
|
||||
.. _`#3127`: https://github.com/python-telegram-bot/python-telegram-bot/pull/3127
|
||||
.. _`#3139`: https://github.com/python-telegram-bot/python-telegram-bot/pull/3139
|
||||
.. _`#3153`: https://github.com/python-telegram-bot/python-telegram-bot/pull/3153
|
||||
.. _`#3135`: https://github.com/python-telegram-bot/python-telegram-bot/pull/3135
|
||||
.. _`#3202`: https://github.com/python-telegram-bot/python-telegram-bot/pull/3202
|
||||
.. _`#3177`: https://github.com/python-telegram-bot/python-telegram-bot/pull/3177
|
||||
.. _`#3085`: https://github.com/python-telegram-bot/python-telegram-bot/pull/3085
|
||||
.. _`#3158`: https://github.com/python-telegram-bot/python-telegram-bot/pull/3158
|
||||
.. _`#3149`: https://github.com/python-telegram-bot/python-telegram-bot/pull/3149
|
||||
.. _`#3132`: https://github.com/python-telegram-bot/python-telegram-bot/pull/3132
|
||||
.. _`#3131`: https://github.com/python-telegram-bot/python-telegram-bot/pull/3131
|
||||
|
||||
Version 20.0a2
|
||||
==============
|
||||
*Released 2022-06-27*
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
include LICENSE LICENSE.lesser Makefile requirements.txt README_RAW.rst telegram/py.typed
|
||||
include LICENSE LICENSE.lesser Makefile requirements.txt requirements-opts.txt README_RAW.rst telegram/py.typed
|
||||
|
||||
+4
-3
@@ -14,7 +14,7 @@
|
||||
:target: https://pypi.org/project/python-telegram-bot/
|
||||
:alt: Supported Python versions
|
||||
|
||||
.. image:: https://img.shields.io/badge/Bot%20API-6.1-blue?logo=telegram
|
||||
.. image:: https://img.shields.io/badge/Bot%20API-6.2-blue?logo=telegram
|
||||
:target: https://core.telegram.org/bots/api-changelog
|
||||
:alt: Supported Bot API versions
|
||||
|
||||
@@ -93,7 +93,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 **6.1** are supported.
|
||||
All types and methods of the Telegram Bot API **6.2** are supported.
|
||||
|
||||
Installing
|
||||
==========
|
||||
@@ -122,7 +122,7 @@ However, for some features using a 3rd party library is more sane than implement
|
||||
The dependencies are:
|
||||
|
||||
* `httpx ~= 0.23.0 <https://www.python-httpx.org>`_ for ``telegram.request.HTTPXRequest``, the default networking backend
|
||||
* `tornado~=6.1 <https://www.tornadoweb.org/en/stable/>`_ for ``telegram.ext.Updater.start_webhook``
|
||||
* `tornado~=6.2 <https://www.tornadoweb.org/en/stable/>`_ for ``telegram.ext.Updater.start_webhook``
|
||||
* `cachetools~=5.2.0 <https://cachetools.readthedocs.io/en/latest/>`_ for ``telegram.ext.CallbackDataCache``
|
||||
* `APScheduler~=3.9.1 <https://apscheduler.readthedocs.io/en/3.x/>`_ for ``telegram.ext.JobQueue``
|
||||
|
||||
@@ -138,6 +138,7 @@ PTB can be installed with optional dependencies:
|
||||
|
||||
* ``pip install python-telegram-bot[passport]`` installs the `cryptography>=3.0 <https://cryptography.io/en/stable>`_ library. Use this, if you want to use Telegram Passport related functionality.
|
||||
* ``pip install python-telegram-bot[socks]`` installs ``httpx[socks]``. Use this, if you want to work behind a Socks5 server.
|
||||
* ``pip install python-telegram-bot[rate-limiter]`` installs ``aiolimiter~=1.0.0``. Use this, if you want to use ``telegram.ext.AIORateLimiter``.
|
||||
|
||||
Quick Start
|
||||
===========
|
||||
|
||||
+2
-2
@@ -14,7 +14,7 @@
|
||||
:target: https://pypi.org/project/python-telegram-bot-raw/
|
||||
:alt: Supported Python versions
|
||||
|
||||
.. image:: https://img.shields.io/badge/Bot%20API-6.1-blue?logo=telegram
|
||||
.. image:: https://img.shields.io/badge/Bot%20API-6.2-blue?logo=telegram
|
||||
:target: https://core.telegram.org/bots/api-changelog
|
||||
:alt: Supported Bot API versions
|
||||
|
||||
@@ -89,7 +89,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 **6.1** are supported.
|
||||
All types and methods of the Telegram Bot API **6.2** are supported.
|
||||
|
||||
Installing
|
||||
==========
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
sphinx==5.0.2
|
||||
sphinx==5.1.1
|
||||
sphinx-pypi-upload
|
||||
furo==2022.6.21
|
||||
sphinx-paramlinks==0.5.4
|
||||
|
||||
+7
-3
@@ -29,9 +29,9 @@ author = "Leandro Toledo"
|
||||
# built documents.
|
||||
#
|
||||
# The short X.Y version.
|
||||
version = "20.0a2" # telegram.__version__[:3]
|
||||
version = "20.0a4" # telegram.__version__[:3]
|
||||
# The full version, including alpha/beta/rc tags.
|
||||
release = "20.0a2" # telegram.__version__
|
||||
release = "20.0a4" # telegram.__version__
|
||||
|
||||
# If your documentation needs a minimal Sphinx version, state it here.
|
||||
needs_sphinx = "4.5.0"
|
||||
@@ -478,6 +478,10 @@ def autodoc_process_bases(app, name, obj, option, bases: list):
|
||||
bases.insert(0, ":class:`str`")
|
||||
continue
|
||||
|
||||
if "IntEnum" in base:
|
||||
bases[idx] = ":class:`enum.IntEnum`"
|
||||
continue
|
||||
|
||||
# Drop generics (at least for now)
|
||||
if base.endswith("]"):
|
||||
base = base.split("[", maxsplit=1)[0]
|
||||
@@ -486,7 +490,7 @@ def autodoc_process_bases(app, name, obj, option, bases: list):
|
||||
# Now convert `telegram._message.Message` to `telegram.Message` etc
|
||||
match = re.search(pattern=r"(telegram(\.ext|))\.[_\w\.]+", string=base)
|
||||
if not match or "_utils" in base:
|
||||
return
|
||||
continue
|
||||
|
||||
parts = match.group(0).split(".")
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ this library, we introduced the
|
||||
:class:`telegram.ext.ConversationHandler`
|
||||
for that exact purpose. This example uses it to retrieve
|
||||
user-information in a conversation-like style. To get a better
|
||||
understanding, take a look at the :ref:`state diagrem <conversationbot-diagram>`.
|
||||
understanding, take a look at the :ref:`state diagram <conversationbot-diagram>`.
|
||||
|
||||
:any:`examples.conversationbot2`
|
||||
--------------------------------
|
||||
|
||||
@@ -206,6 +206,8 @@
|
||||
- Used for getting a sticker set
|
||||
* - :meth:`~telegram.Bot.upload_sticker_file`
|
||||
- Used for uploading a sticker file
|
||||
* - :meth:`~telegram.Bot.get_custom_emoji_stickers`
|
||||
- Used for getting custom emoji files based on their IDs
|
||||
|
||||
.. raw:: html
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
.. tip::
|
||||
When making requests to the Bot API in an asynchronous fashion (e.g. via
|
||||
:attr:`block=False <telegram.ext.BaseHandler.block>`, :meth:`Application.create_task <telegram.ext.Application.create_task>`,
|
||||
:meth:`~telegram.ext.ApplicationBuilder.concurrent_updates` or the :class:`~telegram.ext.JobQueue`), it can happen that more requests
|
||||
are being made in parallel than there are connections in the pool.
|
||||
If the number of requests is much higher than the number of connections, even setting
|
||||
:meth:`~telegram.ext.ApplicationBuilder.pool_timeout` to a larger value may not always be enough to prevent pool
|
||||
timeouts.
|
||||
You should therefore set :meth:`~telegram.ext.ApplicationBuilder.concurrent_updates`, :meth:`~telegram.ext.ApplicationBuilder.connection_pool_size` and
|
||||
:meth:`~telegram.ext.ApplicationBuilder.pool_timeout` to values that make sense for your setup.
|
||||
@@ -0,0 +1,7 @@
|
||||
Arbitrary Callback Data
|
||||
-----------------------
|
||||
|
||||
.. toctree::
|
||||
|
||||
telegram.ext.callbackdatacache
|
||||
telegram.ext.invalidcallbackdata
|
||||
@@ -0,0 +1,6 @@
|
||||
telegram.ext.AIORateLimiter
|
||||
============================
|
||||
|
||||
.. autoclass:: telegram.ext.AIORateLimiter
|
||||
:members:
|
||||
:show-inheritance:
|
||||
@@ -0,0 +1,6 @@
|
||||
telegram.ext.BaseRateLimiter
|
||||
============================
|
||||
|
||||
.. autoclass:: telegram.ext.BaseRateLimiter
|
||||
:members:
|
||||
:show-inheritance:
|
||||
@@ -3,5 +3,4 @@ telegram.ext.ExtBot
|
||||
|
||||
.. autoclass:: telegram.ext.ExtBot
|
||||
:show-inheritance:
|
||||
|
||||
.. autofunction:: telegram.ext.ExtBot.insert_callback_data
|
||||
:members: insert_callback_data, defaults, rate_limiter, initialize, shutdown
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
Handlers
|
||||
--------
|
||||
|
||||
.. toctree::
|
||||
|
||||
telegram.ext.basehandler
|
||||
telegram.ext.callbackqueryhandler
|
||||
telegram.ext.chatjoinrequesthandler
|
||||
telegram.ext.chatmemberhandler
|
||||
telegram.ext.choseninlineresulthandler
|
||||
telegram.ext.commandhandler
|
||||
telegram.ext.conversationhandler
|
||||
telegram.ext.filters
|
||||
telegram.ext.inlinequeryhandler
|
||||
telegram.ext.messagehandler
|
||||
telegram.ext.pollanswerhandler
|
||||
telegram.ext.pollhandler
|
||||
telegram.ext.precheckoutqueryhandler
|
||||
telegram.ext.prefixhandler
|
||||
telegram.ext.shippingqueryhandler
|
||||
telegram.ext.stringcommandhandler
|
||||
telegram.ext.stringregexhandler
|
||||
telegram.ext.typehandler
|
||||
@@ -0,0 +1,9 @@
|
||||
Persistence
|
||||
-----------
|
||||
|
||||
.. toctree::
|
||||
|
||||
telegram.ext.basepersistence
|
||||
telegram.ext.dictpersistence
|
||||
telegram.ext.persistenceinput
|
||||
telegram.ext.picklepersistence
|
||||
@@ -0,0 +1,7 @@
|
||||
Rate Limiting
|
||||
-------------
|
||||
|
||||
.. toctree::
|
||||
|
||||
telegram.ext.baseratelimiter
|
||||
telegram.ext.aioratelimiter
|
||||
@@ -13,45 +13,7 @@ telegram.ext package
|
||||
telegram.ext.job
|
||||
telegram.ext.jobqueue
|
||||
telegram.ext.updater
|
||||
|
||||
Handlers
|
||||
--------
|
||||
|
||||
.. toctree::
|
||||
|
||||
telegram.ext.basehandler
|
||||
telegram.ext.callbackqueryhandler
|
||||
telegram.ext.chatjoinrequesthandler
|
||||
telegram.ext.chatmemberhandler
|
||||
telegram.ext.choseninlineresulthandler
|
||||
telegram.ext.commandhandler
|
||||
telegram.ext.conversationhandler
|
||||
telegram.ext.filters
|
||||
telegram.ext.inlinequeryhandler
|
||||
telegram.ext.messagehandler
|
||||
telegram.ext.pollanswerhandler
|
||||
telegram.ext.pollhandler
|
||||
telegram.ext.precheckoutqueryhandler
|
||||
telegram.ext.prefixhandler
|
||||
telegram.ext.shippingqueryhandler
|
||||
telegram.ext.stringcommandhandler
|
||||
telegram.ext.stringregexhandler
|
||||
telegram.ext.typehandler
|
||||
|
||||
Persistence
|
||||
-----------
|
||||
|
||||
.. toctree::
|
||||
|
||||
telegram.ext.basepersistence
|
||||
telegram.ext.dictpersistence
|
||||
telegram.ext.persistenceinput
|
||||
telegram.ext.picklepersistence
|
||||
|
||||
Arbitrary Callback Data
|
||||
-----------------------
|
||||
|
||||
.. toctree::
|
||||
|
||||
telegram.ext.callbackdatacache
|
||||
telegram.ext.invalidcallbackdata
|
||||
telegram.ext.handlers-tree.rst
|
||||
telegram.ext.persistence-tree.rst
|
||||
telegram.ext.acd-tree.rst
|
||||
telegram.ext.rate-limiting-tree.rst
|
||||
@@ -0,0 +1,8 @@
|
||||
Games
|
||||
-----
|
||||
|
||||
.. toctree::
|
||||
|
||||
telegram.callbackgame
|
||||
telegram.game
|
||||
telegram.gamehighscore
|
||||
@@ -0,0 +1,34 @@
|
||||
Inline Mode
|
||||
-----------
|
||||
|
||||
.. toctree::
|
||||
|
||||
telegram.choseninlineresult
|
||||
telegram.inlinequery
|
||||
telegram.inlinequeryresult
|
||||
telegram.inlinequeryresultarticle
|
||||
telegram.inlinequeryresultaudio
|
||||
telegram.inlinequeryresultcachedaudio
|
||||
telegram.inlinequeryresultcacheddocument
|
||||
telegram.inlinequeryresultcachedgif
|
||||
telegram.inlinequeryresultcachedmpeg4gif
|
||||
telegram.inlinequeryresultcachedphoto
|
||||
telegram.inlinequeryresultcachedsticker
|
||||
telegram.inlinequeryresultcachedvideo
|
||||
telegram.inlinequeryresultcachedvoice
|
||||
telegram.inlinequeryresultcontact
|
||||
telegram.inlinequeryresultdocument
|
||||
telegram.inlinequeryresultgame
|
||||
telegram.inlinequeryresultgif
|
||||
telegram.inlinequeryresultlocation
|
||||
telegram.inlinequeryresultmpeg4gif
|
||||
telegram.inlinequeryresultphoto
|
||||
telegram.inlinequeryresultvenue
|
||||
telegram.inlinequeryresultvideo
|
||||
telegram.inlinequeryresultvoice
|
||||
telegram.inputmessagecontent
|
||||
telegram.inputtextmessagecontent
|
||||
telegram.inputlocationmessagecontent
|
||||
telegram.inputvenuemessagecontent
|
||||
telegram.inputcontactmessagecontent
|
||||
telegram.inputinvoicemessagecontent
|
||||
@@ -0,0 +1,27 @@
|
||||
Passport
|
||||
--------
|
||||
|
||||
.. toctree::
|
||||
|
||||
telegram.credentials
|
||||
telegram.datacredentials
|
||||
telegram.encryptedcredentials
|
||||
telegram.encryptedpassportelement
|
||||
telegram.filecredentials
|
||||
telegram.iddocumentdata
|
||||
telegram.passportdata
|
||||
telegram.passportelementerror
|
||||
telegram.passportelementerrordatafield
|
||||
telegram.passportelementerrorfile
|
||||
telegram.passportelementerrorfiles
|
||||
telegram.passportelementerrorfrontside
|
||||
telegram.passportelementerrorreverseside
|
||||
telegram.passportelementerrorselfie
|
||||
telegram.passportelementerrortranslationfile
|
||||
telegram.passportelementerrortranslationfiles
|
||||
telegram.passportelementerrorunspecified
|
||||
telegram.passportfile
|
||||
telegram.personaldetails
|
||||
telegram.residentialaddress
|
||||
telegram.securedata
|
||||
telegram.securevalue
|
||||
@@ -0,0 +1,13 @@
|
||||
Payments
|
||||
--------
|
||||
|
||||
.. toctree::
|
||||
|
||||
telegram.invoice
|
||||
telegram.labeledprice
|
||||
telegram.orderinfo
|
||||
telegram.precheckoutquery
|
||||
telegram.shippingaddress
|
||||
telegram.shippingoption
|
||||
telegram.shippingquery
|
||||
telegram.successfulpayment
|
||||
@@ -89,98 +89,9 @@ Available Types
|
||||
telegram.webappdata
|
||||
telegram.webappinfo
|
||||
telegram.webhookinfo
|
||||
telegram.stickers-tree.rst
|
||||
telegram.inline-tree.rst
|
||||
telegram.payments-tree.rst
|
||||
telegram.games-tree.rst
|
||||
telegram.passport-tree.rst
|
||||
|
||||
Stickers
|
||||
--------
|
||||
|
||||
.. toctree::
|
||||
|
||||
telegram.maskposition
|
||||
telegram.sticker
|
||||
telegram.stickerset
|
||||
|
||||
Inline Mode
|
||||
-----------
|
||||
|
||||
.. toctree::
|
||||
|
||||
telegram.choseninlineresult
|
||||
telegram.inlinequery
|
||||
telegram.inlinequeryresult
|
||||
telegram.inlinequeryresultarticle
|
||||
telegram.inlinequeryresultaudio
|
||||
telegram.inlinequeryresultcachedaudio
|
||||
telegram.inlinequeryresultcacheddocument
|
||||
telegram.inlinequeryresultcachedgif
|
||||
telegram.inlinequeryresultcachedmpeg4gif
|
||||
telegram.inlinequeryresultcachedphoto
|
||||
telegram.inlinequeryresultcachedsticker
|
||||
telegram.inlinequeryresultcachedvideo
|
||||
telegram.inlinequeryresultcachedvoice
|
||||
telegram.inlinequeryresultcontact
|
||||
telegram.inlinequeryresultdocument
|
||||
telegram.inlinequeryresultgame
|
||||
telegram.inlinequeryresultgif
|
||||
telegram.inlinequeryresultlocation
|
||||
telegram.inlinequeryresultmpeg4gif
|
||||
telegram.inlinequeryresultphoto
|
||||
telegram.inlinequeryresultvenue
|
||||
telegram.inlinequeryresultvideo
|
||||
telegram.inlinequeryresultvoice
|
||||
telegram.inputmessagecontent
|
||||
telegram.inputtextmessagecontent
|
||||
telegram.inputlocationmessagecontent
|
||||
telegram.inputvenuemessagecontent
|
||||
telegram.inputcontactmessagecontent
|
||||
telegram.inputinvoicemessagecontent
|
||||
|
||||
Payments
|
||||
--------
|
||||
|
||||
.. toctree::
|
||||
|
||||
telegram.invoice
|
||||
telegram.labeledprice
|
||||
telegram.orderinfo
|
||||
telegram.precheckoutquery
|
||||
telegram.shippingaddress
|
||||
telegram.shippingoption
|
||||
telegram.shippingquery
|
||||
telegram.successfulpayment
|
||||
|
||||
Games
|
||||
-----
|
||||
|
||||
.. toctree::
|
||||
|
||||
telegram.callbackgame
|
||||
telegram.game
|
||||
telegram.gamehighscore
|
||||
|
||||
Passport
|
||||
--------
|
||||
|
||||
.. toctree::
|
||||
|
||||
telegram.credentials
|
||||
telegram.datacredentials
|
||||
telegram.encryptedcredentials
|
||||
telegram.encryptedpassportelement
|
||||
telegram.filecredentials
|
||||
telegram.iddocumentdata
|
||||
telegram.passportdata
|
||||
telegram.passportelementerror
|
||||
telegram.passportelementerrordatafield
|
||||
telegram.passportelementerrorfile
|
||||
telegram.passportelementerrorfiles
|
||||
telegram.passportelementerrorfrontside
|
||||
telegram.passportelementerrorreverseside
|
||||
telegram.passportelementerrorselfie
|
||||
telegram.passportelementerrortranslationfile
|
||||
telegram.passportelementerrortranslationfiles
|
||||
telegram.passportelementerrorunspecified
|
||||
telegram.passportfile
|
||||
telegram.personaldetails
|
||||
telegram.residentialaddress
|
||||
telegram.securedata
|
||||
telegram.securevalue
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
Stickers
|
||||
--------
|
||||
|
||||
.. toctree::
|
||||
|
||||
telegram.maskposition
|
||||
telegram.sticker
|
||||
telegram.stickerset
|
||||
@@ -0,0 +1,4 @@
|
||||
-r requirements.txt
|
||||
-r requirements-dev.txt
|
||||
-r requirements-opts.txt
|
||||
-r docs/requirements-docs.txt
|
||||
+1
-11
@@ -1,17 +1,7 @@
|
||||
# cryptography is an optional dependency, but running the tests properly requires it
|
||||
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==22.3.0
|
||||
flake8==4.0.1
|
||||
pylint==2.13.9
|
||||
mypy==0.961
|
||||
pyupgrade==2.34.0
|
||||
isort==5.10.1
|
||||
|
||||
pytest==7.1.2
|
||||
pytest-asyncio==0.18.3
|
||||
pytest-asyncio==0.19.0
|
||||
pytest-timeout==2.1.0 # used to timeout tests
|
||||
|
||||
flaky # Used for flaky tests (flaky decorator)
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
# Format:
|
||||
# package_name==version # req-1, req-2, req-3!ext
|
||||
# `pip install ptb-raw[req-1/2]` will install `package_name`
|
||||
# `pip install ptb[req-1/2/3]` will also install `package_name`
|
||||
httpx[socks] # socks
|
||||
cryptography!=3.4,!=3.4.1,!=3.4.2,!=3.4.3,>=3.0 # passport
|
||||
aiolimiter~=1.0.0 # rate-limiter!ext
|
||||
+1
-1
@@ -10,7 +10,7 @@ httpx ~= 0.23.0
|
||||
# only telegram.ext: # Keep this line here; used in setup(-raw).py
|
||||
|
||||
# tornado is rather stable, but let's not allow the next mayor release without prior testing
|
||||
tornado~=6.1
|
||||
tornado~=6.2
|
||||
|
||||
# Cachetools and APS don't have a strict stability policy.
|
||||
# Let's be cautious for now.
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"""The setup and build script for the python-telegram-bot library."""
|
||||
import subprocess
|
||||
import sys
|
||||
from collections import defaultdict
|
||||
from pathlib import Path
|
||||
|
||||
from setuptools import find_packages, setup
|
||||
@@ -35,6 +36,28 @@ def get_packages_requirements(raw=False):
|
||||
return packs, reqs
|
||||
|
||||
|
||||
def get_optional_requirements(raw=False):
|
||||
"""Build the optional dependencies"""
|
||||
requirements = defaultdict(list)
|
||||
|
||||
with Path("requirements-opts.txt").open() as reqs:
|
||||
for line in reqs:
|
||||
if line.startswith("#"):
|
||||
continue
|
||||
dependency, names = line.split("#")
|
||||
dependency = dependency.strip()
|
||||
for name in names.split(","):
|
||||
name = name.strip()
|
||||
if name.endswith("!ext"):
|
||||
if raw:
|
||||
continue
|
||||
else:
|
||||
name = name[:-4]
|
||||
requirements[name].append(dependency)
|
||||
|
||||
return requirements
|
||||
|
||||
|
||||
def get_setup_kwargs(raw=False):
|
||||
"""Builds a dictionary of kwargs for the setup function"""
|
||||
packages, requirements = get_packages_requirements(raw=raw)
|
||||
@@ -69,11 +92,7 @@ def get_setup_kwargs(raw=False):
|
||||
long_description_content_type="text/x-rst",
|
||||
packages=packages,
|
||||
install_requires=requirements,
|
||||
extras_require={
|
||||
"socks": "httpx[socks]",
|
||||
# 3.4-3.4.3 contained some cyclical import bugs
|
||||
"passport": "cryptography!=3.4,!=3.4.1,!=3.4.2,!=3.4.3,>=3.0",
|
||||
},
|
||||
extras_require=get_optional_requirements(raw=raw),
|
||||
include_package_data=True,
|
||||
classifiers=[
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
@@ -89,6 +108,7 @@ def get_setup_kwargs(raw=False):
|
||||
"Programming Language :: Python :: 3.8",
|
||||
"Programming Language :: Python :: 3.9",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
],
|
||||
python_requires=">=3.7",
|
||||
)
|
||||
|
||||
+304
-58
@@ -139,6 +139,10 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
|
||||
serialized instance will not reflect that change. Trying to pickle a bot instance will
|
||||
raise :exc:`pickle.PicklingError`.
|
||||
|
||||
.. seealso:: :attr:`telegram.ext.Application.bot`,
|
||||
:attr:`telegram.ext.CallbackContext.bot`,
|
||||
:attr:`telegram.ext.Updater.bot`
|
||||
|
||||
.. versionadded:: 13.2
|
||||
Objects of this class are comparable in terms of equality. Two objects of this class are
|
||||
considered equal, if their :attr:`bot` is equal.
|
||||
@@ -172,7 +176,7 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
|
||||
private_key (:obj:`bytes`, optional): Private key for decryption of telegram passport data.
|
||||
private_key_password (:obj:`bytes`, optional): Password for above private key.
|
||||
|
||||
.. include:: bot_methods.rst
|
||||
.. include:: inclusions/bot_methods.rst
|
||||
|
||||
"""
|
||||
|
||||
@@ -197,7 +201,9 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
|
||||
private_key: bytes = None,
|
||||
private_key_password: bytes = None,
|
||||
):
|
||||
self.token = self._validate_token(token)
|
||||
if not token:
|
||||
raise InvalidToken("You must pass the token you received from https://t.me/Botfather!")
|
||||
self.token = token
|
||||
|
||||
self.base_url = base_url + self.token
|
||||
self.base_file_url = base_file_url + self.token
|
||||
@@ -294,6 +300,25 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
|
||||
# Drop any None values because Telegram doesn't handle them well
|
||||
data = {key: value for key, value in data.items() if value is not None}
|
||||
|
||||
return await self._do_post(
|
||||
endpoint=endpoint,
|
||||
data=data,
|
||||
read_timeout=read_timeout,
|
||||
write_timeout=write_timeout,
|
||||
connect_timeout=connect_timeout,
|
||||
pool_timeout=pool_timeout,
|
||||
)
|
||||
|
||||
async def _do_post(
|
||||
self,
|
||||
endpoint: str,
|
||||
data: JSONDict,
|
||||
*,
|
||||
read_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
write_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
connect_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
pool_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
) -> Union[bool, JSONDict, None]:
|
||||
# This also converts datetimes into timestamps.
|
||||
# We don't do this earlier so that _insert_defaults (see above) has a chance to convert
|
||||
# to the default timezone in case this is called by ExtBot
|
||||
@@ -372,7 +397,12 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
|
||||
return
|
||||
|
||||
await asyncio.gather(self._request[0].initialize(), self._request[1].initialize())
|
||||
await self.get_me()
|
||||
# Since the bot is to be initialized only once, we can also use it for
|
||||
# verifying the token passed and raising an exception if it's invalid.
|
||||
try:
|
||||
await self.get_me()
|
||||
except InvalidToken as exc:
|
||||
raise InvalidToken(f"The token `{self.token}` was rejected by the server.") from exc
|
||||
self._initialized = True
|
||||
|
||||
async def shutdown(self) -> None:
|
||||
@@ -418,18 +448,6 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
|
||||
"""
|
||||
return self._request[1]
|
||||
|
||||
@staticmethod
|
||||
def _validate_token(token: str) -> str:
|
||||
"""A very basic validation on token."""
|
||||
if any(x.isspace() for x in token):
|
||||
raise InvalidToken()
|
||||
|
||||
left, sep, _right = token.partition(":")
|
||||
if (not sep) or (not left.isdigit()) or (len(left) < 3):
|
||||
raise InvalidToken()
|
||||
|
||||
return token
|
||||
|
||||
@property
|
||||
def bot(self) -> User:
|
||||
""":class:`telegram.User`: User instance for the bot as returned by :meth:`get_me`.
|
||||
@@ -576,6 +594,9 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
|
||||
) -> Message:
|
||||
"""Use this method to send text messages.
|
||||
|
||||
.. seealso:: :attr:`telegram.Message.reply_text`, :attr:`telegram.Chat.send_message`,
|
||||
:attr:`telegram.User.send_message`
|
||||
|
||||
Args:
|
||||
chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username
|
||||
of the target channel (in the format ``@channelusername``).
|
||||
@@ -669,16 +690,19 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
|
||||
Use this method to delete a message, including service messages, with the following
|
||||
limitations:
|
||||
|
||||
- A message can only be deleted if it was sent less than 48 hours ago.
|
||||
- A dice message in a private chat can only be deleted if it was sent more than 24
|
||||
hours ago.
|
||||
- Bots can delete outgoing messages in private chats, groups, and supergroups.
|
||||
- Bots can delete incoming messages in private chats.
|
||||
- Bots granted :attr:`~telegram.ChatMemberAdministrator.can_post_messages` permissions
|
||||
can delete outgoing messages in channels.
|
||||
- If the bot is an administrator of a group, it can delete any message there.
|
||||
- If the bot has :attr:`~telegram.ChatMemberAdministrator.can_delete_messages`
|
||||
permission in a supergroup or a channel, it can delete any message there.
|
||||
- A message can only be deleted if it was sent less than 48 hours ago.
|
||||
- A dice message in a private chat can only be deleted if it was sent more than 24
|
||||
hours ago.
|
||||
- Bots can delete outgoing messages in private chats, groups, and supergroups.
|
||||
- Bots can delete incoming messages in private chats.
|
||||
- Bots granted :attr:`~telegram.ChatMemberAdministrator.can_post_messages` permissions
|
||||
can delete outgoing messages in channels.
|
||||
- If the bot is an administrator of a group, it can delete any message there.
|
||||
- If the bot has :attr:`~telegram.ChatMemberAdministrator.can_delete_messages`
|
||||
permission in a supergroup or a channel, it can delete any message there.
|
||||
|
||||
.. seealso:: :meth:`telegram.Message.delete`,
|
||||
:meth:`telegram.CallbackQuery.delete_message`
|
||||
|
||||
Args:
|
||||
chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username
|
||||
@@ -746,6 +770,9 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
|
||||
As a workaround, it is still possible to use :meth:`copy_message`. However, this
|
||||
behaviour is undocumented and might be changed by Telegram.
|
||||
|
||||
.. seealso:: :attr:`telegram.Message.forward`, :attr:`telegram.Chat.forward_to`,
|
||||
:attr:`telegram.Chat.forward_from`
|
||||
|
||||
Args:
|
||||
chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username
|
||||
of the target channel (in the format ``@channelusername``).
|
||||
@@ -828,6 +855,9 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
|
||||
The photo argument can be either a file_id, an URL or a file from disk
|
||||
``open(filename, 'rb')``
|
||||
|
||||
.. seealso:: :attr:`telegram.Message.reply_photo`, :attr:`telegram.Chat.send_photo`,
|
||||
:attr:`telegram.User.send_photo`
|
||||
|
||||
Args:
|
||||
chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username
|
||||
of the target channel (in the format ``@channelusername``).
|
||||
@@ -958,6 +988,9 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
|
||||
The audio argument can be either a file_id, an URL or a file from disk
|
||||
``open(filename, 'rb')``
|
||||
|
||||
.. seealso:: :attr:`telegram.Message.reply_audio`, :attr:`telegram.Chat.send_audio`,
|
||||
:attr:`telegram.User.send_audio`
|
||||
|
||||
Args:
|
||||
chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username
|
||||
of the target channel (in the format ``@channelusername``).
|
||||
@@ -1105,6 +1138,9 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
|
||||
|
||||
* Sending by URL will currently only work ``GIF``, ``PDF`` & ``ZIP`` files.
|
||||
|
||||
.. seealso:: :attr:`telegram.Message.reply_document`, :attr:`telegram.Chat.send_document`,
|
||||
:attr:`telegram.User.send_document`
|
||||
|
||||
Args:
|
||||
chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username
|
||||
of the target channel (in the format ``@channelusername``).
|
||||
@@ -1233,6 +1269,9 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
|
||||
The :paramref:`sticker` argument can be either a file_id, an URL or a file from disk
|
||||
``open(filename, 'rb')``
|
||||
|
||||
.. seealso:: :attr:`telegram.Message.reply_sticker`, :attr:`telegram.Chat.send_sticker`,
|
||||
:attr:`telegram.User.send_sticker`
|
||||
|
||||
Args:
|
||||
chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username
|
||||
of the target channel (in the format ``@channelusername``).
|
||||
@@ -1340,6 +1379,9 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
|
||||
easily generate thumbnails. However, this behaviour is undocumented and might be
|
||||
changed by Telegram.
|
||||
|
||||
.. seealso:: :attr:`telegram.Message.reply_video`, :attr:`telegram.Chat.send_video`,
|
||||
:attr:`telegram.User.send_video`
|
||||
|
||||
Args:
|
||||
chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username
|
||||
of the target channel (in the format ``@channelusername``).
|
||||
@@ -1486,6 +1528,10 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
|
||||
easily generate thumbnails. However, this behaviour is undocumented and might be
|
||||
changed by Telegram.
|
||||
|
||||
.. seealso:: :attr:`telegram.Message.reply_video_note`,
|
||||
:attr:`telegram.Chat.send_video_note`,
|
||||
:attr:`telegram.User.send_video_note`
|
||||
|
||||
Args:
|
||||
chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username
|
||||
of the target channel (in the format ``@channelusername``).
|
||||
@@ -1616,6 +1662,10 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
|
||||
generate thumb nails. However, this behaviour is undocumented and might be changed
|
||||
by Telegram.
|
||||
|
||||
.. seealso:: :attr:`telegram.Message.reply_animation`,
|
||||
:attr:`telegram.Chat.send_animation`,
|
||||
:attr:`telegram.User.send_animation`
|
||||
|
||||
Args:
|
||||
chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username
|
||||
of the target channel (in the format ``@channelusername``).
|
||||
@@ -1763,6 +1813,9 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
|
||||
* To use this method, the file must have the type :mimetype:`audio/ogg` and be no more
|
||||
than ``1MB`` in size. ``1-20MB`` voice notes will be sent as files.
|
||||
|
||||
.. seealso:: :attr:`telegram.Message.reply_voice`, :attr:`telegram.Chat.send_voice`,
|
||||
:attr:`telegram.User.send_voice`
|
||||
|
||||
Args:
|
||||
chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username
|
||||
of the target channel (in the format ``@channelusername``).
|
||||
@@ -1877,6 +1930,10 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
|
||||
) -> List[Message]:
|
||||
"""Use this method to send a group of photos or videos as an album.
|
||||
|
||||
.. seealso:: :attr:`telegram.Message.reply_media_group`,
|
||||
:attr:`telegram.Chat.send_media_group`,
|
||||
:attr:`telegram.User.send_media_group`
|
||||
|
||||
Args:
|
||||
chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username
|
||||
of the target channel (in the format ``@channelusername``).
|
||||
@@ -1968,6 +2025,9 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
|
||||
You can either supply a :paramref:`latitude` and :paramref:`longitude` or a
|
||||
:paramref:`location`.
|
||||
|
||||
.. seealso:: :attr:`telegram.Message.reply_location`, :attr:`telegram.Chat.send_location`,
|
||||
:attr:`telegram.User.send_location`
|
||||
|
||||
Args:
|
||||
chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username
|
||||
of the target channel (in the format ``@channelusername``).
|
||||
@@ -2069,7 +2129,7 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
|
||||
self,
|
||||
chat_id: Union[str, int] = None,
|
||||
message_id: int = None,
|
||||
inline_message_id: int = None,
|
||||
inline_message_id: str = None,
|
||||
latitude: float = None,
|
||||
longitude: float = None,
|
||||
reply_markup: InlineKeyboardMarkup = None,
|
||||
@@ -2092,6 +2152,9 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
|
||||
You can either supply a :paramref:`latitude` and :paramref:`longitude` or a
|
||||
:paramref:`location`.
|
||||
|
||||
.. seealso:: :attr:`telegram.Message.edit_live_location`,
|
||||
:attr:`telegram.CallbackQuery.edit_message_live_location`
|
||||
|
||||
Args:
|
||||
chat_id (:obj:`int` | :obj:`str`, optional): Required if inline_message_id is not
|
||||
specified. Unique identifier for the target chat or username of the target channel
|
||||
@@ -2180,7 +2243,7 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
|
||||
self,
|
||||
chat_id: Union[str, int] = None,
|
||||
message_id: int = None,
|
||||
inline_message_id: int = None,
|
||||
inline_message_id: str = None,
|
||||
reply_markup: InlineKeyboardMarkup = None,
|
||||
*,
|
||||
read_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
@@ -2192,6 +2255,9 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
|
||||
"""Use this method to stop updating a live location message sent by the bot or via the bot
|
||||
(for inline bots) before live_period expires.
|
||||
|
||||
.. seealso:: :attr:`telegram.Message.stop_live_location`
|
||||
:attr:`telegram.CallbackQuery.stop_message_live_location`
|
||||
|
||||
Args:
|
||||
chat_id (:obj:`int` | :obj:`str`): Required if inline_message_id is not specified.
|
||||
Unique identifier for the target chat or username of the target channel
|
||||
@@ -2278,6 +2344,9 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
|
||||
* Foursquare details and Google Place details are mutually exclusive. However, this
|
||||
behaviour is undocumented and might be changed by Telegram.
|
||||
|
||||
.. seealso:: :attr:`telegram.Message.reply_venue`, :attr:`telegram.Chat.send_venue`,
|
||||
:attr:`telegram.User.send_venue`
|
||||
|
||||
Args:
|
||||
chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username
|
||||
of the target channel (in the format ``@channelusername``).
|
||||
@@ -2416,6 +2485,9 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
|
||||
:paramref:`first_name` with optionally :paramref:`last_name` and optionally
|
||||
:paramref:`vcard`.
|
||||
|
||||
.. seealso:: :attr:`telegram.Message.reply_contact`, :attr:`telegram.Chat.send_contact`,
|
||||
:attr:`telegram.User.send_contact`
|
||||
|
||||
Args:
|
||||
chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username
|
||||
of the target channel (in the format ``@channelusername``).
|
||||
@@ -2527,6 +2599,9 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
|
||||
) -> Message:
|
||||
"""Use this method to send a game.
|
||||
|
||||
.. seealso:: :attr:`telegram.Message.reply_game`, :attr:`telegram.Chat.send_game`,
|
||||
:attr:`telegram.User.send_game`
|
||||
|
||||
Args:
|
||||
chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat.
|
||||
game_short_name (:obj:`str`): Short name of the game, serves as the unique identifier
|
||||
@@ -2604,6 +2679,9 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
|
||||
Telegram clients clear its typing status). Telegram only recommends using this method when
|
||||
a response from the bot will take a noticeable amount of time to arrive.
|
||||
|
||||
.. seealso:: :attr:`telegram.Message.reply_chat_action`, :attr:`telegram.Chat.send_action`,
|
||||
:attr:`telegram.User.send_chat_action`
|
||||
|
||||
Args:
|
||||
chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username
|
||||
of the target channel (in the format ``@channelusername``).
|
||||
@@ -2753,6 +2831,8 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
|
||||
:paramref:`telegram.InlineQuery.answer.auto_pagination` set to :obj:`True`, which will
|
||||
take care of passing the correct value.
|
||||
|
||||
.. seealso:: :attr:`telegram.InlineQuery.answer`
|
||||
|
||||
Args:
|
||||
inline_query_id (:obj:`str`): Unique identifier for the answered query.
|
||||
results (List[:class:`telegram.InlineQueryResult`] | Callable): A list of results for
|
||||
@@ -2857,9 +2937,11 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
|
||||
connect_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
pool_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
api_kwargs: JSONDict = None,
|
||||
) -> Optional[UserProfilePhotos]:
|
||||
) -> UserProfilePhotos:
|
||||
"""Use this method to get a list of profile pictures for a user.
|
||||
|
||||
.. seealso:: :meth:`telegram.User.get_profile_photos`
|
||||
|
||||
Args:
|
||||
user_id (:obj:`int`): Unique identifier of the target user.
|
||||
offset (:obj:`int`, optional): Sequential number of the first photo to be returned.
|
||||
@@ -2907,7 +2989,7 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
|
||||
api_kwargs=api_kwargs,
|
||||
)
|
||||
|
||||
return UserProfilePhotos.de_json(result, self) # type: ignore[arg-type]
|
||||
return UserProfilePhotos.de_json(result, self) # type: ignore[arg-type,return-value]
|
||||
|
||||
@_log
|
||||
async def get_file(
|
||||
@@ -3014,6 +3096,8 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
|
||||
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.
|
||||
|
||||
.. seealso:: :attr:`telegram.Chat.ban_member`
|
||||
|
||||
.. versionadded:: 13.7
|
||||
|
||||
Args:
|
||||
@@ -3095,6 +3179,8 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
|
||||
their channels**. The bot must be an administrator in the supergroup or channel for this
|
||||
to work and must have the appropriate administrator rights.
|
||||
|
||||
.. seealso:: :attr:`telegram.Chat.ban_chat`, :attr:`telegram.Chat.ban_sender_chat`
|
||||
|
||||
.. versionadded:: 13.9
|
||||
|
||||
Args:
|
||||
@@ -3160,6 +3246,8 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
|
||||
join it. So if the user is a member of the chat they will also be *removed* from the chat.
|
||||
If you don't want this, use the parameter :paramref:`only_if_banned`.
|
||||
|
||||
.. seealso:: :attr:`telegram.Chat.unban_member`
|
||||
|
||||
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``).
|
||||
@@ -3222,6 +3310,8 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
|
||||
The bot must be an administrator for this to work and must have the
|
||||
appropriate administrator rights.
|
||||
|
||||
.. seealso:: :attr:`telegram.Chat.unban_chat`
|
||||
|
||||
.. versionadded:: 13.9
|
||||
|
||||
Args:
|
||||
@@ -3290,6 +3380,8 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
|
||||
and accept the terms. Otherwise, you may use links like t.me/your_bot?start=XXXX that open
|
||||
your bot with a parameter.
|
||||
|
||||
.. seealso:: :attr:`telegram.CallbackQuery.answer`
|
||||
|
||||
Args:
|
||||
callback_query_id (:obj:`str`): Unique identifier for the query to be answered.
|
||||
text (:obj:`str`, optional): Text of the notification. If not specified, nothing will
|
||||
@@ -3359,7 +3451,7 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
|
||||
text: str,
|
||||
chat_id: Union[str, int] = None,
|
||||
message_id: int = None,
|
||||
inline_message_id: int = None,
|
||||
inline_message_id: str = None,
|
||||
parse_mode: ODVInput[str] = DEFAULT_NONE,
|
||||
disable_web_page_preview: ODVInput[bool] = DEFAULT_NONE,
|
||||
reply_markup: InlineKeyboardMarkup = None,
|
||||
@@ -3378,6 +3470,9 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
|
||||
It is currently only possible to edit messages without
|
||||
:attr:`telegram.Message.reply_markup` or with inline keyboards.
|
||||
|
||||
.. seealso:: :attr:`telegram.Message.edit_text`,
|
||||
:attr:`telegram.CallbackQuery.edit_message_text`
|
||||
|
||||
Args:
|
||||
chat_id (:obj:`int` | :obj:`str`, optional): Required if inline_message_id is not
|
||||
specified. Unique identifier for the target chat or username of the target channel
|
||||
@@ -3474,6 +3569,9 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
|
||||
It is currently only possible to edit messages without
|
||||
:attr:`telegram.Message.reply_markup` or with inline keyboards
|
||||
|
||||
.. seealso:: :attr:`telegram.Message.edit_caption`,
|
||||
:attr:`telegram.CallbackQuery.edit_message_caption`
|
||||
|
||||
Args:
|
||||
chat_id (:obj:`int` | :obj:`str`, optional): Required if inline_message_id is not
|
||||
specified. Unique identifier for the target chat or username of the target channel
|
||||
@@ -3548,7 +3646,7 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
|
||||
media: "InputMedia",
|
||||
chat_id: Union[str, int] = None,
|
||||
message_id: int = None,
|
||||
inline_message_id: int = None,
|
||||
inline_message_id: str = None,
|
||||
reply_markup: InlineKeyboardMarkup = None,
|
||||
*,
|
||||
read_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
@@ -3568,6 +3666,9 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
|
||||
It is currently only possible to edit messages without
|
||||
:attr:`telegram.Message.reply_markup` or with inline keyboards
|
||||
|
||||
.. seealso:: :attr:`telegram.Message.edit_media`,
|
||||
:attr:`telegram.CallbackQuery.edit_message_media`
|
||||
|
||||
Args:
|
||||
media (:class:`telegram.InputMedia`): An object for a new media content
|
||||
of the message.
|
||||
@@ -3629,7 +3730,7 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
|
||||
self,
|
||||
chat_id: Union[str, int] = None,
|
||||
message_id: int = None,
|
||||
inline_message_id: int = None,
|
||||
inline_message_id: str = None,
|
||||
reply_markup: Optional["InlineKeyboardMarkup"] = None,
|
||||
*,
|
||||
read_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
@@ -3646,6 +3747,9 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
|
||||
It is currently only possible to edit messages without
|
||||
:attr:`telegram.Message.reply_markup` or with inline keyboards
|
||||
|
||||
.. seealso:: :attr:`telegram.Message.edit_reply_markup`,
|
||||
:attr:`telegram.CallbackQuery.edit_message_reply_markup`
|
||||
|
||||
Args:
|
||||
chat_id (:obj:`int` | :obj:`str`, optional): Required if inline_message_id is not
|
||||
specified. Unique identifier for the target chat or username of the target channel
|
||||
@@ -4002,6 +4106,8 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
|
||||
) -> bool:
|
||||
"""Use this method for your bot to leave a group, supergroup or channel.
|
||||
|
||||
.. seealso:: :attr:`telegram.Chat.leave`
|
||||
|
||||
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``).
|
||||
@@ -4113,6 +4219,8 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
|
||||
"""
|
||||
Use this method to get a list of administrators in a chat.
|
||||
|
||||
.. seealso:: :attr:`telegram.Chat.get_administrators`
|
||||
|
||||
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``).
|
||||
@@ -4168,6 +4276,8 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
|
||||
) -> int:
|
||||
"""Use this method to get the number of members in a chat.
|
||||
|
||||
.. seealso:: :attr:`telegram.Chat.get_member_count`
|
||||
|
||||
.. versionadded:: 13.7
|
||||
|
||||
Args:
|
||||
@@ -4223,6 +4333,8 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
|
||||
) -> ChatMember:
|
||||
"""Use this method to get information about a member of a chat.
|
||||
|
||||
.. seealso:: :attr:`telegram.Chat.get_member`
|
||||
|
||||
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``).
|
||||
@@ -4420,7 +4532,7 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
|
||||
score: int,
|
||||
chat_id: Union[str, int] = None,
|
||||
message_id: int = None,
|
||||
inline_message_id: int = None,
|
||||
inline_message_id: str = None,
|
||||
force: bool = None,
|
||||
disable_edit_message: bool = None,
|
||||
*,
|
||||
@@ -4433,6 +4545,8 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
|
||||
"""
|
||||
Use this method to set the score of the specified user in a game message.
|
||||
|
||||
.. seealso::`telegram.CallbackQuery.set_game_score`
|
||||
|
||||
Args:
|
||||
user_id (:obj:`int`): User identifier.
|
||||
score (:obj:`int`): New score, must be non-negative.
|
||||
@@ -4501,7 +4615,7 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
|
||||
user_id: Union[int, str],
|
||||
chat_id: Union[str, int] = None,
|
||||
message_id: int = None,
|
||||
inline_message_id: int = None,
|
||||
inline_message_id: str = None,
|
||||
*,
|
||||
read_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
write_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
@@ -4518,6 +4632,8 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
|
||||
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.
|
||||
|
||||
.. seealso:: :attr:`telegram.CallbackQuery.get_game_high_scores`
|
||||
|
||||
Args:
|
||||
user_id (:obj:`int`): Target user id.
|
||||
chat_id (:obj:`int` | :obj:`str`, optional): Required if inline_message_id is not
|
||||
@@ -4615,6 +4731,9 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
|
||||
order of the arguments had to be changed. Use keyword arguments to make sure that the
|
||||
arguments are passed correctly.
|
||||
|
||||
.. seealso:: :attr:`telegram.Message.reply_invoice`, :attr:`telegram.Chat.send_invoice`,
|
||||
:attr:`telegram.User.send_invoice`
|
||||
|
||||
.. versionchanged:: 13.5
|
||||
As of Bot API 5.2, the parameter :paramref:`start_parameter` is optional.
|
||||
|
||||
@@ -4798,6 +4917,8 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
|
||||
:class:`telegram.Update` with a :attr:`telegram.Update.shipping_query` field to the bot.
|
||||
Use this method to reply to shipping queries.
|
||||
|
||||
.. seealso:: :attr:`telegram.ShippingQuery.answer`
|
||||
|
||||
Args:
|
||||
shipping_query_id (:obj:`str`): Unique identifier for the query to be answered.
|
||||
ok (:obj:`bool`): Specify :obj:`True` if delivery to the specified address is possible
|
||||
@@ -4875,6 +4996,8 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
|
||||
The Bot API must receive an answer within 10 seconds after the pre-checkout
|
||||
query was sent.
|
||||
|
||||
.. seealso:: :attr:`telegram.PreCheckoutQuery.answer`
|
||||
|
||||
Args:
|
||||
pre_checkout_query_id (:obj:`str`): Unique identifier for the query to be answered.
|
||||
ok (:obj:`bool`): Specify :obj:`True` if everything is alright
|
||||
@@ -5005,7 +5128,8 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
|
||||
the supergroup for this to work and must have the appropriate admin rights. Pass
|
||||
:obj:`True` for all boolean parameters to lift restrictions from a user.
|
||||
|
||||
.. seealso:: :meth:`telegram.ChatPermissions.all_permissions`
|
||||
.. seealso:: :meth:`telegram.ChatPermissions.all_permissions`,
|
||||
:attr:`telegram.Chat.restrict_member`
|
||||
|
||||
Args:
|
||||
chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username
|
||||
@@ -5092,6 +5216,8 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
|
||||
an administrator in the chat for this to work and must have the appropriate admin rights.
|
||||
Pass :obj:`False` for all boolean parameters to demote a user.
|
||||
|
||||
.. seealso:: :attr:`telegram.Chat.promote_member`
|
||||
|
||||
.. versionchanged:: 20.0
|
||||
The argument ``can_manage_voice_chats`` was renamed to
|
||||
:paramref:`can_manage_video_chats` in accordance to Bot API 6.0.
|
||||
@@ -5210,6 +5336,8 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
|
||||
administrator in the group or a supergroup for this to work and must have the
|
||||
:attr:`telegram.ChatMemberAdministrator.can_restrict_members` admin rights.
|
||||
|
||||
.. seealso:: :attr:`telegram.Chat.set_permissions`
|
||||
|
||||
Args:
|
||||
chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username of
|
||||
the target supergroup (in the format `@supergroupusername`).
|
||||
@@ -5267,6 +5395,8 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
|
||||
Use this method to set a custom title for administrators promoted by the bot in a
|
||||
supergroup. The bot must be an administrator for this to work.
|
||||
|
||||
.. seealso:: :attr:`telegram.Chat.set_administrator_custom_title`
|
||||
|
||||
Args:
|
||||
chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username of
|
||||
the target supergroup (in the format `@supergroupusername`).
|
||||
@@ -5327,6 +5457,8 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
|
||||
link is revoked. The bot must be an administrator in the chat for this to work and must
|
||||
have the appropriate admin rights.
|
||||
|
||||
.. seealso:: :attr:`telegram.Chat.export_invite_link`
|
||||
|
||||
Args:
|
||||
chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username
|
||||
of the target channel (in the format ``@channelusername``).
|
||||
@@ -5393,6 +5525,8 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
|
||||
administrator in the chat for this to work and must have the appropriate admin rights.
|
||||
The link can be revoked using the method :meth:`revoke_chat_invite_link`.
|
||||
|
||||
.. seealso:: :attr:`telegram.Chat.create_invite_link`
|
||||
|
||||
.. versionadded:: 13.4
|
||||
|
||||
Args:
|
||||
@@ -5491,6 +5625,8 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
|
||||
parameters to the default values. However, since not documented, this behaviour may
|
||||
change unbeknown to PTB.
|
||||
|
||||
.. seealso:: :attr:`telegram.Chat.edit_invite_link`
|
||||
|
||||
.. versionadded:: 13.4
|
||||
|
||||
Args:
|
||||
@@ -5585,6 +5721,8 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
|
||||
revoked, a new link is automatically generated. The bot must be an administrator in the
|
||||
chat for this to work and must have the appropriate admin rights.
|
||||
|
||||
.. seealso:: :attr:`telegram.Chat.revoke_invite_link`
|
||||
|
||||
.. versionadded:: 13.4
|
||||
|
||||
Args:
|
||||
@@ -5650,6 +5788,9 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
|
||||
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.
|
||||
|
||||
.. seealso:: :attr:`telegram.Chat.approve_join_request`,
|
||||
:attr:`telegram.ChatJoinRequest.approve`, :attr:`telegram.User.approve_join_request`
|
||||
|
||||
.. versionadded:: 13.8
|
||||
|
||||
Args:
|
||||
@@ -5710,6 +5851,9 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
|
||||
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.
|
||||
|
||||
.. seealso:: :attr:`telegram.Chat.decline_join_request`,
|
||||
:attr:`telegram.ChatJoinRequest.decline`, :attr:`telegram.User.decline_join_request`
|
||||
|
||||
.. versionadded:: 13.8
|
||||
|
||||
Args:
|
||||
@@ -5770,6 +5914,8 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
|
||||
Photos can't be changed for private chats. The bot must be an administrator in the chat
|
||||
for this to work and must have the appropriate admin rights.
|
||||
|
||||
.. seealso:: :attr:`telegram.Chat.set_photo`
|
||||
|
||||
Args:
|
||||
chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username
|
||||
of the target channel (in the format ``@channelusername``).
|
||||
@@ -5829,6 +5975,8 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
|
||||
must be an administrator in the chat for this to work and must have the appropriate admin
|
||||
rights.
|
||||
|
||||
.. seealso:: :attr:`telegram.Chat.delete_photo`
|
||||
|
||||
Args:
|
||||
chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username
|
||||
of the target channel (in the format ``@channelusername``).
|
||||
@@ -5885,6 +6033,8 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
|
||||
The bot must be an administrator in the chat for this to work and must have the appropriate
|
||||
admin rights.
|
||||
|
||||
.. seealso:: :attr:`telegram.Chat.set_title`
|
||||
|
||||
Args:
|
||||
chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username
|
||||
of the target channel (in the format ``@channelusername``).
|
||||
@@ -5942,6 +6092,8 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
|
||||
must be an administrator in the chat for this to work and must have the appropriate admin
|
||||
rights.
|
||||
|
||||
.. seealso:: :attr:`telegram.Chat.set_description`
|
||||
|
||||
Args:
|
||||
chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username
|
||||
of the target channel (in the format ``@channelusername``).
|
||||
@@ -6005,6 +6157,8 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
|
||||
right in a supergroup or :attr:`~telegram.ChatMemberAdministrator.can_edit_messages` admin
|
||||
right in a channel.
|
||||
|
||||
.. seealso:: :attr:`telegram.Chat.pin_message`, :attr:`telegram.User.pin_message`
|
||||
|
||||
Args:
|
||||
chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username
|
||||
of the target channel (in the format ``@channelusername``).
|
||||
@@ -6071,6 +6225,8 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
|
||||
right in a supergroup or :attr:`~telegram.ChatMemberAdministrator.can_edit_messages` admin
|
||||
right in a channel.
|
||||
|
||||
.. seealso:: :attr:`telegram.Chat.unpin_message`, :attr:`telegram.User.unpin_message`
|
||||
|
||||
Args:
|
||||
chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username
|
||||
of the target channel (in the format ``@channelusername``).
|
||||
@@ -6133,6 +6289,9 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
|
||||
admin right in a supergroup or :attr:`~telegram.ChatMemberAdministrator.can_edit_messages`
|
||||
admin right in a channel.
|
||||
|
||||
.. seealso:: :attr:`telegram.Chat.unpin_all_messages`,
|
||||
:attr:`telegram.User.unpin_all_messages`
|
||||
|
||||
Args:
|
||||
chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username
|
||||
of the target channel (in the format ``@channelusername``).
|
||||
@@ -6222,6 +6381,61 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
|
||||
)
|
||||
return StickerSet.de_json(result, self) # type: ignore[return-value, arg-type]
|
||||
|
||||
@_log
|
||||
async def get_custom_emoji_stickers(
|
||||
self,
|
||||
custom_emoji_ids: List[str],
|
||||
*,
|
||||
read_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
write_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
connect_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
pool_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
api_kwargs: JSONDict = None,
|
||||
) -> List[Sticker]:
|
||||
# skipcq: FLK-D207
|
||||
"""
|
||||
Use this method to get information about emoji stickers by their identifiers.
|
||||
|
||||
Args:
|
||||
custom_emoji_ids (List[:obj:`str`]): List of custom emoji identifiers.
|
||||
At most :tg-const:`telegram.constants.CustomEmojiStickerLimit.\
|
||||
CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
|
||||
|
||||
Keyword Args:
|
||||
read_timeout (:obj:`float` | :obj:`None`, optional): Value to pass to
|
||||
:paramref:`telegram.request.BaseRequest.post.read_timeout`. Defaults to
|
||||
:attr:`~telegram.request.BaseRequest.DEFAULT_NONE`.
|
||||
write_timeout (:obj:`float` | :obj:`None`, optional): Value to pass to
|
||||
:paramref:`telegram.request.BaseRequest.post.write_timeout`. Defaults to
|
||||
:attr:`~telegram.request.BaseRequest.DEFAULT_NONE`.
|
||||
connect_timeout (:obj:`float` | :obj:`None`, optional): Value to pass to
|
||||
:paramref:`telegram.request.BaseRequest.post.connect_timeout`. Defaults to
|
||||
:attr:`~telegram.request.BaseRequest.DEFAULT_NONE`.
|
||||
pool_timeout (:obj:`float` | :obj:`None`, optional): Value to pass to
|
||||
:paramref:`telegram.request.BaseRequest.post.pool_timeout`. Defaults to
|
||||
:attr:`~telegram.request.BaseRequest.DEFAULT_NONE`.
|
||||
api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the
|
||||
Telegram API.
|
||||
|
||||
Returns:
|
||||
List[:class:`telegram.Sticker`]
|
||||
|
||||
Raises:
|
||||
:class:`telegram.error.TelegramError`
|
||||
|
||||
"""
|
||||
data: JSONDict = {"custom_emoji_ids": custom_emoji_ids}
|
||||
result = await self._post(
|
||||
"getCustomEmojiStickers",
|
||||
data,
|
||||
read_timeout=read_timeout,
|
||||
write_timeout=write_timeout,
|
||||
connect_timeout=connect_timeout,
|
||||
pool_timeout=pool_timeout,
|
||||
api_kwargs=api_kwargs,
|
||||
)
|
||||
return Sticker.de_list(result, self) # type: ignore[return-value, arg-type]
|
||||
|
||||
@_log
|
||||
async def upload_sticker_file(
|
||||
self,
|
||||
@@ -6295,10 +6509,10 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
|
||||
title: str,
|
||||
emojis: str,
|
||||
png_sticker: FileInput = None,
|
||||
contains_masks: bool = None,
|
||||
mask_position: MaskPosition = None,
|
||||
tgs_sticker: FileInput = None,
|
||||
webm_sticker: FileInput = None,
|
||||
sticker_type: str = None,
|
||||
*,
|
||||
read_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
write_timeout: ODVInput[float] = 20,
|
||||
@@ -6309,8 +6523,8 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
|
||||
"""
|
||||
Use this method to create new sticker set owned by a user.
|
||||
The bot will be able to edit the created sticker set.
|
||||
You must use exactly one of the fields :paramref:`png_sticker`, :paramref:`tgs_sticker`, or
|
||||
:paramref:`webm_sticker`.
|
||||
You must use exactly one of the fields :paramref:`png_sticker`, :paramref:`tgs_sticker`,
|
||||
or :paramref:`webm_sticker`.
|
||||
|
||||
Warning:
|
||||
As of API 4.7 :paramref:`png_sticker` is an optional argument and therefore the order
|
||||
@@ -6321,6 +6535,10 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
|
||||
The :paramref:`png_sticker` and :paramref:`tgs_sticker` argument can be either a
|
||||
file_id, an URL or a file from disk ``open(filename, 'rb')``
|
||||
|
||||
.. versionchanged:: 20.0
|
||||
The parameter ``contains_masks`` has been removed. Use :paramref:`sticker_type`
|
||||
instead.
|
||||
|
||||
Args:
|
||||
user_id (:obj:`int`): User identifier of created sticker set owner.
|
||||
name (:obj:`str`): Short name of sticker set, to be used in t.me/addstickers/ URLs
|
||||
@@ -6354,10 +6572,14 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
|
||||
.. versionadded:: 13.11
|
||||
|
||||
emojis (:obj:`str`): One or more emoji corresponding to the sticker.
|
||||
contains_masks (:obj:`bool`, optional): Pass :obj:`True`, if a set of mask stickers
|
||||
should be created.
|
||||
mask_position (:class:`telegram.MaskPosition`, optional): Position where the mask
|
||||
should be placed on faces.
|
||||
sticker_type (:obj:`str`, optional): Type of stickers in the set, pass
|
||||
:attr:`telegram.Sticker.REGULAR` or :attr:`telegram.Sticker.MASK`. Custom emoji
|
||||
sticker sets can't be created via the Bot API at the moment. By default, a
|
||||
regular sticker set is created.
|
||||
|
||||
.. versionadded:: 20.0
|
||||
|
||||
Keyword Args:
|
||||
read_timeout (:obj:`float` | :obj:`None`, optional): Value to pass to
|
||||
@@ -6390,10 +6612,10 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
|
||||
data["tgs_sticker"] = parse_file_input(tgs_sticker)
|
||||
if webm_sticker is not None:
|
||||
data["webm_sticker"] = parse_file_input(webm_sticker)
|
||||
if contains_masks is not None:
|
||||
data["contains_masks"] = contains_masks
|
||||
if mask_position is not None:
|
||||
data["mask_position"] = mask_position
|
||||
if sticker_type is not None:
|
||||
data["sticker_type"] = sticker_type
|
||||
|
||||
result = await self._post(
|
||||
"createNewStickerSet",
|
||||
@@ -6793,6 +7015,9 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
|
||||
"""
|
||||
Use this method to send a native poll.
|
||||
|
||||
.. seealso:: :attr:`telegram.Message.reply_poll`, :attr:`telegram.Chat.send_poll`,
|
||||
:attr:`telegram.User.send_poll`
|
||||
|
||||
Args:
|
||||
chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username
|
||||
of the target channel (in the format ``@channelusername``).
|
||||
@@ -6925,6 +7150,8 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
|
||||
"""
|
||||
Use this method to stop a poll which was sent by the bot.
|
||||
|
||||
.. seealso:: :attr:`telegram.Message.stop_poll`
|
||||
|
||||
Args:
|
||||
chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username
|
||||
of the target channel (in the format ``@channelusername``).
|
||||
@@ -6991,9 +7218,20 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
|
||||
"""
|
||||
Use this method to send an animated emoji that will display a random value.
|
||||
|
||||
.. seealso:: :attr:`telegram.Message.reply_dice`, :attr:`telegram.Chat.send_dice`,
|
||||
:attr:`telegram.User.send_dice`
|
||||
|
||||
Args:
|
||||
chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username
|
||||
of the target channel (in the format ``@channelusername``).
|
||||
disable_notification (:obj:`bool`, optional): Sends the message silently. Users will
|
||||
receive a notification with no sound.
|
||||
reply_to_message_id (:obj:`int`, optional): If the message is a reply, ID of the
|
||||
original message.
|
||||
reply_markup (:class:`InlineKeyboardMarkup` | :class:`ReplyKeyboardMarkup` | \
|
||||
:class:`ReplyKeyboardRemove` | :class:`ForceReply`, optional):
|
||||
Additional interface options. An object for an inline keyboard, custom reply
|
||||
keyboard, instructions to remove reply keyboard or to force a reply from the user
|
||||
emoji (:obj:`str`, optional): Emoji on which the dice throw animation is based.
|
||||
Currently, must be one of :class:`telegram.constants.DiceEmoji`. Dice can have
|
||||
values 1-6 for :tg-const:`telegram.constants.DiceEmoji.DICE`,
|
||||
@@ -7005,23 +7243,14 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
|
||||
:tg-const:`telegram.constants.DiceEmoji.DICE`.
|
||||
|
||||
.. versionchanged:: 13.4
|
||||
Added the :tg-const:`telegram.constants.DiceEmoji.BOWLING` emoji.
|
||||
disable_notification (:obj:`bool`, optional): Sends the message silently. Users will
|
||||
receive a notification with no sound.
|
||||
Added the :tg-const:`telegram.constants.DiceEmoji.BOWLING` emoji..
|
||||
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.
|
||||
protect_content (:obj:`bool`, optional): Protects the contents of the sent message from
|
||||
forwarding and saving.
|
||||
|
||||
.. versionadded:: 13.10
|
||||
|
||||
reply_to_message_id (:obj:`int`, optional): If the message is a reply, ID of the
|
||||
original 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.
|
||||
reply_markup (:class:`InlineKeyboardMarkup` | :class:`ReplyKeyboardMarkup` | \
|
||||
:class:`ReplyKeyboardRemove` | :class:`ForceReply`, optional):
|
||||
Additional interface options. An object for an inline keyboard, custom reply
|
||||
keyboard, instructions to remove reply keyboard or to force a reply from the user.
|
||||
|
||||
Keyword Args:
|
||||
read_timeout (:obj:`float` | :obj:`None`, optional): Value to pass to
|
||||
:paramref:`telegram.request.BaseRequest.post.read_timeout`. Defaults to
|
||||
@@ -7345,7 +7574,7 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
|
||||
return result # type: ignore[return-value]
|
||||
|
||||
@_log
|
||||
def delete_my_commands(
|
||||
async def delete_my_commands(
|
||||
self,
|
||||
scope: BotCommandScope = None,
|
||||
language_code: str = None,
|
||||
@@ -7402,7 +7631,7 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
|
||||
if language_code:
|
||||
data["language_code"] = language_code
|
||||
|
||||
result = self._post(
|
||||
result = await self._post(
|
||||
"deleteMyCommands",
|
||||
data,
|
||||
read_timeout=read_timeout,
|
||||
@@ -7422,6 +7651,7 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
|
||||
write_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
connect_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
pool_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
api_kwargs: JSONDict = None,
|
||||
) -> bool:
|
||||
"""
|
||||
Use this method to log out from the cloud Bot API server before launching the bot locally.
|
||||
@@ -7443,6 +7673,10 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
|
||||
pool_timeout (:obj:`float` | :obj:`None`, optional): Value to pass to
|
||||
:paramref:`telegram.request.BaseRequest.post.pool_timeout`. Defaults to
|
||||
:attr:`~telegram.request.BaseRequest.DEFAULT_NONE`.
|
||||
api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the
|
||||
Telegram API.
|
||||
|
||||
.. versionadded:: 20.0
|
||||
|
||||
Returns:
|
||||
:obj:`True`: On success
|
||||
@@ -7457,6 +7691,7 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
|
||||
write_timeout=write_timeout,
|
||||
connect_timeout=connect_timeout,
|
||||
pool_timeout=pool_timeout,
|
||||
api_kwargs=api_kwargs,
|
||||
)
|
||||
|
||||
@_log
|
||||
@@ -7533,6 +7768,10 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
|
||||
be copied. The method is analogous to the method :meth:`forward_message`, but the copied
|
||||
message doesn't have a link to the original message.
|
||||
|
||||
.. seealso:: :attr:`telegram.Message.copy`, :attr:`telegram.Chat.send_copy`,
|
||||
:attr:`telegram.Chat.copy_message`, :attr:`telegram.User.send_copy`,
|
||||
:attr:`telegram.User.copy_message`
|
||||
|
||||
Args:
|
||||
chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username
|
||||
of the target channel (in the format ``@channelusername``).
|
||||
@@ -7584,6 +7823,7 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
|
||||
|
||||
Raises:
|
||||
:class:`telegram.error.TelegramError`
|
||||
|
||||
"""
|
||||
data: JSONDict = {
|
||||
"chat_id": chat_id,
|
||||
@@ -7630,7 +7870,8 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
|
||||
button.
|
||||
|
||||
.. seealso:: :meth:`get_chat_menu_button`, :meth:`telegram.Chat.set_menu_button`,
|
||||
:meth:`telegram.User.set_menu_button`
|
||||
:meth:`telegram.Chat.get_menu_button`, meth:`telegram.User.set_menu_button`,
|
||||
:meth:`telegram.User.get_menu_button`
|
||||
|
||||
.. versionadded:: 20.0
|
||||
|
||||
@@ -7658,6 +7899,7 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
|
||||
|
||||
Returns:
|
||||
:obj:`bool`: On success, :obj:`True` is returned.
|
||||
|
||||
"""
|
||||
data: JSONDict = {}
|
||||
if chat_id is not None:
|
||||
@@ -7690,7 +7932,8 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
|
||||
the default menu button.
|
||||
|
||||
.. seealso:: :meth:`set_chat_menu_button`, :meth:`telegram.Chat.get_menu_button`,
|
||||
:meth:`telegram.User.get_menu_button`
|
||||
:meth:`telegram.Chat.set_menu_button`, :meth:`telegram.User.get_menu_button`,
|
||||
:meth:`telegram.User.set_menu_button`
|
||||
|
||||
.. versionadded:: 20.0
|
||||
|
||||
@@ -7716,6 +7959,7 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
|
||||
|
||||
Returns:
|
||||
:class:`telegram.MenuButton`: On success, the current menu button is returned.
|
||||
|
||||
"""
|
||||
data = {}
|
||||
if chat_id is not None:
|
||||
@@ -8033,6 +8277,8 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
|
||||
"""Alias for :meth:`unpin_chat_message`"""
|
||||
unpinAllChatMessages = unpin_all_chat_messages
|
||||
"""Alias for :meth:`unpin_all_chat_messages`"""
|
||||
getCustomEmojiStickers = get_custom_emoji_stickers
|
||||
"""Alias for :meth:`get_custom_emoji_stickers`"""
|
||||
getStickerSet = get_sticker_set
|
||||
"""Alias for :meth:`get_sticker_set`"""
|
||||
uploadStickerFile = upload_sticker_file
|
||||
|
||||
+16
-2
@@ -134,6 +134,12 @@ class Chat(TelegramObject):
|
||||
:meth:`telegram.Bot.get_chat`.
|
||||
|
||||
.. versionadded:: 20.0
|
||||
has_restricted_voice_and_video_messages (:obj:`bool`, optional): :obj:`True`, if the
|
||||
privacy settings of the other party restrict sending voice and video note messages
|
||||
in the private chat. Returned only in :meth:`telegram.Bot.get_chat`.
|
||||
|
||||
.. versionadded:: 20.0
|
||||
|
||||
**kwargs (:obj:`dict`): Arbitrary keyword arguments.
|
||||
|
||||
Attributes:
|
||||
@@ -187,6 +193,11 @@ class Chat(TelegramObject):
|
||||
joining the supergroup need to be approved by supergroup administrators. Returned only
|
||||
in :meth:`telegram.Bot.get_chat`.
|
||||
|
||||
.. versionadded:: 20.0
|
||||
has_restricted_voice_and_video_messages (:obj:`bool`): Optional. :obj:`True`, if the
|
||||
privacy settings of the other party restrict sending voice and video note messages
|
||||
in the private chat. Returned only in :meth:`telegram.Bot.get_chat`.
|
||||
|
||||
.. versionadded:: 20.0
|
||||
|
||||
"""
|
||||
@@ -215,6 +226,7 @@ class Chat(TelegramObject):
|
||||
"has_private_forwards",
|
||||
"join_to_send_messages",
|
||||
"join_by_request",
|
||||
"has_restricted_voice_and_video_messages",
|
||||
)
|
||||
|
||||
SENDER: ClassVar[str] = constants.ChatType.SENDER
|
||||
@@ -256,6 +268,7 @@ class Chat(TelegramObject):
|
||||
has_protected_content: bool = None,
|
||||
join_to_send_messages: bool = None,
|
||||
join_by_request: bool = None,
|
||||
has_restricted_voice_and_video_messages: bool = None,
|
||||
**_kwargs: Any,
|
||||
):
|
||||
# Required
|
||||
@@ -286,6 +299,7 @@ class Chat(TelegramObject):
|
||||
self.location = location
|
||||
self.join_to_send_messages = join_to_send_messages
|
||||
self.join_by_request = join_by_request
|
||||
self.has_restricted_voice_and_video_messages = has_restricted_voice_and_video_messages
|
||||
|
||||
self.set_bot(bot)
|
||||
self._id_attrs = (self.id,)
|
||||
@@ -2375,7 +2389,7 @@ class Chat(TelegramObject):
|
||||
Caution:
|
||||
Can only work, if the chat is a private chat.
|
||||
|
||||
..seealso:: :meth:`get_menu_button`
|
||||
.. seealso:: :meth:`get_menu_button`
|
||||
|
||||
.. versionadded:: 20.0
|
||||
|
||||
@@ -2411,7 +2425,7 @@ class Chat(TelegramObject):
|
||||
Caution:
|
||||
Can only work, if the chat is a private chat.
|
||||
|
||||
..seealso:: :meth:`set_menu_button`
|
||||
.. seealso:: :meth:`set_menu_button`
|
||||
|
||||
.. versionadded:: 20.0
|
||||
|
||||
|
||||
@@ -44,6 +44,8 @@ class ChatMember(TelegramObject):
|
||||
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.
|
||||
|
||||
.. seealso:: `Chat Member Example <examples.chatmemberbot.html>`_
|
||||
|
||||
.. versionchanged:: 20.0
|
||||
|
||||
* As of Bot API 5.3, :class:`ChatMember` is nothing but the base class for the subclasses
|
||||
|
||||
@@ -44,7 +44,7 @@ class File(TelegramObject):
|
||||
|
||||
Note:
|
||||
* Maximum file size to download is
|
||||
:tg-const:`telegram.constants.FileSizeLimit.FILESIZE_DOWNLOAD`.
|
||||
:tg-const:`telegram.constants.FileSizeLimit.FILESIZE_DOWNLOAD`.
|
||||
* If you obtain an instance of this class from :attr:`telegram.PassportFile.get_file`,
|
||||
then it will automatically be decrypted as it downloads when you call :meth:`download()`.
|
||||
|
||||
@@ -111,7 +111,7 @@ class File(TelegramObject):
|
||||
original filename as reported by Telegram. If the file has no filename, it the file ID will
|
||||
be used as filename. If a :paramref:`custom_path` is supplied, it will be saved to that
|
||||
path instead. If :paramref:`out` is defined, the file contents will be saved to that object
|
||||
using the ``out.write`` method.
|
||||
using the :obj:`out.write<io.BufferedWriter.write>` method.
|
||||
|
||||
Note:
|
||||
* :paramref:`custom_path` and :paramref:`out` are mutually exclusive.
|
||||
@@ -144,8 +144,8 @@ class File(TelegramObject):
|
||||
|
||||
Returns:
|
||||
:class:`pathlib.Path` | :obj:`io.BufferedWriter`: The same object as :paramref:`out` if
|
||||
specified. Otherwise, returns the filename downloaded to or the file path of the
|
||||
local file.
|
||||
specified. Otherwise, returns the filename downloaded to or the file path of the
|
||||
local file.
|
||||
|
||||
Raises:
|
||||
ValueError: If both :paramref:`custom_path` and :paramref:`out` are passed.
|
||||
@@ -214,7 +214,7 @@ class File(TelegramObject):
|
||||
|
||||
Returns:
|
||||
:obj:`bytearray`: The same object as :paramref:`buf` if it was specified. Otherwise a
|
||||
newly allocated :obj:`bytearray`.
|
||||
newly allocated :obj:`bytearray`.
|
||||
|
||||
"""
|
||||
if buf is None:
|
||||
|
||||
@@ -54,6 +54,11 @@ class Sticker(_BaseThumbedMedium):
|
||||
is_video (:obj:`bool`): :obj:`True`, if the sticker is a video sticker.
|
||||
|
||||
.. versionadded:: 13.11
|
||||
type (:obj:`str`): Type of the sticker. Currently one of :attr:`REGULAR`,
|
||||
:attr:`MASK`, :attr:`CUSTOM_EMOJI`. The type of the sticker is independent from its
|
||||
format, which is determined by the fields :attr:`is_animated` and :attr:`is_video`.
|
||||
|
||||
.. versionadded:: 20.0
|
||||
thumb (:class:`telegram.PhotoSize`, optional): Sticker thumbnail in the ``.WEBP`` or
|
||||
``.JPG`` format.
|
||||
emoji (:obj:`str`, optional): Emoji associated with the sticker
|
||||
@@ -63,8 +68,12 @@ class Sticker(_BaseThumbedMedium):
|
||||
position where the mask should be placed.
|
||||
file_size (:obj:`int`, optional): File size in bytes.
|
||||
bot (:class:`telegram.Bot`, optional): The Bot to use for instance methods.
|
||||
premium_animation (:class:`telegram.File`, optional): Premium animation for the sticker,
|
||||
if the sticker is premium.
|
||||
premium_animation (:class:`telegram.File`, optional): For premium regular stickers,
|
||||
premium animation for the sticker.
|
||||
|
||||
.. versionadded:: 20.0
|
||||
custom_emoji (:obj:`str`, optional): For custom emoji stickers, unique identifier of the
|
||||
custom emoji.
|
||||
|
||||
.. versionadded:: 20.0
|
||||
_kwargs (:obj:`dict`): Arbitrary keyword arguments.
|
||||
@@ -80,6 +89,11 @@ class Sticker(_BaseThumbedMedium):
|
||||
is_video (:obj:`bool`): :obj:`True`, if the sticker is a video sticker.
|
||||
|
||||
.. versionadded:: 13.11
|
||||
type (:obj:`str`): Type of the sticker. Currently one of :attr:`REGULAR`,
|
||||
:attr:`MASK`, :attr:`CUSTOM_EMOJI`. The type of the sticker is independent from its
|
||||
format, which is determined by the fields :attr:`is_animated` and :attr:`is_video`.
|
||||
|
||||
.. versionadded:: 20.0
|
||||
thumb (:class:`telegram.PhotoSize`): Optional. Sticker thumbnail in the ``.WEBP`` or
|
||||
``.JPG`` format.
|
||||
emoji (:obj:`str`): Optional. Emoji associated with the sticker.
|
||||
@@ -88,11 +102,14 @@ class Sticker(_BaseThumbedMedium):
|
||||
where the mask should be placed.
|
||||
file_size (:obj:`int`): Optional. File size in bytes.
|
||||
bot (:class:`telegram.Bot`): Optional. The Bot to use for instance methods.
|
||||
premium_animation (:class:`telegram.File`): Optional. Premium animation for the
|
||||
sticker, if the sticker is premium.
|
||||
premium_animation (:class:`telegram.File`): Optional. For premium regular stickers,
|
||||
premium animation for the sticker.
|
||||
|
||||
.. versionadded:: 20.0
|
||||
custom_emoji (:obj:`str`): Optional. For custom emoji stickers, unique identifier of the
|
||||
custom emoji.
|
||||
|
||||
.. versionadded:: 20.0
|
||||
"""
|
||||
|
||||
__slots__ = (
|
||||
@@ -104,6 +121,8 @@ class Sticker(_BaseThumbedMedium):
|
||||
"set_name",
|
||||
"width",
|
||||
"premium_animation",
|
||||
"type",
|
||||
"custom_emoji_id",
|
||||
)
|
||||
|
||||
def __init__(
|
||||
@@ -114,6 +133,7 @@ class Sticker(_BaseThumbedMedium):
|
||||
height: int,
|
||||
is_animated: bool,
|
||||
is_video: bool,
|
||||
type: str, # pylint: disable=redefined-builtin
|
||||
thumb: PhotoSize = None,
|
||||
emoji: str = None,
|
||||
file_size: int = None,
|
||||
@@ -121,6 +141,7 @@ class Sticker(_BaseThumbedMedium):
|
||||
mask_position: "MaskPosition" = None,
|
||||
bot: "Bot" = None,
|
||||
premium_animation: "File" = None,
|
||||
custom_emoji_id: str = None,
|
||||
**_kwargs: Any,
|
||||
):
|
||||
super().__init__(
|
||||
@@ -135,11 +156,20 @@ class Sticker(_BaseThumbedMedium):
|
||||
self.height = height
|
||||
self.is_animated = is_animated
|
||||
self.is_video = is_video
|
||||
self.type = type
|
||||
# Optional
|
||||
self.emoji = emoji
|
||||
self.set_name = set_name
|
||||
self.mask_position = mask_position
|
||||
self.premium_animation = premium_animation
|
||||
self.custom_emoji_id = custom_emoji_id
|
||||
|
||||
REGULAR: ClassVar[str] = constants.StickerType.REGULAR
|
||||
""":const:`telegram.constants.StickerType.REGULAR`"""
|
||||
MASK: ClassVar[str] = constants.StickerType.MASK
|
||||
""":const:`telegram.constants.StickerType.MASK`"""
|
||||
CUSTOM_EMOJI: ClassVar[str] = constants.StickerType.CUSTOM_EMOJI
|
||||
""":const:`telegram.constants.StickerType.CUSTOM_EMOJI`"""
|
||||
|
||||
@classmethod
|
||||
def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["Sticker"]:
|
||||
@@ -167,6 +197,9 @@ class StickerSet(TelegramObject):
|
||||
arguments had to be changed. Use keyword arguments to make sure that the arguments are
|
||||
passed correctly.
|
||||
|
||||
.. versionchanged:: 20.0:
|
||||
The parameter ``contains_masks`` has been removed. Use :paramref:`sticker_type` instead.
|
||||
|
||||
Args:
|
||||
name (:obj:`str`): Sticker set name.
|
||||
title (:obj:`str`): Sticker set title.
|
||||
@@ -174,8 +207,12 @@ class StickerSet(TelegramObject):
|
||||
is_video (:obj:`bool`): :obj:`True`, if the sticker set contains video stickers.
|
||||
|
||||
.. versionadded:: 13.11
|
||||
contains_masks (:obj:`bool`): :obj:`True`, if the sticker set contains masks.
|
||||
stickers (List[:class:`telegram.Sticker`]): List of all set stickers.
|
||||
sticker_type (:obj:`str`): Type of stickers in the set, currently one of
|
||||
:attr:`telegram.Sticker.REGULAR`, :attr:`telegram.Sticker.MASK`,
|
||||
:attr:`telegram.Sticker.CUSTOM_EMOJI`.
|
||||
|
||||
.. versionadded:: 20.0
|
||||
thumb (:class:`telegram.PhotoSize`, optional): Sticker set thumbnail in the ``.WEBP``,
|
||||
``.TGS``, or ``.WEBM`` format.
|
||||
|
||||
@@ -186,21 +223,23 @@ class StickerSet(TelegramObject):
|
||||
is_video (:obj:`bool`): :obj:`True`, if the sticker set contains video stickers.
|
||||
|
||||
.. versionadded:: 13.11
|
||||
contains_masks (:obj:`bool`): :obj:`True`, if the sticker set contains masks.
|
||||
stickers (List[:class:`telegram.Sticker`]): List of all set stickers.
|
||||
sticker_type (:obj:`str`): Type of stickers in the set.
|
||||
|
||||
.. versionadded:: 20.0
|
||||
thumb (:class:`telegram.PhotoSize`): Optional. Sticker set thumbnail in the ``.WEBP``,
|
||||
``.TGS`` or ``.WEBM`` format.
|
||||
|
||||
"""
|
||||
|
||||
__slots__ = (
|
||||
"contains_masks",
|
||||
"is_animated",
|
||||
"is_video",
|
||||
"name",
|
||||
"stickers",
|
||||
"thumb",
|
||||
"title",
|
||||
"sticker_type",
|
||||
)
|
||||
|
||||
def __init__(
|
||||
@@ -208,9 +247,9 @@ class StickerSet(TelegramObject):
|
||||
name: str,
|
||||
title: str,
|
||||
is_animated: bool,
|
||||
contains_masks: bool,
|
||||
stickers: List[Sticker],
|
||||
is_video: bool,
|
||||
sticker_type: str,
|
||||
thumb: PhotoSize = None,
|
||||
**_kwargs: Any,
|
||||
):
|
||||
@@ -218,8 +257,8 @@ class StickerSet(TelegramObject):
|
||||
self.title = title
|
||||
self.is_animated = is_animated
|
||||
self.is_video = is_video
|
||||
self.contains_masks = contains_masks
|
||||
self.stickers = stickers
|
||||
self.sticker_type = sticker_type
|
||||
# Optional
|
||||
self.thumb = thumb
|
||||
|
||||
|
||||
@@ -62,6 +62,10 @@ class InlineKeyboardButton(TelegramObject):
|
||||
|
||||
* After Bot API 6.1, only ``HTTPS`` links will be allowed in :paramref:`login_url`.
|
||||
|
||||
.. seealso:: `Inline Keyboard Example 1 <examples.inlinekeyboard.html>`_,
|
||||
`Inline Keyboard Example 2 <examples.inlinekeyboard2.html>`_,
|
||||
:class:`telegram.InlineKeyboardMarkup`
|
||||
|
||||
.. versionchanged:: 20.0
|
||||
:attr:`web_app` is considered as well when comparing objects of this type in terms of
|
||||
equality.
|
||||
|
||||
@@ -36,6 +36,9 @@ class InlineKeyboardMarkup(TelegramObject):
|
||||
Objects of this class are comparable in terms of equality. Two objects of this class are
|
||||
considered equal, if their size of :attr:`inline_keyboard` and all the buttons are equal.
|
||||
|
||||
.. seealso:: `Inline Keyboard Example 1 <examples.inlinekeyboard.html>`_,
|
||||
`Inline Keyboard Example 2 <examples.inlinekeyboard2.html>`_
|
||||
|
||||
Args:
|
||||
inline_keyboard (List[List[:class:`telegram.InlineKeyboardButton`]]): List of button rows,
|
||||
each represented by a list of InlineKeyboardButton objects.
|
||||
|
||||
@@ -31,6 +31,8 @@ if TYPE_CHECKING:
|
||||
class InlineQueryResultArticle(InlineQueryResult):
|
||||
"""This object represents a Telegram InlineQueryResultArticle.
|
||||
|
||||
.. seealso:: `Inline Example <examples.inlinebot.html>`_
|
||||
|
||||
Args:
|
||||
id (:obj:`str`): Unique identifier for this result, 1-64 Bytes.
|
||||
title (:obj:`str`): Title of the result.
|
||||
|
||||
@@ -33,6 +33,8 @@ class InputTextMessageContent(InputMessageContent):
|
||||
Objects of this class are comparable in terms of equality. Two objects of this class are
|
||||
considered equal, if their :attr:`message_text` is equal.
|
||||
|
||||
.. seealso:: `Inline Example <examples.inlinebot.html>`_
|
||||
|
||||
Args:
|
||||
message_text (:obj:`str`): Text of the message to be sent,
|
||||
1-:tg-const:`telegram.constants.MessageLimit.TEXT_LENGTH` characters after entities
|
||||
|
||||
@@ -29,6 +29,8 @@ class KeyboardButtonPollType(TelegramObject):
|
||||
Objects of this class are comparable in terms of equality. Two objects of this class are
|
||||
considered equal, if their :attr:`type` is equal.
|
||||
|
||||
.. seealso:: `Pollbot Example <examples.pollbot.html>`_
|
||||
|
||||
Attributes:
|
||||
type (:obj:`str`): Optional. If :tg-const:`telegram.Poll.QUIZ` is passed, the user will be
|
||||
allowed to create only polls in the quiz mode. If :tg-const:`telegram.Poll.REGULAR` is
|
||||
|
||||
@@ -370,6 +370,8 @@ class Message(TelegramObject):
|
||||
to the message.
|
||||
bot (:class:`telegram.Bot`): Optional. The Bot to use for instance methods.
|
||||
|
||||
.. |custom_emoji_formatting_note| replace:: Custom emoji entities will currently be ignored
|
||||
by this function. Instead, the supplied replacement for the emoji will be used.
|
||||
"""
|
||||
|
||||
# fmt: on
|
||||
@@ -2864,6 +2866,9 @@ class Message(TelegramObject):
|
||||
Use this if you want to retrieve the message text with the entities formatted as HTML in
|
||||
the same way the original message was formatted.
|
||||
|
||||
Note:
|
||||
|custom_emoji_formatting_note|
|
||||
|
||||
.. versionchanged:: 13.10
|
||||
Spoiler entities are now formatted as HTML.
|
||||
|
||||
@@ -2880,6 +2885,9 @@ class Message(TelegramObject):
|
||||
Use this if you want to retrieve the message text with the entities formatted as HTML.
|
||||
This also formats :attr:`telegram.MessageEntity.URL` as a hyperlink.
|
||||
|
||||
Note:
|
||||
|custom_emoji_formatting_note|
|
||||
|
||||
.. versionchanged:: 13.10
|
||||
Spoiler entities are now formatted as HTML.
|
||||
|
||||
@@ -2897,6 +2905,9 @@ class Message(TelegramObject):
|
||||
Use this if you want to retrieve the message caption with the caption entities formatted as
|
||||
HTML in the same way the original message was formatted.
|
||||
|
||||
Note:
|
||||
|custom_emoji_formatting_note|
|
||||
|
||||
.. versionchanged:: 13.10
|
||||
Spoiler entities are now formatted as HTML.
|
||||
|
||||
@@ -2913,6 +2924,9 @@ class Message(TelegramObject):
|
||||
Use this if you want to retrieve the message caption with the caption entities formatted as
|
||||
HTML. This also formats :attr:`telegram.MessageEntity.URL` as a hyperlink.
|
||||
|
||||
Note:
|
||||
|custom_emoji_formatting_note|
|
||||
|
||||
.. versionchanged:: 13.10
|
||||
Spoiler entities are now formatted as HTML.
|
||||
|
||||
@@ -3093,6 +3107,8 @@ class Message(TelegramObject):
|
||||
:tg-const:`telegram.constants.ParseMode.MARKDOWN` is a legacy mode, retained by
|
||||
Telegram for backward compatibility. You should use :meth:`text_markdown_v2` instead.
|
||||
|
||||
|custom_emoji_formatting_note|
|
||||
|
||||
Returns:
|
||||
:obj:`str`: Message text with entities formatted as Markdown.
|
||||
|
||||
@@ -3111,6 +3127,9 @@ class Message(TelegramObject):
|
||||
Use this if you want to retrieve the message text with the entities formatted as Markdown
|
||||
in the same way the original message was formatted.
|
||||
|
||||
Note:
|
||||
|custom_emoji_formatting_note|
|
||||
|
||||
.. versionchanged:: 13.10
|
||||
Spoiler entities are now formatted as Markdown V2.
|
||||
|
||||
@@ -3132,6 +3151,8 @@ class Message(TelegramObject):
|
||||
Telegram for backward compatibility. You should use :meth:`text_markdown_v2_urled`
|
||||
instead.
|
||||
|
||||
|custom_emoji_formatting_note|
|
||||
|
||||
Returns:
|
||||
:obj:`str`: Message text with entities formatted as Markdown.
|
||||
|
||||
@@ -3150,6 +3171,9 @@ class Message(TelegramObject):
|
||||
Use this if you want to retrieve the message text with the entities formatted as Markdown.
|
||||
This also formats :attr:`telegram.MessageEntity.URL` as a hyperlink.
|
||||
|
||||
Note:
|
||||
|custom_emoji_formatting_note|
|
||||
|
||||
.. versionchanged:: 13.10
|
||||
Spoiler entities are now formatted as Markdown V2.
|
||||
|
||||
@@ -3171,6 +3195,8 @@ class Message(TelegramObject):
|
||||
Telegram for backward compatibility. You should use :meth:`caption_markdown_v2`
|
||||
instead.
|
||||
|
||||
|custom_emoji_formatting_note|
|
||||
|
||||
Returns:
|
||||
:obj:`str`: Message caption with caption entities formatted as Markdown.
|
||||
|
||||
@@ -3189,6 +3215,9 @@ class Message(TelegramObject):
|
||||
Use this if you want to retrieve the message caption with the caption entities formatted as
|
||||
Markdown in the same way the original message was formatted.
|
||||
|
||||
Note:
|
||||
|custom_emoji_formatting_note|
|
||||
|
||||
.. versionchanged:: 13.10
|
||||
Spoiler entities are now formatted as Markdown V2.
|
||||
|
||||
@@ -3212,6 +3241,8 @@ class Message(TelegramObject):
|
||||
Telegram for backward compatibility. You should use :meth:`caption_markdown_v2_urled`
|
||||
instead.
|
||||
|
||||
|custom_emoji_formatting_note|
|
||||
|
||||
Returns:
|
||||
:obj:`str`: Message caption with caption entities formatted as Markdown.
|
||||
|
||||
@@ -3230,6 +3261,9 @@ class Message(TelegramObject):
|
||||
Use this if you want to retrieve the message caption with the caption entities formatted as
|
||||
Markdown. This also formats :attr:`telegram.MessageEntity.URL` as a hyperlink.
|
||||
|
||||
Note:
|
||||
|custom_emoji_formatting_note|
|
||||
|
||||
.. versionchanged:: 13.10
|
||||
Spoiler entities are now formatted as Markdown V2.
|
||||
|
||||
|
||||
@@ -44,7 +44,11 @@ class MessageEntity(TelegramObject):
|
||||
:attr:`URL`, :attr:`EMAIL`, :attr:`PHONE_NUMBER`, :attr:`BOLD` (bold text),
|
||||
:attr:`ITALIC` (italic text), :attr:`STRIKETHROUGH`, :attr:`SPOILER` (spoiler message),
|
||||
:attr:`CODE` (monowidth string), :attr:`PRE` (monowidth block), :attr:`TEXT_LINK` (for
|
||||
clickable text URLs), :attr:`TEXT_MENTION` (for users without usernames).
|
||||
clickable text URLs), :attr:`TEXT_MENTION` (for users without usernames),
|
||||
:attr:`CUSTOM_EMOJI` (for inline custom emoji stickers).
|
||||
|
||||
.. versionadded:: 20.0
|
||||
added inline custom emoji
|
||||
offset (:obj:`int`): Offset in UTF-16 code units to the start of the entity.
|
||||
length (:obj:`int`): Length of the entity in UTF-16 code units.
|
||||
url (:obj:`str`, optional): For :attr:`TEXT_LINK` only, url that will be opened after
|
||||
@@ -53,6 +57,11 @@ class MessageEntity(TelegramObject):
|
||||
user.
|
||||
language (:obj:`str`, optional): For :attr:`PRE` only, the programming language of
|
||||
the entity text.
|
||||
custom_emoji_id (:obj:`str`, optional): For :attr:`CUSTOM_EMOJI` only, unique identifier
|
||||
of the custom emoji. Use :meth:`telegram.Bot.get_custom_emoji_stickers` to get full
|
||||
information about the sticker.
|
||||
|
||||
.. versionadded:: 20.0
|
||||
|
||||
Attributes:
|
||||
type (:obj:`str`): Type of the entity.
|
||||
@@ -61,10 +70,13 @@ class MessageEntity(TelegramObject):
|
||||
url (:obj:`str`): Optional. Url that will be opened after user taps on the text.
|
||||
user (:class:`telegram.User`): Optional. The mentioned user.
|
||||
language (:obj:`str`): Optional. Programming language of the entity text.
|
||||
custom_emoji_id (:obj:`str`): Optional. Unique identifier of the custom emoji.
|
||||
|
||||
.. versionadded:: 20.0
|
||||
|
||||
"""
|
||||
|
||||
__slots__ = ("length", "url", "user", "type", "language", "offset")
|
||||
__slots__ = ("length", "url", "user", "type", "language", "offset", "custom_emoji_id")
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -74,6 +86,7 @@ class MessageEntity(TelegramObject):
|
||||
url: str = None,
|
||||
user: User = None,
|
||||
language: str = None,
|
||||
custom_emoji_id: str = None,
|
||||
**_kwargs: Any,
|
||||
):
|
||||
# Required
|
||||
@@ -84,6 +97,7 @@ class MessageEntity(TelegramObject):
|
||||
self.url = url
|
||||
self.user = user
|
||||
self.language = language
|
||||
self.custom_emoji_id = custom_emoji_id
|
||||
|
||||
self._id_attrs = (self.type, self.offset, self.length)
|
||||
|
||||
@@ -134,5 +148,10 @@ class MessageEntity(TelegramObject):
|
||||
|
||||
.. versionadded:: 13.10
|
||||
"""
|
||||
CUSTOM_EMOJI: ClassVar[str] = constants.MessageEntityType.CUSTOM_EMOJI
|
||||
""":const:`telegram.constants.MessageEntityType.CUSTOM_EMOJI`
|
||||
|
||||
.. versionadded:: 20.0
|
||||
"""
|
||||
ALL_TYPES: ClassVar[List[str]] = list(constants.MessageEntityType)
|
||||
"""List[:obj:`str`]: A list of all available message entity types."""
|
||||
|
||||
@@ -29,6 +29,8 @@ class LabeledPrice(TelegramObject):
|
||||
Objects of this class are comparable in terms of equality. Two objects of this class are
|
||||
considered equal, if their :attr:`label` and :attr:`amount` are equal.
|
||||
|
||||
.. seealso:: `Paymentbot Example <examples.paymentbot.html>`_
|
||||
|
||||
Args:
|
||||
label (:obj:`str`): Portion label.
|
||||
amount (:obj:`int`): Price of the product in the smallest units of the currency (integer,
|
||||
|
||||
@@ -33,6 +33,8 @@ class ShippingOption(TelegramObject):
|
||||
Objects of this class are comparable in terms of equality. Two objects of this class are
|
||||
considered equal, if their :attr:`id` is equal.
|
||||
|
||||
.. seealso:: `Paymentbot Example <examples.paymentbot.html>`_
|
||||
|
||||
Args:
|
||||
id (:obj:`str`): Shipping option identifier.
|
||||
title (:obj:`str`): Option title.
|
||||
|
||||
@@ -112,6 +112,8 @@ class Poll(TelegramObject):
|
||||
Objects of this class are comparable in terms of equality. Two objects of this class are
|
||||
considered equal, if their :attr:`id` is equal.
|
||||
|
||||
.. seealso:: `Pollbot Example <examples.pollbot.html>`_
|
||||
|
||||
Args:
|
||||
id (:obj:`str`): Unique poll identifier.
|
||||
question (:obj:`str`): Poll question, 1-300 characters.
|
||||
|
||||
+2
-2
@@ -1463,7 +1463,7 @@ class User(TelegramObject):
|
||||
For the documentation of the arguments, please see
|
||||
:meth:`telegram.Bot.set_chat_menu_button`.
|
||||
|
||||
..seealso:: :meth:`get_menu_button`
|
||||
.. seealso:: :meth:`get_menu_button`
|
||||
|
||||
.. versionadded:: 20.0
|
||||
|
||||
@@ -1496,7 +1496,7 @@ class User(TelegramObject):
|
||||
For the documentation of the arguments, please see
|
||||
:meth:`telegram.Bot.get_chat_menu_button`.
|
||||
|
||||
..seealso:: :meth:`set_menu_button`
|
||||
.. seealso:: :meth:`set_menu_button`
|
||||
|
||||
.. versionadded:: 20.0
|
||||
|
||||
|
||||
+32
-8
@@ -23,30 +23,54 @@ Warning:
|
||||
user. Changes to this module are not considered breaking changes and may not be documented in
|
||||
the changelog.
|
||||
"""
|
||||
from enum import Enum
|
||||
import enum as _enum
|
||||
import sys
|
||||
from typing import Type, TypeVar, Union
|
||||
|
||||
_A = TypeVar("_A")
|
||||
_B = TypeVar("_B")
|
||||
_Enum = TypeVar("_Enum", bound=Enum)
|
||||
_Enum = TypeVar("_Enum", bound=_enum.Enum)
|
||||
|
||||
|
||||
def get_member(enum: Type[_Enum], value: _A, default: _B) -> Union[_Enum, _A, _B]:
|
||||
"""Tries to call ``enum(value)`` to convert the value into an enumeration member.
|
||||
def get_member(enum_cls: Type[_Enum], value: _A, default: _B) -> Union[_Enum, _A, _B]:
|
||||
"""Tries to call ``enum_cls(value)`` to convert the value into an enumeration member.
|
||||
If that fails, the ``default`` is returned.
|
||||
"""
|
||||
try:
|
||||
return enum(value)
|
||||
return enum_cls(value)
|
||||
except ValueError:
|
||||
return default
|
||||
|
||||
|
||||
class StringEnum(str, Enum):
|
||||
"""Helper class for string enums where the value is not important to be displayed on
|
||||
stringification.
|
||||
# Python 3.11 and above has a different output for mixin classes for IntEnum, StrEnum and IntFlag
|
||||
# see https://docs.python.org/3.11/library/enum.html#notes. We want e.g. str(StrEnumTest.FOO) to
|
||||
# return "foo" instead of "StrEnumTest.FOO", which is not the case < py3.11
|
||||
class StringEnum(str, _enum.Enum):
|
||||
"""Helper class for string enums where ``str(member)`` prints the value, but ``repr(member)``
|
||||
gives ``EnumName.MEMBER_NAME``.
|
||||
"""
|
||||
|
||||
__slots__ = ()
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<{self.__class__.__name__}.{self.name}>"
|
||||
|
||||
def __str__(self) -> str:
|
||||
return str.__str__(self)
|
||||
|
||||
|
||||
# Apply the __repr__ modification and __str__ fix to IntEnum
|
||||
class IntEnum(_enum.IntEnum):
|
||||
"""Helper class for int enums where ``str(member)`` prints the value, but ``repr(member)``
|
||||
gives ``EnumName.MEMBER_NAME``.
|
||||
"""
|
||||
|
||||
__slots__ = ()
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<{self.__class__.__name__}.{self.name}>"
|
||||
|
||||
if sys.version_info < (3, 11):
|
||||
|
||||
def __str__(self) -> str:
|
||||
return str(self.value)
|
||||
|
||||
@@ -50,7 +50,7 @@ class Version(NamedTuple):
|
||||
return version
|
||||
|
||||
|
||||
__version_info__ = Version(major=20, minor=0, micro=0, releaselevel="alpha", serial=2)
|
||||
__version_info__ = Version(major=20, minor=0, micro=0, releaselevel="alpha", serial=4)
|
||||
__version__ = str(__version_info__)
|
||||
|
||||
# # SETUP.PY MARKER
|
||||
|
||||
@@ -29,6 +29,8 @@ class WebAppData(TelegramObject):
|
||||
Objects of this class are comparable in terms of equality. Two objects of this class are
|
||||
considered equal, if their :attr:`data` and :attr:`button_text` are equal.
|
||||
|
||||
.. seealso:: `Webappbot Example <examples.webappbot.html>`_
|
||||
|
||||
.. versionadded:: 20.0
|
||||
|
||||
Args:
|
||||
|
||||
@@ -30,6 +30,8 @@ class WebAppInfo(TelegramObject):
|
||||
Objects of this class are comparable in terms of equality. Two objects of this class are
|
||||
considered equal, if their :attr:`url` are equal.
|
||||
|
||||
.. seealso:: `Webappbot Example <examples.webappbot.html>`_
|
||||
|
||||
.. versionadded:: 20.0
|
||||
|
||||
Args:
|
||||
|
||||
+44
-4
@@ -25,7 +25,8 @@ enums. If they are related to a specific class, then they are also available as
|
||||
those classes.
|
||||
|
||||
.. versionchanged:: 20.0
|
||||
Since v20.0, most of the constants in this module are grouped into enums.
|
||||
|
||||
* Most of the constants in this module are grouped into enums.
|
||||
"""
|
||||
|
||||
__all__ = [
|
||||
@@ -38,6 +39,7 @@ __all__ = [
|
||||
"ChatInviteLinkLimit",
|
||||
"ChatMemberStatus",
|
||||
"ChatType",
|
||||
"CustomEmojiStickerLimit",
|
||||
"DiceEmoji",
|
||||
"FileSizeLimit",
|
||||
"FloodLimit",
|
||||
@@ -57,14 +59,14 @@ __all__ = [
|
||||
"PollLimit",
|
||||
"PollType",
|
||||
"SUPPORTED_WEBHOOK_PORTS",
|
||||
"StickerType",
|
||||
"WebhookLimit",
|
||||
"UpdateType",
|
||||
]
|
||||
|
||||
from enum import IntEnum
|
||||
from typing import List, NamedTuple
|
||||
|
||||
from telegram._utils.enum import StringEnum
|
||||
from telegram._utils.enum import IntEnum, StringEnum
|
||||
|
||||
|
||||
class _BotAPIVersion(NamedTuple):
|
||||
@@ -92,7 +94,7 @@ class _BotAPIVersion(NamedTuple):
|
||||
#: :data:`telegram.__bot_api_version_info__`.
|
||||
#:
|
||||
#: .. versionadded:: 20.0
|
||||
BOT_API_VERSION_INFO = _BotAPIVersion(major=6, minor=1)
|
||||
BOT_API_VERSION_INFO = _BotAPIVersion(major=6, minor=2)
|
||||
#: :obj:`str`: Telegram Bot API
|
||||
#: version supported by this version of `python-telegram-bot`. Also available as
|
||||
#: :data:`telegram.__bot_api_version__`.
|
||||
@@ -277,6 +279,22 @@ class ChatType(StringEnum):
|
||||
""":obj:`str`: A :class:`telegram.Chat` that is a channel."""
|
||||
|
||||
|
||||
class CustomEmojiStickerLimit(IntEnum):
|
||||
"""This enum contains limitations for :meth:`telegram.Bot.get_custom_emoji_stickers`.
|
||||
The enum members of this enumeration are instances of :class:`int` and can be treated as such.
|
||||
|
||||
.. versionadded:: 20.0
|
||||
"""
|
||||
|
||||
__slots__ = ()
|
||||
|
||||
CUSTOM_EMOJI_IDENTIFIER_LIMIT = 200
|
||||
""":obj:`int`: Maximum amount of custom emoji identifiers which can be specified for the
|
||||
:paramref:`~telegram.Bot.get_custom_emoji_stickers.custom_emoji_ids` parameter of
|
||||
:meth:`telegram.Bot.get_custom_emoji_stickers`.
|
||||
"""
|
||||
|
||||
|
||||
class DiceEmoji(StringEnum):
|
||||
"""This enum contains the available emoji for :class:`telegram.Dice`/
|
||||
:meth:`telegram.Bot.send_dice`. The enum
|
||||
@@ -605,6 +623,11 @@ class MessageEntityType(StringEnum):
|
||||
""":obj:`str`: Message entities representing strikethrough text."""
|
||||
SPOILER = "spoiler"
|
||||
""":obj:`str`: Message entities representing spoiler text."""
|
||||
CUSTOM_EMOJI = "custom_emoji"
|
||||
""":obj:`str`: Message entities representing inline custom emoji stickers.
|
||||
|
||||
.. versionadded:: 20.0
|
||||
"""
|
||||
|
||||
|
||||
class MessageLimit(IntEnum):
|
||||
@@ -718,6 +741,23 @@ class MessageType(StringEnum):
|
||||
""":obj:`str`: Messages with :attr:`telegram.Message.video_chat_participants_invited`."""
|
||||
|
||||
|
||||
class StickerType(StringEnum):
|
||||
"""This enum contains the available types of :class:`telegram.Sticker`. The enum
|
||||
members of this enumeration are instances of :class:`str` and can be treated as such.
|
||||
|
||||
.. versionadded:: 20.0
|
||||
"""
|
||||
|
||||
__slots__ = ()
|
||||
|
||||
REGULAR = "regular"
|
||||
""":obj:`str`: Regular sticker."""
|
||||
MASK = "mask"
|
||||
""":obj:`str`: Mask sticker."""
|
||||
CUSTOM_EMOJI = "custom_emoji"
|
||||
""":obj:`str`: Custom emoji sticker."""
|
||||
|
||||
|
||||
class ParseMode(StringEnum):
|
||||
"""This enum contains the available parse modes. The enum
|
||||
members of this enumeration are instances of :class:`str` and can be treated as such.
|
||||
|
||||
+3
-7
@@ -35,7 +35,7 @@ __all__ = (
|
||||
"TimedOut",
|
||||
)
|
||||
|
||||
from typing import Optional, Tuple, Union
|
||||
from typing import Tuple, Union
|
||||
|
||||
|
||||
def _lstrip_str(in_s: str, lstr: str) -> str:
|
||||
@@ -100,14 +100,10 @@ class InvalidToken(TelegramError):
|
||||
.. versionadded:: 20.0
|
||||
"""
|
||||
|
||||
__slots__ = ("_message",)
|
||||
__slots__ = ()
|
||||
|
||||
def __init__(self, message: str = None) -> None:
|
||||
self._message = message
|
||||
super().__init__("Invalid token" if self._message is None else self._message)
|
||||
|
||||
def __reduce__(self) -> Tuple[type, Tuple[Optional[str]]]: # type: ignore[override]
|
||||
return self.__class__, (self._message,)
|
||||
super().__init__("Invalid token" if message is None else message)
|
||||
|
||||
|
||||
class NetworkError(TelegramError):
|
||||
|
||||
@@ -19,10 +19,13 @@
|
||||
"""Extensions over the Telegram Bot API to facilitate bot making"""
|
||||
|
||||
__all__ = (
|
||||
"AIORateLimiter",
|
||||
"Application",
|
||||
"ApplicationBuilder",
|
||||
"ApplicationHandlerStop",
|
||||
"BaseHandler",
|
||||
"BasePersistence",
|
||||
"BaseRateLimiter",
|
||||
"CallbackContext",
|
||||
"CallbackDataCache",
|
||||
"CallbackQueryHandler",
|
||||
@@ -36,7 +39,6 @@ __all__ = (
|
||||
"DictPersistence",
|
||||
"ExtBot",
|
||||
"filters",
|
||||
"BaseHandler",
|
||||
"InlineQueryHandler",
|
||||
"InvalidCallbackData",
|
||||
"Job",
|
||||
@@ -56,9 +58,11 @@ __all__ = (
|
||||
)
|
||||
|
||||
from . import filters
|
||||
from ._aioratelimiter import AIORateLimiter
|
||||
from ._application import Application, ApplicationHandlerStop
|
||||
from ._applicationbuilder import ApplicationBuilder
|
||||
from ._basepersistence import BasePersistence, PersistenceInput
|
||||
from ._baseratelimiter import BaseRateLimiter
|
||||
from ._callbackcontext import CallbackContext
|
||||
from ._callbackdatacache import CallbackDataCache, InvalidCallbackData
|
||||
from ._callbackqueryhandler import CallbackQueryHandler
|
||||
|
||||
@@ -0,0 +1,262 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# A library that provides a Python interface to the Telegram Bot API
|
||||
# Copyright (C) 2015-2022
|
||||
# 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 implementation of the BaseRateLimiter class based on the aiolimiter
|
||||
library.
|
||||
"""
|
||||
import asyncio
|
||||
import contextlib
|
||||
import logging
|
||||
import sys
|
||||
from typing import Any, AsyncIterator, Callable, Coroutine, Dict, Optional, Union
|
||||
|
||||
try:
|
||||
from aiolimiter import AsyncLimiter
|
||||
|
||||
AIO_LIMITER_AVAILABLE = True
|
||||
except ImportError:
|
||||
AIO_LIMITER_AVAILABLE = False
|
||||
|
||||
from telegram._utils.types import JSONDict
|
||||
from telegram.error import RetryAfter
|
||||
from telegram.ext._baseratelimiter import BaseRateLimiter
|
||||
|
||||
# Useful for something like:
|
||||
# async with group_limiter if group else null_context():
|
||||
# so we don't have to differentiate between "I'm using a context manager" and "I'm not"
|
||||
if sys.version_info >= (3, 10):
|
||||
null_context = contextlib.nullcontext # pylint: disable=invalid-name
|
||||
else:
|
||||
|
||||
@contextlib.asynccontextmanager
|
||||
async def null_context() -> AsyncIterator[None]:
|
||||
yield None
|
||||
|
||||
|
||||
class AIORateLimiter(BaseRateLimiter[int]):
|
||||
"""
|
||||
Implementation of :class:`~telegram.ext.BaseRateLimiter` using the library
|
||||
`aiolimiter <https://aiolimiter.readthedocs.io/>`_.
|
||||
|
||||
Important:
|
||||
If you want to use this class, you must install PTB with the optional requirement
|
||||
``rate-limiter``, i.e.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
pip install python-telegram-bot[rate-limiter]
|
||||
|
||||
The rate limiting is applied by combining two levels of throttling and :meth:`process_request`
|
||||
roughly boils down to::
|
||||
|
||||
async with group_limiter(group_id):
|
||||
async with overall_limiter:
|
||||
await callback(*args, **kwargs)
|
||||
|
||||
Here, ``group_id`` is determined by checking if there is a ``chat_id`` parameter in the
|
||||
:paramref:`~telegram.ext.BaseRateLimiter.process_request.data`.
|
||||
The ``overall_limiter`` is applied only if a ``chat_id`` argument is present at all.
|
||||
|
||||
Attention:
|
||||
* Some bot methods accept a ``chat_id`` parameter in form of a ``@username`` for
|
||||
supergroups and channels. As we can't know which ``@username`` corresponds to which
|
||||
integer ``chat_id``, these will be treated as different groups, which may lead to
|
||||
exceeding the rate limit.
|
||||
* As channels can't be differentiated from supergroups by the ``@username`` or integer
|
||||
``chat_id``, this also applies the group related rate limits to channels.
|
||||
* A :exc:`~telegram.error.RetryAfter` exception will halt *all* requests for
|
||||
:attr:`~telegram.error.RetryAfter.retry_after` + 0.1 seconds. This may be stricter than
|
||||
necessary in some cases, e.g. the bot may hit a rate limit in one group but might still
|
||||
be allowed to send messages in another group.
|
||||
|
||||
Note:
|
||||
This class is to be understood as minimal effort reference implementation.
|
||||
If you would like to handle rate limiting in a more sophisticated, fine-tuned way, we
|
||||
welcome you to implement your own subclass of :class:`~telegram.ext.BaseRateLimiter`.
|
||||
Feel free to check out the source code of this class for inspiration.
|
||||
|
||||
.. versionadded:: 20.0
|
||||
|
||||
Args:
|
||||
overall_max_rate (:obj:`float`): The maximum number of requests allowed for the entire bot
|
||||
per :paramref:`overall_time_period`. When set to 0, no rate limiting will be applied.
|
||||
Defaults to ``30``.
|
||||
overall_time_period (:obj:`float`): The time period (in seconds) during which the
|
||||
:paramref:`overall_max_rate` is enforced. When set to 0, no rate limiting will be
|
||||
applied. Defaults to 1.
|
||||
group_max_rate (:obj:`float`): The maximum number of requests allowed for requests related
|
||||
to groups and channels per :paramref:`group_time_period`. When set to 0, no rate
|
||||
limiting will be applied. Defaults to 20.
|
||||
group_time_period (:obj:`float`): The time period (in seconds) during which the
|
||||
:paramref:`group_time_period` is enforced. When set to 0, no rate limiting will be
|
||||
applied. Defaults to 60.
|
||||
max_retries (:obj:`int`): The maximum number of retries to be made in case of a
|
||||
:exc:`~telegram.error.RetryAfter` exception.
|
||||
If set to 0, no retries will be made. Defaults to ``0``.
|
||||
|
||||
"""
|
||||
|
||||
__slots__ = (
|
||||
"_base_limiter",
|
||||
"_group_limiters",
|
||||
"_group_max_rate",
|
||||
"_group_time_period",
|
||||
"_logger",
|
||||
"_max_retries",
|
||||
"_retry_after_event",
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
overall_max_rate: float = 30,
|
||||
overall_time_period: float = 1,
|
||||
group_max_rate: float = 20,
|
||||
group_time_period: float = 60,
|
||||
max_retries: int = 0,
|
||||
) -> None:
|
||||
if not AIO_LIMITER_AVAILABLE:
|
||||
raise RuntimeError(
|
||||
"To use `AIORateLimiter`, PTB must be installed via `pip install "
|
||||
"python-telegram-bot[rate-limiter]`."
|
||||
)
|
||||
if overall_max_rate and overall_time_period:
|
||||
self._base_limiter: Optional[AsyncLimiter] = AsyncLimiter(
|
||||
max_rate=overall_max_rate, time_period=overall_time_period
|
||||
)
|
||||
else:
|
||||
self._base_limiter = None
|
||||
|
||||
if group_max_rate and group_time_period:
|
||||
self._group_max_rate = group_max_rate
|
||||
self._group_time_period = group_time_period
|
||||
else:
|
||||
self._group_max_rate = 0
|
||||
self._group_time_period = 0
|
||||
|
||||
self._group_limiters: Dict[Union[str, int], AsyncLimiter] = {}
|
||||
self._max_retries = max_retries
|
||||
self._logger = logging.getLogger(__name__)
|
||||
self._retry_after_event = asyncio.Event()
|
||||
self._retry_after_event.set()
|
||||
|
||||
async def initialize(self) -> None:
|
||||
"""Does nothing."""
|
||||
|
||||
async def shutdown(self) -> None:
|
||||
"""Does nothing."""
|
||||
|
||||
def _get_group_limiter(self, group_id: Union[str, int, bool]) -> "AsyncLimiter":
|
||||
# Remove limiters that haven't been used for so long that all their capacity is unused
|
||||
# We only do that if we have a lot of limiters lying around to avoid looping on every call
|
||||
# This is a minimal effort approach - a full-fledged cache could use a TTL approach
|
||||
# or at least adapt the threshold dynamically depending on the number of active limiters
|
||||
if len(self._group_limiters) > 512:
|
||||
# We copy to avoid modifying the dict while we iterate over it
|
||||
for key, limiter in self._group_limiters.copy().items():
|
||||
if key == group_id:
|
||||
continue
|
||||
if limiter.has_capacity(limiter.max_rate):
|
||||
del self._group_limiters[key]
|
||||
|
||||
if group_id not in self._group_limiters:
|
||||
self._group_limiters[group_id] = AsyncLimiter(
|
||||
max_rate=self._group_max_rate,
|
||||
time_period=self._group_time_period,
|
||||
)
|
||||
return self._group_limiters[group_id]
|
||||
|
||||
async def _run_request(
|
||||
self,
|
||||
chat: bool,
|
||||
group: Union[str, int, bool],
|
||||
callback: Callable[..., Coroutine[Any, Any, Union[bool, JSONDict, None]]],
|
||||
args: Any,
|
||||
kwargs: Dict[str, Any],
|
||||
) -> Union[bool, JSONDict, None]:
|
||||
base_context = self._base_limiter if (chat and self._base_limiter) else null_context()
|
||||
group_context = (
|
||||
self._get_group_limiter(group) if group and self._group_max_rate else null_context()
|
||||
)
|
||||
|
||||
async with group_context: # skipcq: PTC-W0062
|
||||
async with base_context:
|
||||
# In case a retry_after was hit, we wait with processing the request
|
||||
await self._retry_after_event.wait()
|
||||
|
||||
return await callback(*args, **kwargs)
|
||||
|
||||
# mypy doesn't understand that the last run of the for loop raises an exception
|
||||
async def process_request( # type: ignore[return]
|
||||
self,
|
||||
callback: Callable[..., Coroutine[Any, Any, Union[bool, JSONDict, None]]],
|
||||
args: Any,
|
||||
kwargs: Dict[str, Any],
|
||||
endpoint: str, # skipcq: PYL-W0613
|
||||
data: Dict[str, Any],
|
||||
rate_limit_args: Optional[int],
|
||||
) -> Union[bool, JSONDict, None]:
|
||||
"""
|
||||
Processes a request by applying rate limiting.
|
||||
|
||||
See :meth:`telegram.ext.BaseRateLimiter.process_request` for detailed information on the
|
||||
arguments.
|
||||
|
||||
Args:
|
||||
rate_limit_args (:obj:`None` | :obj:`int`): If set, specifies the maximum number of
|
||||
retries to be made in case of a :exc:`~telegram.error.RetryAfter` exception.
|
||||
Defaults to :paramref:`AIORateLimiter.max_retries`.
|
||||
"""
|
||||
max_retries = rate_limit_args or self._max_retries
|
||||
|
||||
group: Union[int, str, bool] = False
|
||||
chat: bool = False
|
||||
chat_id = data.get("chat_id")
|
||||
if chat_id is not None:
|
||||
chat = True
|
||||
|
||||
# In case user passes integer chat id as string
|
||||
try:
|
||||
chat_id = int(chat_id)
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
if (isinstance(chat_id, int) and chat_id < 0) or isinstance(chat_id, str):
|
||||
# string chat_id only works for channels and supergroups
|
||||
# We can't really tell channels from groups though ...
|
||||
group = chat_id
|
||||
|
||||
for i in range(max_retries + 1):
|
||||
try:
|
||||
return await self._run_request(
|
||||
chat=chat, group=group, callback=callback, args=args, kwargs=kwargs
|
||||
)
|
||||
except RetryAfter as exc:
|
||||
if i == max_retries:
|
||||
self._logger.exception(
|
||||
"Rate limit hit after maximum of %d retries", max_retries, exc_info=exc
|
||||
)
|
||||
raise exc
|
||||
|
||||
sleep = exc.retry_after + 0.1
|
||||
self._logger.info("Rate limit hit. Retrying after %f seconds", sleep)
|
||||
# Make sure we don't allow other requests to be processed
|
||||
self._retry_after_event.clear()
|
||||
await asyncio.sleep(sleep)
|
||||
finally:
|
||||
# Allow other requests to be processed
|
||||
self._retry_after_event.set()
|
||||
@@ -182,6 +182,9 @@ class Application(Generic[BT, CCT, UD, CD, BD, JQ], AbstractAsyncContextManager)
|
||||
post_init (:term:`coroutine function`): Optional. A callback that will be executed by
|
||||
:meth:`Application.run_polling` and :meth:`Application.run_webhook` after initializing
|
||||
the application via :meth:`initialize`.
|
||||
post_shutdown (:term:`coroutine function`): Optional. A callback that will be executed by
|
||||
:meth:`Application.run_polling` and :meth:`Application.run_webhook` after shutting down
|
||||
the application via :meth:`shutdown`.
|
||||
|
||||
"""
|
||||
|
||||
@@ -213,6 +216,7 @@ class Application(Generic[BT, CCT, UD, CD, BD, JQ], AbstractAsyncContextManager)
|
||||
"job_queue",
|
||||
"persistence",
|
||||
"post_init",
|
||||
"post_shutdown",
|
||||
"update_queue",
|
||||
"updater",
|
||||
"user_data",
|
||||
@@ -231,6 +235,9 @@ class Application(Generic[BT, CCT, UD, CD, BD, JQ], AbstractAsyncContextManager)
|
||||
post_init: Optional[
|
||||
Callable[["Application[BT, CCT, UD, CD, BD, JQ]"], Coroutine[Any, Any, None]]
|
||||
],
|
||||
post_shutdown: Optional[
|
||||
Callable[["Application[BT, CCT, UD, CD, BD, JQ]"], Coroutine[Any, Any, None]]
|
||||
],
|
||||
):
|
||||
if not was_called_by(
|
||||
inspect.currentframe(), Path(__file__).parent.resolve() / "_applicationbuilder.py"
|
||||
@@ -248,11 +255,12 @@ class Application(Generic[BT, CCT, UD, CD, BD, JQ], AbstractAsyncContextManager)
|
||||
self.handlers: Dict[int, List[BaseHandler]] = {}
|
||||
self.error_handlers: Dict[Callable, Union[bool, DefaultValue]] = {}
|
||||
self.post_init = post_init
|
||||
self.post_shutdown = post_shutdown
|
||||
|
||||
if isinstance(concurrent_updates, int) and concurrent_updates < 0:
|
||||
raise ValueError("`concurrent_updates` must be a non-negative integer!")
|
||||
if concurrent_updates is True:
|
||||
concurrent_updates = 4096
|
||||
concurrent_updates = 256
|
||||
self._concurrent_updates_sem = asyncio.BoundedSemaphore(concurrent_updates or 1)
|
||||
self._concurrent_updates: int = concurrent_updates or 0
|
||||
|
||||
@@ -362,6 +370,9 @@ class Application(Generic[BT, CCT, UD, CD, BD, JQ], AbstractAsyncContextManager)
|
||||
* :attr:`persistence` by calling :meth:`update_persistence` and
|
||||
:meth:`BasePersistence.flush`
|
||||
|
||||
Does *not* call :attr:`post_shutdown` - that is only done by :meth:`run_polling` and
|
||||
:meth:`run_webhook`.
|
||||
|
||||
.. seealso::
|
||||
:meth:`initialize`
|
||||
|
||||
@@ -573,6 +584,9 @@ class Application(Generic[BT, CCT, UD, CD, BD, JQ], AbstractAsyncContextManager)
|
||||
If :attr:`post_init` is set, it will be called between :meth:`initialize` and
|
||||
:meth:`telegram.ext.Updater.start_polling`.
|
||||
|
||||
If :attr:`post_shutdown` is set, it will be called after both :meth:`shutdown`
|
||||
and :meth:`telegram.ext.Updater.shutdown`.
|
||||
|
||||
.. seealso::
|
||||
:meth:`initialize`, :meth:`start`, :meth:`stop`, :meth:`shutdown`
|
||||
:meth:`telegram.ext.Updater.start_polling`, :meth:`run_webhook`
|
||||
@@ -683,6 +697,9 @@ class Application(Generic[BT, CCT, UD, CD, BD, JQ], AbstractAsyncContextManager)
|
||||
If :attr:`post_init` is set, it will be called between :meth:`initialize` and
|
||||
:meth:`telegram.ext.Updater.start_webhook`.
|
||||
|
||||
If :attr:`post_shutdown` is set, it will be called after both :meth:`shutdown`
|
||||
and :meth:`telegram.ext.Updater.shutdown`.
|
||||
|
||||
.. seealso::
|
||||
:meth:`initialize`, :meth:`start`, :meth:`stop`, :meth:`shutdown`
|
||||
:meth:`telegram.ext.Updater.start_webhook`, :meth:`run_polling`
|
||||
@@ -691,7 +708,8 @@ class Application(Generic[BT, CCT, UD, CD, BD, JQ], AbstractAsyncContextManager)
|
||||
listen (:obj:`str`, optional): IP-Address to listen on. Defaults to
|
||||
`127.0.0.1 <https://en.wikipedia.org/wiki/Localhost>`_.
|
||||
port (:obj:`int`, optional): Port the bot should be listening on. Must be one of
|
||||
:attr:`telegram.constants.SUPPORTED_WEBHOOK_PORTS`. Defaults to ``80``.
|
||||
:attr:`telegram.constants.SUPPORTED_WEBHOOK_PORTS` unless the bot is running
|
||||
behind a proxy. Defaults to ``80``.
|
||||
url_path (:obj:`str`, optional): Path inside url. Defaults to `` '' ``
|
||||
cert (:class:`pathlib.Path` | :obj:`str`, optional): Path to the SSL certificate file.
|
||||
key (:class:`pathlib.Path` | :obj:`str`, optional): Path to the SSL key file.
|
||||
@@ -813,7 +831,8 @@ class Application(Generic[BT, CCT, UD, CD, BD, JQ], AbstractAsyncContextManager)
|
||||
if self.running:
|
||||
loop.run_until_complete(self.stop())
|
||||
loop.run_until_complete(self.shutdown())
|
||||
loop.run_until_complete(self.updater.shutdown()) # type: ignore[union-attr]
|
||||
if self.post_shutdown:
|
||||
loop.run_until_complete(self.post_shutdown(self))
|
||||
finally:
|
||||
if close_loop:
|
||||
loop.close()
|
||||
@@ -1399,6 +1418,8 @@ class Application(Generic[BT, CCT, UD, CD, BD, JQ], AbstractAsyncContextManager)
|
||||
Note:
|
||||
Attempts to add the same callback multiple times will be ignored.
|
||||
|
||||
.. seealso:: `Errorhandler Example <examples.errorhandlerbot.py>`_
|
||||
|
||||
Args:
|
||||
callback (:term:`coroutine function`): The callback function for this error handler.
|
||||
Will be called when an error is raised. Callback signature::
|
||||
|
||||
@@ -45,7 +45,8 @@ from telegram.request import BaseRequest
|
||||
from telegram.request._httpxrequest import HTTPXRequest
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from telegram.ext import BasePersistence, CallbackContext, Defaults
|
||||
from telegram.ext import BasePersistence, BaseRateLimiter, CallbackContext, Defaults
|
||||
from telegram.ext._utils.types import RLARGS
|
||||
|
||||
# Type hinting is a bit complicated here because we try to get to a sane level of
|
||||
# leveraging generics and therefore need a number of type variables.
|
||||
@@ -81,6 +82,7 @@ _BOT_CHECKS = [
|
||||
("defaults", "defaults"),
|
||||
("arbitrary_callback_data", "arbitrary_callback_data"),
|
||||
("private_key", "private_key"),
|
||||
("rate_limiter", "rate_limiter instance"),
|
||||
]
|
||||
|
||||
_TWO_ARGS_REQ = "The parameter `{}` may only be set, if no {} was set."
|
||||
@@ -135,9 +137,11 @@ class ApplicationBuilder(Generic[BT, CCT, UD, CD, BD, JQ]):
|
||||
"_persistence",
|
||||
"_pool_timeout",
|
||||
"_post_init",
|
||||
"_post_shutdown",
|
||||
"_private_key",
|
||||
"_private_key_password",
|
||||
"_proxy_url",
|
||||
"_rate_limiter",
|
||||
"_read_timeout",
|
||||
"_request",
|
||||
"_token",
|
||||
@@ -178,6 +182,8 @@ class ApplicationBuilder(Generic[BT, CCT, UD, CD, BD, JQ]):
|
||||
self._concurrent_updates: DVInput[Union[int, bool]] = DEFAULT_FALSE
|
||||
self._updater: ODVInput[Updater] = DEFAULT_NONE
|
||||
self._post_init: Optional[Callable[[Application], Coroutine[Any, Any, None]]] = None
|
||||
self._post_shutdown: Optional[Callable[[Application], Coroutine[Any, Any, None]]] = None
|
||||
self._rate_limiter: ODVInput["BaseRateLimiter"] = DEFAULT_NONE
|
||||
|
||||
def _build_request(self, get_updates: bool) -> BaseRequest:
|
||||
prefix = "_get_updates_" if get_updates else "_"
|
||||
@@ -191,7 +197,7 @@ class ApplicationBuilder(Generic[BT, CCT, UD, CD, BD, JQ]):
|
||||
)
|
||||
else:
|
||||
connection_pool_size = (
|
||||
DefaultValue.get_value(getattr(self, f"{prefix}connection_pool_size")) or 128
|
||||
DefaultValue.get_value(getattr(self, f"{prefix}connection_pool_size")) or 256
|
||||
)
|
||||
|
||||
timeouts = dict(
|
||||
@@ -225,6 +231,7 @@ class ApplicationBuilder(Generic[BT, CCT, UD, CD, BD, JQ]):
|
||||
arbitrary_callback_data=DefaultValue.get_value(self._arbitrary_callback_data),
|
||||
request=self._build_request(get_updates=False),
|
||||
get_updates_request=self._build_request(get_updates=True),
|
||||
rate_limiter=DefaultValue.get_value(self._rate_limiter),
|
||||
)
|
||||
|
||||
def build(
|
||||
@@ -271,6 +278,7 @@ class ApplicationBuilder(Generic[BT, CCT, UD, CD, BD, JQ]):
|
||||
persistence=persistence,
|
||||
context_types=DefaultValue.get_value(self._context_types),
|
||||
post_init=self._post_init,
|
||||
post_shutdown=self._post_shutdown,
|
||||
**self._application_kwargs, # For custom Application subclasses
|
||||
)
|
||||
|
||||
@@ -424,7 +432,9 @@ class ApplicationBuilder(Generic[BT, CCT, UD, CD, BD, JQ]):
|
||||
def connection_pool_size(self: BuilderType, connection_pool_size: int) -> BuilderType:
|
||||
"""Sets the size of the connection pool for the
|
||||
:paramref:`~telegram.request.HTTPXRequest.connection_pool_size` parameter of
|
||||
:attr:`telegram.Bot.request`. Defaults to ``128``.
|
||||
:attr:`telegram.Bot.request`. Defaults to ``256``.
|
||||
|
||||
.. include:: inclusions/pool_size_tip.rst
|
||||
|
||||
Args:
|
||||
connection_pool_size (:obj:`int`): The size of the connection pool.
|
||||
@@ -504,6 +514,8 @@ class ApplicationBuilder(Generic[BT, CCT, UD, CD, BD, JQ]):
|
||||
:paramref:`~telegram.request.HTTPXRequest.pool_timeout` parameter of
|
||||
:attr:`telegram.Bot.request`. Defaults to :obj:`None`.
|
||||
|
||||
.. include:: inclusions/pool_size_tip.rst
|
||||
|
||||
Args:
|
||||
pool_timeout (:obj:`float`): See
|
||||
:paramref:`telegram.request.HTTPXRequest.pool_timeout` for more information.
|
||||
@@ -770,11 +782,13 @@ class ApplicationBuilder(Generic[BT, CCT, UD, CD, BD, JQ]):
|
||||
that your bot does not (explicitly or implicitly) rely on updates being processed
|
||||
sequentially.
|
||||
|
||||
.. include:: inclusions/pool_size_tip.rst
|
||||
|
||||
.. seealso:: :attr:`telegram.ext.Application.concurrent_updates`
|
||||
|
||||
Args:
|
||||
concurrent_updates (:obj:`bool` | :obj:`int`): Passing :obj:`True` will allow for
|
||||
``4096`` updates to be processed concurrently. Pass an integer to specify a
|
||||
``256`` updates to be processed concurrently. Pass an integer to specify a
|
||||
different number of updates that may be processed concurrently.
|
||||
|
||||
Returns:
|
||||
@@ -928,10 +942,67 @@ class ApplicationBuilder(Generic[BT, CCT, UD, CD, BD, JQ]):
|
||||
self._post_init = post_init
|
||||
return self
|
||||
|
||||
def post_shutdown(
|
||||
self: BuilderType, post_shutdown: Callable[[Application], Coroutine[Any, Any, None]]
|
||||
) -> BuilderType:
|
||||
"""
|
||||
Sets a callback to be executed by :meth:`Application.run_polling` and
|
||||
:meth:`Application.run_webhook` *after* executing :meth:`Updater.shutdown`
|
||||
and :meth:`Application.shutdown`.
|
||||
|
||||
Tip:
|
||||
This can be used for custom shutdown logic that requires to await coroutines, e.g.
|
||||
closing a database connection
|
||||
|
||||
Example:
|
||||
.. code::
|
||||
|
||||
async def post_shutdown(application: Application) -> None:
|
||||
await application.bot_data['database'].close()
|
||||
|
||||
application = Application.builder()
|
||||
.token("TOKEN")
|
||||
.post_shutdown(post_shutdown)
|
||||
.build()
|
||||
|
||||
Args:
|
||||
post_shutdown (:term:`coroutine function`): The custom callback. Must be a
|
||||
:term:`coroutine function` and must accept exactly one positional argument, which
|
||||
is the :class:`~telegram.ext.Application`::
|
||||
|
||||
async def post_shutdown(application: Application) -> None:
|
||||
|
||||
Returns:
|
||||
:class:`ApplicationBuilder`: The same builder with the updated argument.
|
||||
"""
|
||||
self._post_shutdown = post_shutdown
|
||||
return self
|
||||
|
||||
def rate_limiter(
|
||||
self: "ApplicationBuilder[BT, CCT, UD, CD, BD, JQ]",
|
||||
rate_limiter: "BaseRateLimiter[RLARGS]",
|
||||
) -> "ApplicationBuilder[ExtBot[RLARGS], CCT, UD, CD, BD, JQ]":
|
||||
"""Sets a :class:`telegram.ext.BaseRateLimiter` instance for the
|
||||
:paramref:`telegram.ext.ExtBot.rate_limiter` parameter of
|
||||
:attr:`telegram.ext.Application.bot`.
|
||||
|
||||
Args:
|
||||
rate_limiter (:class:`telegram.ext.BaseRateLimiter`): The rate limiter.
|
||||
|
||||
Returns:
|
||||
:class:`ApplicationBuilder`: The same builder with the updated argument.
|
||||
"""
|
||||
if self._bot is not DEFAULT_NONE:
|
||||
raise RuntimeError(_TWO_ARGS_REQ.format("rate_limiter", "bot instance"))
|
||||
if self._updater not in (DEFAULT_NONE, None):
|
||||
raise RuntimeError(_TWO_ARGS_REQ.format("rate_limiter", "updater"))
|
||||
self._rate_limiter = rate_limiter
|
||||
return self # type: ignore[return-value]
|
||||
|
||||
|
||||
InitApplicationBuilder = ( # This is defined all the way down here so that its type is inferred
|
||||
ApplicationBuilder[ # by Pylance correctly.
|
||||
ExtBot,
|
||||
ExtBot[None],
|
||||
ContextTypes.DEFAULT_TYPE,
|
||||
Dict,
|
||||
Dict,
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# A library that provides a Python interface to the Telegram Bot API
|
||||
# Copyright (C) 2015-2022
|
||||
# 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 a class that allows to rate limit requests to the Bot API."""
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Any, Callable, Coroutine, Dict, Generic, Optional, Union
|
||||
|
||||
from telegram._utils.types import JSONDict
|
||||
from telegram.ext._utils.types import RLARGS
|
||||
|
||||
|
||||
class BaseRateLimiter(ABC, Generic[RLARGS]):
|
||||
"""
|
||||
Abstract interface class that allows to rate limit the requests that python-telegram-bot
|
||||
sends to the Telegram Bot API. An implementation of this class
|
||||
must implement all abstract methods and properties.
|
||||
|
||||
This class is a :class:`~typing.Generic` class and accepts one type variable that specifies
|
||||
the type of the argument :paramref:`~process_request.rate_limit_args` of
|
||||
:meth:`process_request` and the methods of :class:`~telegram.ext.ExtBot`.
|
||||
|
||||
Hint:
|
||||
Requests to :meth:`~telegram.Bot.get_updates` are never rate limited.
|
||||
|
||||
.. versionadded:: 20.0
|
||||
"""
|
||||
|
||||
__slots__ = ()
|
||||
|
||||
@abstractmethod
|
||||
async def initialize(self) -> None:
|
||||
"""Initialize resources used by this class. Must be implemented by a subclass."""
|
||||
|
||||
@abstractmethod
|
||||
async def shutdown(self) -> None:
|
||||
"""Stop & clear resources used by this class. Must be implemented by a subclass."""
|
||||
|
||||
@abstractmethod
|
||||
async def process_request(
|
||||
self,
|
||||
callback: Callable[..., Coroutine[Any, Any, Union[bool, JSONDict, None]]],
|
||||
args: Any,
|
||||
kwargs: Dict[str, Any],
|
||||
endpoint: str,
|
||||
data: Dict[str, Any],
|
||||
rate_limit_args: Optional[RLARGS],
|
||||
) -> Union[bool, JSONDict, None]:
|
||||
"""
|
||||
Process a request. Must be implemented by a subclass.
|
||||
|
||||
This method must call :paramref:`callback` and return the result of the call.
|
||||
`When` the callback is called is up to the implementation.
|
||||
|
||||
Important:
|
||||
This method must only return once the result of :paramref:`callback` is known!
|
||||
|
||||
If a :exc:`~telegram.error.RetryAfter` error is raised, this method may try to make
|
||||
a new request by calling the callback again.
|
||||
|
||||
Warning:
|
||||
This method *should not* handle any other exception raised by :paramref:`callback`!
|
||||
|
||||
There are basically two different approaches how a rate limiter can be implemented:
|
||||
|
||||
1. React only if necessary. In this case, the :paramref:`callback` is called without any
|
||||
precautions. If a :exc:`~telegram.error.RetryAfter` error is raised, processing requests
|
||||
is halted for the :attr:`~telegram.error.RetryAfter.retry_after` and finally the
|
||||
:paramref:`callback` is called again. This approach is often amendable for bots that
|
||||
don't have a large user base and/or don't send more messages than they get updates.
|
||||
2. Throttle all outgoing requests. In this case the implementation makes sure that the
|
||||
requests are spread out over a longer time interval in order to stay below the rate
|
||||
limits. This approach is often amendable for bots that have a large user base and/or
|
||||
send more messages than they get updates.
|
||||
|
||||
An implementation can use the information provided by :paramref:`data`,
|
||||
:paramref:`endpoint` and :paramref:`rate_limit_args` to handle each request differently.
|
||||
|
||||
Examples:
|
||||
* It is usually desirable to call :meth:`telegram.Bot.answer_inline_query`
|
||||
as quickly as possible, while delaying :meth:`telegram.Bot.send_message`
|
||||
is acceptable.
|
||||
* There are `different <https://core.telegram.org/bots/faq\
|
||||
#my-bot-is-hitting-limits-how-do-i-avoid-this>`_ rate limits for group chats and
|
||||
private chats.
|
||||
* When sending broadcast messages to a large number of users, these requests can
|
||||
typically be delayed for a longer time than messages that are direct replies to a
|
||||
user input.
|
||||
|
||||
Args:
|
||||
callback (Callable[..., :term:`coroutine`]): The coroutine function that must be called
|
||||
to make the request.
|
||||
args (Tuple[:obj:`object`]): The positional arguments for the :paramref:`callback`
|
||||
function.
|
||||
kwargs (Dict[:obj:`str`, :obj:`object`]): The keyword arguments for the
|
||||
:paramref:`callback` function.
|
||||
endpoint (:obj:`str`): The endpoint that the request is made for, e.g.
|
||||
``"sendMessage"``.
|
||||
data (Dict[:obj:`str`, :obj:`object`]): The parameters that were passed to the method
|
||||
of :class:`~telegram.ext.ExtBot`. Any ``api_kwargs`` are included in this and
|
||||
any :paramref:`~telegram.ext.ExtBot.defaults` are already applied.
|
||||
|
||||
Example:
|
||||
|
||||
When calling::
|
||||
|
||||
await ext_bot.send_message(
|
||||
chat_id=1,
|
||||
text="Hello world!",
|
||||
api_kwargs={"custom": "arg"}
|
||||
)
|
||||
|
||||
then :paramref:`data` will be::
|
||||
|
||||
{"chat_id": 1, "text": "Hello world!", "custom": "arg"}
|
||||
|
||||
rate_limit_args (:obj:`None` | :class:`object`): Custom arguments passed to the methods
|
||||
of :class:`~telegram.ext.ExtBot`. Can e.g. be used to specify the priority of
|
||||
the request.
|
||||
|
||||
Returns:
|
||||
:obj:`bool` | Dict[:obj:`str`, :obj:`object`] | :obj:`None`: The result of the
|
||||
callback function.
|
||||
"""
|
||||
@@ -160,6 +160,9 @@ class CallbackContext(Generic[BT, UD, CD, BD]):
|
||||
``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>`_.
|
||||
|
||||
.. versionchanged:: 20.0
|
||||
The chat data is now also present in error handlers if the error is caused by a job.
|
||||
"""
|
||||
if self._chat_id is not None:
|
||||
return self._application.chat_data[self._chat_id]
|
||||
@@ -176,6 +179,9 @@ class CallbackContext(Generic[BT, UD, CD, BD]):
|
||||
""":obj:`ContextTypes.user_data`: Optional. An object that can be used to keep any data in.
|
||||
For each update from the same user it will be the same :obj:`ContextTypes.user_data`.
|
||||
Defaults to :obj:`dict`.
|
||||
|
||||
.. versionchanged:: 20.0
|
||||
The user data is now also present in error handlers if the error is caused by a job.
|
||||
"""
|
||||
if self._user_id is not None:
|
||||
return self._application.user_data[self._user_id]
|
||||
@@ -275,10 +281,16 @@ class CallbackContext(Generic[BT, UD, CD, BD]):
|
||||
Returns:
|
||||
:class:`telegram.ext.CallbackContext`
|
||||
"""
|
||||
self = cls.from_update(update, application)
|
||||
# update and job will never be present at the same time
|
||||
if update is not None:
|
||||
self = cls.from_update(update, application)
|
||||
elif job is not None:
|
||||
self = cls.from_job(job, application)
|
||||
else:
|
||||
self = cls(application) # type: ignore
|
||||
|
||||
self.error = error
|
||||
self.coroutine = coroutine
|
||||
self.job = job
|
||||
return self
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -94,6 +94,11 @@ class CallbackDataCache:
|
||||
sent via inline mode.
|
||||
If necessary, will drop the least recently used items.
|
||||
|
||||
.. seealso:: :attr:`telegram.ext.ExtBot.callback_data_cache`,
|
||||
`Arbitrary callback_data <https://github.com/python-telegram-bot/
|
||||
python-telegram-bot/wiki/Arbitrary-callback_data>`_,
|
||||
Arbitrary Callback Data Example <examples.arbitrarycallbackdatabot.html>
|
||||
|
||||
.. versionadded:: 13.6
|
||||
|
||||
Args:
|
||||
|
||||
@@ -31,12 +31,14 @@ RT = TypeVar("RT")
|
||||
class ChatMemberHandler(BaseHandler[Update, CCT]):
|
||||
"""BaseHandler class to handle Telegram updates that contain a chat member update.
|
||||
|
||||
.. versionadded:: 13.4
|
||||
|
||||
Warning:
|
||||
When setting :paramref:`block` to :obj:`False`, you cannot rely on adding custom
|
||||
attributes to :class:`telegram.ext.CallbackContext`. See its docs for more info.
|
||||
|
||||
.. seealso:: `Chat Member Example <examples.chatmemberbot.html>`_
|
||||
|
||||
.. versionadded:: 13.4
|
||||
|
||||
Args:
|
||||
callback (:term:`coroutine function`): The callback function for this handler. Will be
|
||||
called when :meth:`check_update` has determined that an update should be processed by
|
||||
|
||||
@@ -33,6 +33,8 @@ class ContextTypes(Generic[CCT, UD, CD, BD]):
|
||||
Convenience class to gather customizable types of the :class:`telegram.ext.CallbackContext`
|
||||
interface.
|
||||
|
||||
.. seealso:: `ContextTypes Example <examples.contexttypesbot.html>`_
|
||||
|
||||
.. versionadded:: 13.6
|
||||
|
||||
Args:
|
||||
|
||||
@@ -184,7 +184,12 @@ class ConversationHandler(BaseHandler[Update, CCT]):
|
||||
states to continue the parent conversation after the child conversation has ended or even
|
||||
map a state to :attr:`END` to end the *parent* conversation from within the child
|
||||
conversation. For an example on nested :class:`ConversationHandler` s, see
|
||||
:any:`examples.conversationbot`.
|
||||
:any:`examples.nestedconversationbot`.
|
||||
|
||||
.. seealso:: `Conversation Example <examples.conversationbot.html>`_,
|
||||
`Conversation Example 2 <examples.conversationbot2.html>`_,
|
||||
`Nested Conversation Example <examples.nestedconversationbot.html>`_,
|
||||
`Persistent Conversation Example <examples.persistentconversationbot.html>`_
|
||||
|
||||
Args:
|
||||
entry_points (List[:class:`telegram.ext.BaseHandler`]): A list of :obj:`BaseHandler`
|
||||
@@ -693,6 +698,8 @@ class ConversationHandler(BaseHandler[Update, CCT]):
|
||||
return None
|
||||
if self.per_chat and not update.effective_chat:
|
||||
return None
|
||||
if self.per_user and not update.effective_user:
|
||||
return None
|
||||
if self.per_message and not update.callback_query:
|
||||
return None
|
||||
if update.callback_query and self.per_chat and not update.callback_query.message:
|
||||
|
||||
+2706
-14
File diff suppressed because it is too large
Load Diff
@@ -46,6 +46,8 @@ class InlineQueryHandler(BaseHandler[Update, CCT]):
|
||||
chats and may not be set for inline queries coming from third-party clients. These
|
||||
updates won't be handled, if :attr:`chat_types` is passed.
|
||||
|
||||
.. seealso:: `Inlinebot Example <examples.inlinebot.html>`_
|
||||
|
||||
Args:
|
||||
callback (:term:`coroutine function`): The callback function for this handler. Will be
|
||||
called when :meth:`check_update` has determined that an update should be processed by
|
||||
|
||||
@@ -40,6 +40,12 @@ class JobQueue:
|
||||
"""This class allows you to periodically perform tasks with the bot. It is a convenience
|
||||
wrapper for the APScheduler library.
|
||||
|
||||
.. seealso:: :attr:`telegram.ext.Application.job_queue`,
|
||||
:attr:`telegram.ext.CallbackContext.job_queue`,
|
||||
`Timerbot Example <examples.timerbot.html>`_,
|
||||
`Job Queue <https://github.com/python-telegram-bot/
|
||||
python-telegram-bot/wiki/Extensions-%E2%80%93-JobQueue>`_
|
||||
|
||||
Attributes:
|
||||
scheduler (:class:`apscheduler.schedulers.asyncio.AsyncIOScheduler`): The scheduler.
|
||||
|
||||
@@ -47,7 +53,6 @@ class JobQueue:
|
||||
Uses :class:`~apscheduler.schedulers.asyncio.AsyncIOScheduler` instead of
|
||||
:class:`~apscheduler.schedulers.background.BackgroundScheduler`
|
||||
|
||||
|
||||
"""
|
||||
|
||||
__slots__ = ("_application", "scheduler", "_executor")
|
||||
@@ -567,7 +572,11 @@ class JobQueue:
|
||||
await asyncio.sleep(0.01)
|
||||
|
||||
def jobs(self) -> Tuple["Job", ...]:
|
||||
"""Returns a tuple of all *scheduled* jobs that are currently in the :class:`JobQueue`."""
|
||||
"""Returns a tuple of all *scheduled* jobs that are currently in the :class:`JobQueue`.
|
||||
|
||||
Returns:
|
||||
Tuple[:class:`Job`]: Tuple of all *scheduled* jobs.
|
||||
"""
|
||||
return tuple(
|
||||
Job._from_aps_job(job) # pylint: disable=protected-access
|
||||
for job in self.scheduler.get_jobs()
|
||||
@@ -576,6 +585,9 @@ class JobQueue:
|
||||
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 :class:`JobQueue`.
|
||||
|
||||
Returns:
|
||||
Tuple[:class:`Job`]: Tuple of all *pending* or *scheduled* jobs matching the name.
|
||||
"""
|
||||
return tuple(job for job in self.jobs() if job.name == name)
|
||||
|
||||
|
||||
@@ -32,6 +32,8 @@ class PollAnswerHandler(BaseHandler[Update, CCT]):
|
||||
When setting :paramref:`block` to :obj:`False`, you cannot rely on adding custom
|
||||
attributes to :class:`telegram.ext.CallbackContext`. See its docs for more info.
|
||||
|
||||
.. seealso:: `Pollbot EXample <examples.pollbot.html>`_
|
||||
|
||||
Args:
|
||||
callback (:term:`coroutine function`): The callback function for this handler. Will be
|
||||
called when :meth:`check_update` has determined that an update should be processed by
|
||||
|
||||
@@ -32,6 +32,8 @@ class PollHandler(BaseHandler[Update, CCT]):
|
||||
When setting :paramref:`block` to :obj:`False`, you cannot rely on adding custom
|
||||
attributes to :class:`telegram.ext.CallbackContext`. See its docs for more info.
|
||||
|
||||
.. seealso:: `Pollbot Example <examples.pollbot.html>`_
|
||||
|
||||
Args:
|
||||
callback (:term:`coroutine function`): The callback function for this handler. Will be
|
||||
called when :meth:`check_update` has determined that an update should be processed by
|
||||
|
||||
@@ -31,6 +31,8 @@ class PreCheckoutQueryHandler(BaseHandler[Update, CCT]):
|
||||
When setting :paramref:`block` to :obj:`False`, you cannot rely on adding custom
|
||||
attributes to :class:`telegram.ext.CallbackContext`. See its docs for more info.
|
||||
|
||||
.. seealso:: `Paymentbot Example <examples.paymentbot.html>`_
|
||||
|
||||
Args:
|
||||
callback (:term:`coroutine function`): The callback function for this handler. Will be
|
||||
called when :meth:`check_update` has determined that an update should be processed by
|
||||
|
||||
@@ -31,6 +31,8 @@ class ShippingQueryHandler(BaseHandler[Update, CCT]):
|
||||
When setting :paramref:`block` to :obj:`False`, you cannot rely on adding custom
|
||||
attributes to :class:`telegram.ext.CallbackContext`. See its docs for more info.
|
||||
|
||||
.. seealso:: `Paymentbot Example <examples.paymentbot.html>`_
|
||||
|
||||
Args:
|
||||
callback (:term:`coroutine function`): The callback function for this handler. Will be
|
||||
called when :meth:`check_update` has determined that an update should be processed by
|
||||
|
||||
@@ -389,7 +389,8 @@ class Updater(AbstractAsyncContextManager):
|
||||
listen (:obj:`str`, optional): IP-Address to listen on. Defaults to
|
||||
`127.0.0.1 <https://en.wikipedia.org/wiki/Localhost>`_.
|
||||
port (:obj:`int`, optional): Port the bot should be listening on. Must be one of
|
||||
:attr:`telegram.constants.SUPPORTED_WEBHOOK_PORTS`. Defaults to ``80``.
|
||||
:attr:`telegram.constants.SUPPORTED_WEBHOOK_PORTS` unless the bot is running
|
||||
behind a proxy. Defaults to ``80``.
|
||||
url_path (:obj:`str`, optional): Path inside url (http(s)://listen:port/<url_path>).
|
||||
Defaults to ``''``.
|
||||
cert (:class:`pathlib.Path` | :obj:`str`, optional): Path to the SSL certificate file.
|
||||
|
||||
@@ -39,8 +39,10 @@ from typing import (
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Optional
|
||||
|
||||
from telegram import Bot
|
||||
from telegram.ext import CallbackContext, JobQueue
|
||||
from telegram.ext import BaseRateLimiter, CallbackContext, JobQueue
|
||||
|
||||
CCT = TypeVar("CCT", bound="CallbackContext")
|
||||
"""An instance of :class:`telegram.ext.CallbackContext` or a custom subclass.
|
||||
@@ -101,3 +103,13 @@ JQ = TypeVar("JQ", bound=Union[None, "JobQueue"])
|
||||
"""Type of the job queue.
|
||||
|
||||
.. versionadded:: 20.0"""
|
||||
|
||||
RL = TypeVar("RL", bound="Optional[BaseRateLimiter]")
|
||||
"""Type of the rate limiter.
|
||||
|
||||
.. versionadded:: 20.0"""
|
||||
|
||||
RLARGS = TypeVar("RLARGS")
|
||||
"""Type of the rate limiter arguments.
|
||||
|
||||
.. versionadded:: 20.0"""
|
||||
|
||||
@@ -1965,6 +1965,7 @@ class Sticker:
|
||||
|
||||
.. versionadded:: 20.0
|
||||
"""
|
||||
# neither mask nor emoji can be a message.sticker, so no filters for them
|
||||
|
||||
|
||||
class _SuccessfulPayment(MessageFilter):
|
||||
|
||||
+6
-1
@@ -93,7 +93,10 @@ def mention_markdown(user_id: Union[int, str], name: str, version: int = 1) -> s
|
||||
Returns:
|
||||
:obj:`str`: The inline mention for the user as Markdown.
|
||||
"""
|
||||
return f"[{escape_markdown(name, version=version)}](tg://user?id={user_id})"
|
||||
tg_link = f"tg://user?id={user_id}"
|
||||
if version == 1:
|
||||
return f"[{name}]({tg_link})"
|
||||
return f"[{escape_markdown(name, version=version)}]({tg_link})"
|
||||
|
||||
|
||||
def effective_message_type(entity: Union["Message", "Update"]) -> Optional[str]:
|
||||
@@ -143,6 +146,8 @@ def create_deep_linked_url(bot_username: str, payload: str = None, group: bool =
|
||||
Examples:
|
||||
``create_deep_linked_url(bot.get_me().username, "some-params")``
|
||||
|
||||
.. seealso:: `Deeplinking Example <examples.deeplinking.html>`_
|
||||
|
||||
Args:
|
||||
bot_username (:obj:`str`): The username to link to
|
||||
payload (:obj:`str`, optional): Parameters to encode in the created URL
|
||||
|
||||
@@ -312,20 +312,20 @@ class BaseRequest(
|
||||
|
||||
message += f"\nThe server response contained unknown parameters: {parameters}"
|
||||
|
||||
if code == HTTPStatus.FORBIDDEN:
|
||||
if code == HTTPStatus.FORBIDDEN: # 403
|
||||
raise Forbidden(message)
|
||||
if code in (HTTPStatus.NOT_FOUND, HTTPStatus.UNAUTHORIZED):
|
||||
if code in (HTTPStatus.NOT_FOUND, HTTPStatus.UNAUTHORIZED): # 404 and 401
|
||||
# TG returns 404 Not found for
|
||||
# 1) malformed tokens
|
||||
# 2) correct tokens but non-existing method, e.g. api.tg.org/botTOKEN/unkonwnMethod
|
||||
# We can basically rule out 2) since we don't let users make requests manually
|
||||
# TG returns 401 Unauthorized for correctly formatted tokens that are not valid
|
||||
raise InvalidToken(message)
|
||||
if code == HTTPStatus.BAD_REQUEST:
|
||||
if code == HTTPStatus.BAD_REQUEST: # 400
|
||||
raise BadRequest(message)
|
||||
if code == HTTPStatus.CONFLICT:
|
||||
if code == HTTPStatus.CONFLICT: # 409
|
||||
raise Conflict(message)
|
||||
if code == HTTPStatus.BAD_GATEWAY:
|
||||
if code == HTTPStatus.BAD_GATEWAY: # 502
|
||||
raise NetworkError(description or "Bad Gateway")
|
||||
raise NetworkError(f"{message} ({code})")
|
||||
|
||||
|
||||
+1
-1
@@ -58,7 +58,7 @@ def get(name, fallback):
|
||||
if GITHUB_ACTION is not None and BOTS is not None and JOB_INDEX is not None:
|
||||
try:
|
||||
return BOTS[JOB_INDEX][name]
|
||||
except KeyError:
|
||||
except (KeyError, IndexError):
|
||||
pass
|
||||
|
||||
# Otherwise go with the fallback
|
||||
|
||||
@@ -558,6 +558,7 @@ async def check_shortcut_call(
|
||||
bot: The bot
|
||||
bot_method_name: The bot methods name, e.g. `'send_message'`
|
||||
skip_params: Parameters that are allowed to be missing, e.g. `['inline_message_id']`
|
||||
`rate_limit_args` will be skipped by default
|
||||
shortcut_kwargs: The kwargs passed by the shortcut directly, e.g. ``chat_id``
|
||||
|
||||
Returns:
|
||||
@@ -565,8 +566,13 @@ async def check_shortcut_call(
|
||||
"""
|
||||
if not skip_params:
|
||||
skip_params = set()
|
||||
else:
|
||||
skip_params = set(skip_params)
|
||||
skip_params.add("rate_limit_args")
|
||||
if not shortcut_kwargs:
|
||||
shortcut_kwargs = set()
|
||||
else:
|
||||
shortcut_kwargs = set(shortcut_kwargs)
|
||||
|
||||
orig_bot_method = getattr(bot, bot_method_name)
|
||||
bot_signature = inspect.signature(orig_bot_method)
|
||||
|
||||
+123
-2
@@ -130,6 +130,7 @@ class TestApplication:
|
||||
updater=updater,
|
||||
concurrent_updates=False,
|
||||
post_init=None,
|
||||
post_shutdown=None,
|
||||
)
|
||||
assert len(recwarn) == 1
|
||||
assert (
|
||||
@@ -139,7 +140,7 @@ class TestApplication:
|
||||
assert recwarn[0].filename == __file__, "stacklevel is incorrect!"
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"concurrent_updates, expected", [(0, 0), (4, 4), (False, 0), (True, 4096)]
|
||||
"concurrent_updates, expected", [(0, 0), (4, 4), (False, 0), (True, 256)]
|
||||
)
|
||||
@pytest.mark.filterwarnings("ignore: `Application` instances should")
|
||||
def test_init(self, bot, concurrent_updates, expected):
|
||||
@@ -152,6 +153,9 @@ class TestApplication:
|
||||
async def post_init(application: Application) -> None:
|
||||
pass
|
||||
|
||||
async def post_shutdown(application: Application) -> None:
|
||||
pass
|
||||
|
||||
app = Application(
|
||||
bot=bot,
|
||||
update_queue=update_queue,
|
||||
@@ -161,6 +165,7 @@ class TestApplication:
|
||||
updater=updater,
|
||||
concurrent_updates=concurrent_updates,
|
||||
post_init=post_init,
|
||||
post_shutdown=post_shutdown,
|
||||
)
|
||||
assert app.bot is bot
|
||||
assert app.update_queue is update_queue
|
||||
@@ -172,6 +177,7 @@ class TestApplication:
|
||||
assert app.bot is updater.bot
|
||||
assert app.concurrent_updates == expected
|
||||
assert app.post_init is post_init
|
||||
assert app.post_shutdown is post_shutdown
|
||||
|
||||
# These should be done by the builder
|
||||
assert app.persistence.bot is None
|
||||
@@ -192,6 +198,7 @@ class TestApplication:
|
||||
updater=updater,
|
||||
concurrent_updates=-1,
|
||||
post_init=None,
|
||||
post_shutdown=None,
|
||||
)
|
||||
|
||||
def test_custom_context_init(self, bot):
|
||||
@@ -1433,6 +1440,52 @@ class TestApplication:
|
||||
thread.join()
|
||||
assert events == ["init", "post_init", "start_polling"], "Wrong order of events detected!"
|
||||
|
||||
@pytest.mark.skipif(
|
||||
platform.system() == "Windows",
|
||||
reason="Can't send signals without stopping whole process on windows",
|
||||
)
|
||||
def test_run_polling_post_shutdown(self, bot, monkeypatch):
|
||||
events = []
|
||||
|
||||
async def get_updates(*args, **kwargs):
|
||||
# This makes sure that other coroutines have a chance of running as well
|
||||
await asyncio.sleep(0)
|
||||
return []
|
||||
|
||||
def thread_target():
|
||||
waited = 0
|
||||
while not app.running:
|
||||
time.sleep(0.05)
|
||||
waited += 0.05
|
||||
if waited > 5:
|
||||
pytest.fail("App apparently won't start")
|
||||
|
||||
os.kill(os.getpid(), signal.SIGINT)
|
||||
|
||||
async def post_shutdown(app: Application) -> None:
|
||||
events.append("post_shutdown")
|
||||
|
||||
app = Application.builder().token(bot.token).post_shutdown(post_shutdown).build()
|
||||
monkeypatch.setattr(app.bot, "get_updates", get_updates)
|
||||
monkeypatch.setattr(
|
||||
app, "shutdown", call_after(app.shutdown, lambda _: events.append("shutdown"))
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
app.updater,
|
||||
"shutdown",
|
||||
call_after(app.updater.shutdown, lambda _: events.append("updater.shutdown")),
|
||||
)
|
||||
|
||||
thread = Thread(target=thread_target)
|
||||
thread.start()
|
||||
app.run_polling(drop_pending_updates=True, close_loop=False)
|
||||
thread.join()
|
||||
assert events == [
|
||||
"updater.shutdown",
|
||||
"shutdown",
|
||||
"post_shutdown",
|
||||
], "Wrong order of events detected!"
|
||||
|
||||
@pytest.mark.skipif(
|
||||
platform.system() == "Windows",
|
||||
reason="Can't send signals without stopping whole process on windows",
|
||||
@@ -1620,6 +1673,69 @@ class TestApplication:
|
||||
thread.join()
|
||||
assert events == ["init", "post_init", "start_webhook"], "Wrong order of events detected!"
|
||||
|
||||
@pytest.mark.skipif(
|
||||
platform.system() == "Windows",
|
||||
reason="Can't send signals without stopping whole process on windows",
|
||||
)
|
||||
def test_run_webhook_post_shutdown(self, bot, monkeypatch):
|
||||
events = []
|
||||
|
||||
async def delete_webhook(*args, **kwargs):
|
||||
return True
|
||||
|
||||
async def set_webhook(*args, **kwargs):
|
||||
return True
|
||||
|
||||
async def get_updates(*args, **kwargs):
|
||||
# This makes sure that other coroutines have a chance of running as well
|
||||
await asyncio.sleep(0)
|
||||
return []
|
||||
|
||||
def thread_target():
|
||||
waited = 0
|
||||
while not app.running:
|
||||
time.sleep(0.05)
|
||||
waited += 0.05
|
||||
if waited > 5:
|
||||
pytest.fail("App apparently won't start")
|
||||
|
||||
os.kill(os.getpid(), signal.SIGINT)
|
||||
|
||||
async def post_shutdown(app: Application) -> None:
|
||||
events.append("post_shutdown")
|
||||
|
||||
app = Application.builder().token(bot.token).post_shutdown(post_shutdown).build()
|
||||
monkeypatch.setattr(app.bot, "set_webhook", set_webhook)
|
||||
monkeypatch.setattr(app.bot, "delete_webhook", delete_webhook)
|
||||
monkeypatch.setattr(
|
||||
app, "shutdown", call_after(app.shutdown, lambda _: events.append("shutdown"))
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
app.updater,
|
||||
"shutdown",
|
||||
call_after(app.updater.shutdown, lambda _: events.append("updater.shutdown")),
|
||||
)
|
||||
|
||||
thread = Thread(target=thread_target)
|
||||
thread.start()
|
||||
|
||||
ip = "127.0.0.1"
|
||||
port = randrange(1024, 49152)
|
||||
|
||||
app.run_webhook(
|
||||
ip_address=ip,
|
||||
port=port,
|
||||
url_path="TOKEN",
|
||||
drop_pending_updates=True,
|
||||
close_loop=False,
|
||||
)
|
||||
thread.join()
|
||||
assert events == [
|
||||
"updater.shutdown",
|
||||
"shutdown",
|
||||
"post_shutdown",
|
||||
], "Wrong order of events detected!"
|
||||
|
||||
@pytest.mark.skipif(
|
||||
platform.system() == "Windows",
|
||||
reason="Can't send signals without stopping whole process on windows",
|
||||
@@ -1718,7 +1834,12 @@ class TestApplication:
|
||||
|
||||
assert not app.running
|
||||
assert not app.updater.running
|
||||
assert set(shutdowns) == {"application", "updater"}
|
||||
if method == "initialize":
|
||||
# If App.initialize fails, then App.shutdown pretty much does nothing, especially
|
||||
# doesn't call Updater.shutdown.
|
||||
assert set(shutdowns) == {"application"}
|
||||
else:
|
||||
assert set(shutdowns) == {"application", "updater"}
|
||||
|
||||
@pytest.mark.parametrize("method", ["start_polling", "start_webhook"])
|
||||
@pytest.mark.filterwarnings("ignore::telegram.warnings.PTBUserWarning")
|
||||
|
||||
@@ -23,6 +23,7 @@ import httpx
|
||||
import pytest
|
||||
|
||||
from telegram.ext import (
|
||||
AIORateLimiter,
|
||||
Application,
|
||||
ApplicationBuilder,
|
||||
ContextTypes,
|
||||
@@ -82,6 +83,7 @@ class TestApplicationBuilder:
|
||||
assert app.bot.private_key is None
|
||||
assert app.bot.arbitrary_callback_data is False
|
||||
assert app.bot.defaults is None
|
||||
assert app.bot.rate_limiter is None
|
||||
|
||||
get_updates_client = app.bot._request[0]._client
|
||||
assert get_updates_client.limits == httpx.Limits(
|
||||
@@ -93,7 +95,7 @@ class TestApplicationBuilder:
|
||||
)
|
||||
|
||||
client = app.bot.request._client
|
||||
assert client.limits == httpx.Limits(max_connections=128, max_keepalive_connections=128)
|
||||
assert client.limits == httpx.Limits(max_connections=256, max_keepalive_connections=256)
|
||||
assert client.proxies is None
|
||||
assert client.timeout == httpx.Timeout(connect=5.0, read=5.0, write=5.0, pool=1.0)
|
||||
|
||||
@@ -107,6 +109,7 @@ class TestApplicationBuilder:
|
||||
|
||||
assert app.persistence is None
|
||||
assert app.post_init is None
|
||||
assert app.post_shutdown is None
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"method, description", _BOT_CHECKS, ids=[entry[0] for entry in _BOT_CHECKS]
|
||||
@@ -195,6 +198,7 @@ class TestApplicationBuilder:
|
||||
"proxy_url",
|
||||
"bot",
|
||||
"update_queue",
|
||||
"rate_limiter",
|
||||
]
|
||||
+ [entry[0] for entry in _BOT_CHECKS],
|
||||
)
|
||||
@@ -246,10 +250,13 @@ class TestApplicationBuilder:
|
||||
defaults = Defaults()
|
||||
request = HTTPXRequest()
|
||||
get_updates_request = HTTPXRequest()
|
||||
rate_limiter = AIORateLimiter()
|
||||
builder.token(bot.token).base_url("base_url").base_file_url("base_file_url").private_key(
|
||||
PRIVATE_KEY
|
||||
).defaults(defaults).arbitrary_callback_data(42).request(request).get_updates_request(
|
||||
get_updates_request
|
||||
).rate_limiter(
|
||||
rate_limiter
|
||||
)
|
||||
built_bot = builder.build().bot
|
||||
|
||||
@@ -265,6 +272,7 @@ class TestApplicationBuilder:
|
||||
assert built_bot._request[0] is get_updates_request
|
||||
assert built_bot.callback_data_cache.maxsize == 42
|
||||
assert built_bot.private_key
|
||||
assert built_bot.rate_limiter is rate_limiter
|
||||
|
||||
@dataclass
|
||||
class Client:
|
||||
@@ -322,6 +330,9 @@ class TestApplicationBuilder:
|
||||
async def post_init(app: Application) -> None:
|
||||
pass
|
||||
|
||||
async def post_shutdown(app: Application) -> None:
|
||||
pass
|
||||
|
||||
app = (
|
||||
builder.token(bot.token)
|
||||
.job_queue(job_queue)
|
||||
@@ -330,6 +341,7 @@ class TestApplicationBuilder:
|
||||
.context_types(context_types)
|
||||
.concurrent_updates(concurrent_updates)
|
||||
.post_init(post_init)
|
||||
.post_shutdown(post_shutdown)
|
||||
).build()
|
||||
assert app.job_queue is job_queue
|
||||
assert app.job_queue.application is app
|
||||
@@ -341,6 +353,7 @@ class TestApplicationBuilder:
|
||||
assert app.context_types is context_types
|
||||
assert app.concurrent_updates == concurrent_updates
|
||||
assert app.post_init is post_init
|
||||
assert app.post_shutdown is post_shutdown
|
||||
|
||||
updater = Updater(bot=bot, update_queue=update_queue)
|
||||
app = ApplicationBuilder().updater(updater).build()
|
||||
|
||||
+94
-85
@@ -137,6 +137,34 @@ xfail = pytest.mark.xfail(
|
||||
)
|
||||
|
||||
|
||||
def bot_methods(ext_bot=True):
|
||||
arg_values = []
|
||||
ids = []
|
||||
non_api_methods = [
|
||||
"de_json",
|
||||
"de_list",
|
||||
"to_dict",
|
||||
"to_json",
|
||||
"parse_data",
|
||||
"get_bot",
|
||||
"set_bot",
|
||||
"initialize",
|
||||
"shutdown",
|
||||
"insert_callback_data",
|
||||
]
|
||||
classes = (Bot, ExtBot) if ext_bot else (Bot,)
|
||||
for cls in classes:
|
||||
for name, attribute in inspect.getmembers(cls, predicate=inspect.isfunction):
|
||||
if name.startswith("_") or name in non_api_methods:
|
||||
continue
|
||||
arg_values.append((cls, name, attribute))
|
||||
ids.append(f"{cls.__name__}.{name}")
|
||||
|
||||
return pytest.mark.parametrize(
|
||||
argnames="bot_class, bot_method_name,bot_method", argvalues=arg_values, ids=ids
|
||||
)
|
||||
|
||||
|
||||
class TestBot:
|
||||
"""
|
||||
Most are executed on tg.ext.ExtBot, as that class only extends the functionality of tg.bot
|
||||
@@ -155,22 +183,6 @@ class TestBot:
|
||||
assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'"
|
||||
assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot"
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"token",
|
||||
argvalues=[
|
||||
"123",
|
||||
"12a:abcd1234",
|
||||
"12:abcd1234",
|
||||
"1234:abcd1234\n",
|
||||
" 1234:abcd1234",
|
||||
" 1234:abcd1234\r",
|
||||
"1234:abcd 1234",
|
||||
],
|
||||
)
|
||||
async def test_invalid_token(self, token):
|
||||
with pytest.raises(InvalidToken, match="Invalid token"):
|
||||
Bot(token)
|
||||
|
||||
async def test_initialize_and_shutdown(self, bot, monkeypatch):
|
||||
async def initialize(*args, **kwargs):
|
||||
self.test_flag = ["initialize"]
|
||||
@@ -279,12 +291,14 @@ class TestBot:
|
||||
assert acd_bot.arbitrary_callback_data == acd
|
||||
assert acd_bot.callback_data_cache.maxsize == maxsize
|
||||
|
||||
@flaky(3, 1)
|
||||
async def test_invalid_token_server_response(self, monkeypatch):
|
||||
monkeypatch.setattr("telegram.Bot._validate_token", lambda x, y: "")
|
||||
with pytest.raises(InvalidToken):
|
||||
async with make_bot(token="12") as bot:
|
||||
await bot.get_me()
|
||||
async def test_no_token_passed(self):
|
||||
with pytest.raises(InvalidToken, match="You must pass the token"):
|
||||
Bot("")
|
||||
|
||||
async def test_invalid_token_server_response(self):
|
||||
with pytest.raises(InvalidToken, match="The token `12` was rejected by the server."):
|
||||
async with make_bot(token="12"):
|
||||
pass
|
||||
|
||||
async def test_unknown_kwargs(self, bot, monkeypatch):
|
||||
async def post(url, request_data: RequestData, *args, **kwargs):
|
||||
@@ -366,29 +380,10 @@ class TestBot:
|
||||
with pytest.raises(pickle.PicklingError, match="Bot objects cannot be pickled"):
|
||||
pickle.dumps(bot)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"bot_method_name",
|
||||
argvalues=[
|
||||
name
|
||||
for name, _ in inspect.getmembers(Bot, predicate=inspect.isfunction)
|
||||
if not name.startswith("_")
|
||||
and name
|
||||
not in [
|
||||
"de_json",
|
||||
"de_list",
|
||||
"to_dict",
|
||||
"to_json",
|
||||
"parse_data",
|
||||
"get_updates",
|
||||
"getUpdates",
|
||||
"get_bot",
|
||||
"set_bot",
|
||||
"initialize",
|
||||
"shutdown",
|
||||
]
|
||||
],
|
||||
)
|
||||
async def test_defaults_handling(self, bot_method_name, bot, raw_bot, monkeypatch):
|
||||
@bot_methods(ext_bot=False)
|
||||
async def test_defaults_handling(
|
||||
self, bot_class, bot_method_name, bot_method, bot, raw_bot, monkeypatch
|
||||
):
|
||||
"""
|
||||
Here we check that the bot methods handle tg.ext.Defaults correctly. This has two parts:
|
||||
|
||||
@@ -408,6 +403,9 @@ class TestBot:
|
||||
Finally, there are some tests for Defaults.{parse_mode, quote, allow_sending_without_reply}
|
||||
at the appropriate places, as those are the only things we can actually check.
|
||||
"""
|
||||
if bot_method_name.lower().replace("_", "") == "getupdates":
|
||||
return
|
||||
|
||||
try:
|
||||
# Check that ExtBot does the right thing
|
||||
bot_method = getattr(bot, bot_method_name)
|
||||
@@ -483,9 +481,9 @@ class TestBot:
|
||||
corresponding methods of tg.Bot.
|
||||
"""
|
||||
# Some methods of ext.ExtBot
|
||||
global_extra_args = set()
|
||||
global_extra_args = {"rate_limit_args"}
|
||||
extra_args_per_method = defaultdict(
|
||||
set, {"__init__": {"arbitrary_callback_data", "defaults"}}
|
||||
set, {"__init__": {"arbitrary_callback_data", "defaults", "rate_limiter"}}
|
||||
)
|
||||
different_hints_per_method = defaultdict(set, {"__setattr__": {"ext_bot"}})
|
||||
|
||||
@@ -958,30 +956,18 @@ class TestBot:
|
||||
assert not unprotected_dice.has_protected_content
|
||||
|
||||
@flaky(3, 1)
|
||||
@pytest.mark.parametrize(
|
||||
"chat_action",
|
||||
[
|
||||
ChatAction.FIND_LOCATION,
|
||||
ChatAction.RECORD_VIDEO,
|
||||
ChatAction.RECORD_VIDEO_NOTE,
|
||||
ChatAction.RECORD_VOICE,
|
||||
ChatAction.TYPING,
|
||||
ChatAction.UPLOAD_DOCUMENT,
|
||||
ChatAction.UPLOAD_PHOTO,
|
||||
ChatAction.UPLOAD_VIDEO,
|
||||
ChatAction.UPLOAD_VIDEO_NOTE,
|
||||
ChatAction.UPLOAD_VOICE,
|
||||
ChatAction.CHOOSE_STICKER,
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize("chat_action", list(ChatAction))
|
||||
async def test_send_chat_action(self, bot, chat_id, chat_action):
|
||||
assert await bot.send_chat_action(chat_id, chat_action)
|
||||
|
||||
async def test_wrong_chat_action(self, bot, chat_id):
|
||||
with pytest.raises(BadRequest, match="Wrong parameter action"):
|
||||
await bot.send_chat_action(chat_id, "unknown action")
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_answer_web_app_query(self, bot, monkeypatch):
|
||||
params = False
|
||||
|
||||
# For now just test that our internals pass the correct data
|
||||
|
||||
async def make_assertion(url, request_data: RequestData, *args, **kwargs):
|
||||
@@ -2253,8 +2239,8 @@ class TestBot:
|
||||
)
|
||||
|
||||
# get_sticker_set, upload_sticker_file, create_new_sticker_set, add_sticker_to_set,
|
||||
# set_sticker_position_in_set and delete_sticker_from_set are tested in the
|
||||
# test_sticker module.
|
||||
# set_sticker_position_in_set, delete_sticker_from_set and get_custom_emoji_stickers
|
||||
# are tested in the test_sticker module.
|
||||
|
||||
async def test_timeout_propagation_explicit(self, monkeypatch, bot, chat_id):
|
||||
# Use BaseException that's not a subclass of Exception such that
|
||||
@@ -2923,24 +2909,47 @@ class TestBot:
|
||||
bot.callback_data_cache.clear_callback_data()
|
||||
bot.callback_data_cache.clear_callback_queries()
|
||||
|
||||
def test_camel_case_redefinition_extbot(self):
|
||||
invalid_camel_case_functions = []
|
||||
for function_name, function in ExtBot.__dict__.items():
|
||||
camel_case_function = getattr(ExtBot, to_camel_case(function_name), False)
|
||||
if callable(function) and camel_case_function and camel_case_function is not function:
|
||||
invalid_camel_case_functions.append(function_name)
|
||||
assert invalid_camel_case_functions == []
|
||||
@bot_methods()
|
||||
def test_camel_case_aliases(self, bot_class, bot_method_name, bot_method):
|
||||
camel_case_name = to_camel_case(bot_method_name)
|
||||
camel_case_function = getattr(bot_class, camel_case_name, False)
|
||||
assert camel_case_function is not False, f"{camel_case_name} not found"
|
||||
assert camel_case_function is bot_method, f"{camel_case_name} is not {bot_method}"
|
||||
|
||||
def test_camel_case_bot(self):
|
||||
not_available_camelcase_functions = []
|
||||
for function_name, function in Bot.__dict__.items():
|
||||
if (
|
||||
function_name.startswith("_")
|
||||
or not callable(function)
|
||||
or function_name in ["to_dict"]
|
||||
):
|
||||
continue
|
||||
camel_case_function = getattr(Bot, to_camel_case(function_name), False)
|
||||
if not camel_case_function:
|
||||
not_available_camelcase_functions.append(function_name)
|
||||
assert not_available_camelcase_functions == []
|
||||
@bot_methods()
|
||||
def test_coroutine_functions(self, bot_class, bot_method_name, bot_method):
|
||||
"""Check that all bot methods are defined as async def ..."""
|
||||
# not islower() skips the camelcase aliases
|
||||
if not bot_method_name.islower():
|
||||
return
|
||||
# unfortunately `inspect.iscoroutinefunction` doesn't do the trick directly,
|
||||
# as the @_log decorator interferes
|
||||
source = "".join(inspect.getsourcelines(bot_method)[0])
|
||||
assert (
|
||||
f"async def {bot_method_name}" in source
|
||||
), f"{bot_method_name} should be a coroutine function"
|
||||
|
||||
@bot_methods()
|
||||
def test_api_kwargs_and_timeouts_present(self, bot_class, bot_method_name, bot_method):
|
||||
"""Check that all bot methods have `api_kwargs` and timeout params."""
|
||||
param_names = inspect.signature(bot_method).parameters.keys()
|
||||
assert (
|
||||
"pool_timeout" in param_names
|
||||
), f"{bot_method_name} is missing the parameter `pool_timeout`"
|
||||
assert (
|
||||
"read_timeout" in param_names
|
||||
), f"{bot_method_name} is missing the parameter `read_timeout`"
|
||||
assert (
|
||||
"connect_timeout" in param_names
|
||||
), f"{bot_method_name} is missing the parameter `connect_timeout`"
|
||||
assert (
|
||||
"write_timeout" in param_names
|
||||
), f"{bot_method_name} is missing the parameter `write_timeout`"
|
||||
assert (
|
||||
"api_kwargs" in param_names
|
||||
), f"{bot_method_name} is missing the parameter `api_kwargs`"
|
||||
|
||||
if bot_class is ExtBot and bot_method_name.replace("_", "").lower() != "getupdates":
|
||||
assert (
|
||||
"rate_limit_args" in param_names
|
||||
), f"{bot_method_name} of ExtBot is missing the parameter `rate_limit_args`"
|
||||
|
||||
@@ -30,7 +30,7 @@ from telegram import (
|
||||
User,
|
||||
)
|
||||
from telegram.error import TelegramError
|
||||
from telegram.ext import ApplicationBuilder, CallbackContext
|
||||
from telegram.ext import ApplicationBuilder, CallbackContext, Job
|
||||
|
||||
"""
|
||||
CallbackContext.refresh_data is tested in TestBasePersistence
|
||||
@@ -116,11 +116,10 @@ class TestCallbackContext:
|
||||
update = Update(
|
||||
0, message=Message(0, None, Chat(1, "chat"), from_user=User(1, "user", False))
|
||||
)
|
||||
job = object()
|
||||
coroutine = object()
|
||||
|
||||
callback_context = CallbackContext.from_error(
|
||||
update=update, error=error, application=app, job=job, coroutine=coroutine
|
||||
update=update, error=error, application=app, coroutine=coroutine
|
||||
)
|
||||
|
||||
assert callback_context.error is error
|
||||
@@ -131,6 +130,22 @@ class TestCallbackContext:
|
||||
assert callback_context.job_queue is app.job_queue
|
||||
assert callback_context.update_queue is app.update_queue
|
||||
assert callback_context.coroutine is coroutine
|
||||
|
||||
def test_from_error_job_user_chat_data(self, app):
|
||||
error = TelegramError("test")
|
||||
job = Job(callback=lambda x: x, data=None, chat_id=42, user_id=43)
|
||||
|
||||
callback_context = CallbackContext.from_error(
|
||||
update=None, error=error, application=app, job=job
|
||||
)
|
||||
|
||||
assert callback_context.error is error
|
||||
assert callback_context.chat_data == {}
|
||||
assert callback_context.user_data == {}
|
||||
assert callback_context.bot_data is app.bot_data
|
||||
assert callback_context.bot is app.bot
|
||||
assert callback_context.job_queue is app.job_queue
|
||||
assert callback_context.update_queue is app.update_queue
|
||||
assert callback_context.job is job
|
||||
|
||||
def test_match(self, app):
|
||||
|
||||
@@ -44,6 +44,7 @@ def chat(bot):
|
||||
has_protected_content=True,
|
||||
join_to_send_messages=True,
|
||||
join_by_request=True,
|
||||
has_restricted_voice_and_video_messages=True,
|
||||
)
|
||||
|
||||
|
||||
@@ -68,6 +69,7 @@ class TestChat:
|
||||
has_private_forwards = True
|
||||
join_to_send_messages = True
|
||||
join_by_request = True
|
||||
has_restricted_voice_and_video_messages = True
|
||||
|
||||
def test_slot_behaviour(self, chat, mro_slots):
|
||||
for attr in chat.__slots__:
|
||||
@@ -92,6 +94,9 @@ class TestChat:
|
||||
"location": self.location.to_dict(),
|
||||
"join_to_send_messages": self.join_to_send_messages,
|
||||
"join_by_request": self.join_by_request,
|
||||
"has_restricted_voice_and_video_messages": (
|
||||
self.has_restricted_voice_and_video_messages
|
||||
),
|
||||
}
|
||||
chat = Chat.de_json(json_dict, bot)
|
||||
|
||||
@@ -112,6 +117,10 @@ class TestChat:
|
||||
assert chat.location.address == self.location.address
|
||||
assert chat.join_to_send_messages == self.join_to_send_messages
|
||||
assert chat.join_by_request == self.join_by_request
|
||||
assert (
|
||||
chat.has_restricted_voice_and_video_messages
|
||||
== self.has_restricted_voice_and_video_messages
|
||||
)
|
||||
|
||||
def test_to_dict(self, chat):
|
||||
chat_dict = chat.to_dict()
|
||||
@@ -131,6 +140,10 @@ class TestChat:
|
||||
assert chat_dict["location"] == chat.location.to_dict()
|
||||
assert chat_dict["join_to_send_messages"] == chat.join_to_send_messages
|
||||
assert chat_dict["join_by_request"] == chat.join_by_request
|
||||
assert (
|
||||
chat_dict["has_restricted_voice_and_video_messages"]
|
||||
== chat.has_restricted_voice_and_video_messages
|
||||
)
|
||||
|
||||
def test_enum_init(self):
|
||||
chat = Chat(id=1, type="foo")
|
||||
|
||||
+18
-3
@@ -17,13 +17,12 @@
|
||||
# You should have received a copy of the GNU Lesser Public License
|
||||
# along with this program. If not, see [http://www.gnu.org/licenses/].
|
||||
import json
|
||||
from enum import IntEnum
|
||||
|
||||
import pytest
|
||||
from flaky import flaky
|
||||
|
||||
from telegram import constants
|
||||
from telegram._utils.enum import StringEnum
|
||||
from telegram._utils.enum import IntEnum, StringEnum
|
||||
from telegram.error import BadRequest
|
||||
from tests.conftest import data_file
|
||||
|
||||
@@ -62,8 +61,24 @@ class TestConstants:
|
||||
assert json.dumps(IntEnumTest.FOO) == json.dumps(1)
|
||||
|
||||
def test_string_representation(self):
|
||||
# test __repr__
|
||||
assert repr(StrEnumTest.FOO) == "<StrEnumTest.FOO>"
|
||||
assert str(StrEnumTest.FOO) == "StrEnumTest.FOO"
|
||||
|
||||
# test __format__
|
||||
assert f"{StrEnumTest.FOO} this {StrEnumTest.BAR}" == "foo this bar"
|
||||
assert f"{StrEnumTest.FOO:*^10}" == "***foo****"
|
||||
|
||||
# test __str__
|
||||
assert str(StrEnumTest.FOO) == "foo"
|
||||
|
||||
def test_int_representation(self):
|
||||
# test __repr__
|
||||
assert repr(IntEnumTest.FOO) == "<IntEnumTest.FOO>"
|
||||
# test __format__
|
||||
assert f"{IntEnumTest.FOO}/0 is undefined!" == "1/0 is undefined!"
|
||||
assert f"{IntEnumTest.FOO:*^10}" == "****1*****"
|
||||
# test __str__
|
||||
assert str(IntEnumTest.FOO) == "1"
|
||||
|
||||
def test_string_inheritance(self):
|
||||
assert isinstance(StrEnumTest.FOO, str)
|
||||
|
||||
@@ -740,6 +740,12 @@ class TestConversationHandler:
|
||||
],
|
||||
bot=bot,
|
||||
)
|
||||
|
||||
# First check that updates without user won't be handled
|
||||
message.from_user = None
|
||||
assert not handler.check_update(Update(update_id=0, message=message))
|
||||
|
||||
message.from_user = user1
|
||||
async with app:
|
||||
await app.process_update(Update(update_id=0, message=message))
|
||||
|
||||
|
||||
@@ -48,16 +48,16 @@ Because imports in pytest are intricate, we just run
|
||||
|
||||
pytest -k test_helpers.py
|
||||
|
||||
with the TEST_NO_PYTZ environment variable set in addition to the regular test suite.
|
||||
with the TEST_PYTZ environment variable set to False in addition to the regular test suite.
|
||||
Because actually uninstalling pytz would lead to errors in the test suite we just mock the
|
||||
import to raise the expected exception.
|
||||
|
||||
Note that a fixture that just does this for every test that needs it is a nice idea, but for some
|
||||
reason makes test_updater.py hang indefinitely on GitHub Actions (at least when Hinrich tried that)
|
||||
"""
|
||||
TEST_NO_PYTZ = env_var_2_bool(os.getenv("TEST_NO_PYTZ", False))
|
||||
TEST_PYTZ = env_var_2_bool(os.getenv("TEST_PYTZ", True))
|
||||
|
||||
if TEST_NO_PYTZ:
|
||||
if not TEST_PYTZ:
|
||||
orig_import = __import__
|
||||
|
||||
def import_mock(module_name, *args, **kwargs):
|
||||
@@ -72,7 +72,7 @@ if TEST_NO_PYTZ:
|
||||
class TestDatetime:
|
||||
def test_helpers_utc(self):
|
||||
# Here we just test, that we got the correct UTC variant
|
||||
if TEST_NO_PYTZ:
|
||||
if not TEST_PYTZ:
|
||||
assert tg_dtm.UTC is tg_dtm.DTM_UTC
|
||||
else:
|
||||
assert tg_dtm.UTC is not tg_dtm.DTM_UTC
|
||||
|
||||
@@ -828,7 +828,7 @@ class TestFilters:
|
||||
|
||||
def test_filters_sticker(self, update):
|
||||
assert not filters.Sticker.ALL.check_update(update)
|
||||
update.message.sticker = Sticker("1", "uniq", 1, 2, False, False)
|
||||
update.message.sticker = Sticker("1", "uniq", 1, 2, False, False, Sticker.REGULAR)
|
||||
assert filters.Sticker.ALL.check_update(update)
|
||||
assert filters.Sticker.STATIC.check_update(update)
|
||||
assert not filters.Sticker.VIDEO.check_update(update)
|
||||
|
||||
+52
-24
@@ -25,32 +25,50 @@ from telegram.constants import MessageType
|
||||
|
||||
|
||||
class TestHelpers:
|
||||
def test_escape_markdown(self):
|
||||
test_str = "*bold*, _italic_, `code`, [text_link](http://github.com/)"
|
||||
expected_str = r"\*bold\*, \_italic\_, \`code\`, \[text\_link](http://github.com/)"
|
||||
@pytest.mark.parametrize(
|
||||
"test_str,expected",
|
||||
[
|
||||
("*bold*", r"\*bold\*"),
|
||||
("_italic_", r"\_italic\_"),
|
||||
("`code`", r"\`code\`"),
|
||||
("[text_link](https://github.com/)", r"\[text\_link](https://github.com/)"),
|
||||
],
|
||||
ids=["bold", "italic", "code", "text_link"],
|
||||
)
|
||||
def test_escape_markdown(self, test_str, expected):
|
||||
assert expected == helpers.escape_markdown(test_str)
|
||||
|
||||
assert expected_str == helpers.escape_markdown(test_str)
|
||||
@pytest.mark.parametrize(
|
||||
"test_str, expected",
|
||||
[
|
||||
(r"a_b*c[d]e", r"a\_b\*c\[d\]e"),
|
||||
(r"(fg) ", r"\(fg\) "),
|
||||
(r"h~I`>JK#L+MN", r"h\~I\`\>JK\#L\+MN"),
|
||||
(r"-O=|p{qr}s.t!\ ", r"\-O\=\|p\{qr\}s\.t\!\\ "),
|
||||
(r"\u", r"\\u"),
|
||||
],
|
||||
)
|
||||
def test_escape_markdown_v2(self, test_str, expected):
|
||||
assert expected == helpers.escape_markdown(test_str, version=2)
|
||||
|
||||
def test_escape_markdown_v2(self):
|
||||
test_str = r"a_b*c[d]e (fg) h~I`>JK#L+MN -O=|p{qr}s.t!\ \u"
|
||||
expected_str = r"a\_b\*c\[d\]e \(fg\) h\~I\`\>JK\#L\+MN \-O\=\|p\{qr\}s\.t\!\\ \\u"
|
||||
|
||||
assert expected_str == helpers.escape_markdown(test_str, version=2)
|
||||
|
||||
def test_escape_markdown_v2_monospaced(self):
|
||||
|
||||
test_str = r"mono/pre: `abc` \int (`\some \`stuff)"
|
||||
expected_str = "mono/pre: \\`abc\\` \\\\int (\\`\\\\some \\\\\\`stuff)"
|
||||
|
||||
assert expected_str == helpers.escape_markdown(
|
||||
@pytest.mark.parametrize(
|
||||
"test_str, expected",
|
||||
[
|
||||
(r"mono/pre:", r"mono/pre:"),
|
||||
("`abc`", r"\`abc\`"),
|
||||
(r"\int", r"\\int"),
|
||||
(r"(`\some \` stuff)", r"(\`\\some \\\` stuff)"),
|
||||
],
|
||||
)
|
||||
def test_escape_markdown_v2_monospaced(self, test_str, expected):
|
||||
assert expected == helpers.escape_markdown(
|
||||
test_str, version=2, entity_type=MessageEntity.PRE
|
||||
)
|
||||
assert expected_str == helpers.escape_markdown(
|
||||
assert expected == helpers.escape_markdown(
|
||||
test_str, version=2, entity_type=MessageEntity.CODE
|
||||
)
|
||||
|
||||
def test_escape_markdown_v2_text_link(self):
|
||||
|
||||
test_str = "https://url.containing/funny)cha)\\ra\\)cter\\s"
|
||||
expected_str = "https://url.containing/funny\\)cha\\)\\\\ra\\\\\\)cter\\\\s"
|
||||
|
||||
@@ -59,8 +77,10 @@ class TestHelpers:
|
||||
)
|
||||
|
||||
def test_markdown_invalid_version(self):
|
||||
with pytest.raises(ValueError):
|
||||
with pytest.raises(ValueError, match="Markdown version must be either"):
|
||||
helpers.escape_markdown("abc", version=-1)
|
||||
with pytest.raises(ValueError, match="Markdown version must be either"):
|
||||
helpers.mention_markdown(1, "abc", version=-1)
|
||||
|
||||
def test_create_deep_linked_url(self):
|
||||
username = "JamesTheMock"
|
||||
@@ -124,12 +144,20 @@ class TestHelpers:
|
||||
|
||||
assert expected == helpers.mention_html(1, "the name")
|
||||
|
||||
def test_mention_markdown(self):
|
||||
expected = "[the name](tg://user?id=1)"
|
||||
|
||||
assert expected == helpers.mention_markdown(1, "the name")
|
||||
@pytest.mark.parametrize(
|
||||
"test_str, expected",
|
||||
[
|
||||
("the name", "[the name](tg://user?id=1)"),
|
||||
("under_score", "[under_score](tg://user?id=1)"),
|
||||
("starred*text", "[starred*text](tg://user?id=1)"),
|
||||
("`backtick`", "[`backtick`](tg://user?id=1)"),
|
||||
("[square brackets", "[[square brackets](tg://user?id=1)"),
|
||||
],
|
||||
)
|
||||
def test_mention_markdown(self, test_str, expected):
|
||||
assert expected == helpers.mention_markdown(1, test_str)
|
||||
|
||||
def test_mention_markdown_2(self):
|
||||
expected = r"[the\_name](tg://user?id=1)"
|
||||
|
||||
assert expected == helpers.mention_markdown(1, "the_name")
|
||||
assert expected == helpers.mention_markdown(1, "the_name", 2)
|
||||
|
||||
@@ -86,7 +86,12 @@ class TestJobQueue:
|
||||
self.job_time = time.time()
|
||||
|
||||
async def error_handler_context(self, update, context):
|
||||
self.received_error = (str(context.error), context.job)
|
||||
self.received_error = (
|
||||
str(context.error),
|
||||
context.job,
|
||||
context.user_data,
|
||||
context.chat_data,
|
||||
)
|
||||
|
||||
async def error_handler_raise_error(self, *args):
|
||||
raise Exception("Failing bigly")
|
||||
@@ -453,7 +458,7 @@ class TestJobQueue:
|
||||
async def test_process_error_context(self, job_queue, app):
|
||||
app.add_error_handler(self.error_handler_context)
|
||||
|
||||
job = job_queue.run_once(self.job_with_exception, 0.1)
|
||||
job = job_queue.run_once(self.job_with_exception, 0.1, chat_id=42, user_id=43)
|
||||
await asyncio.sleep(0.15)
|
||||
assert self.received_error[0] == "Test Error"
|
||||
assert self.received_error[1] is job
|
||||
@@ -461,6 +466,8 @@ class TestJobQueue:
|
||||
await job.run(app)
|
||||
assert self.received_error[0] == "Test Error"
|
||||
assert self.received_error[1] is job
|
||||
assert self.received_error[2] is app.user_data[43]
|
||||
assert self.received_error[3] is app.chat_data[42]
|
||||
|
||||
# Remove handler
|
||||
app.remove_error_handler(self.error_handler_context)
|
||||
|
||||
+61
-1
@@ -105,7 +105,7 @@ def message(bot):
|
||||
)
|
||||
},
|
||||
{"photo": [PhotoSize("photo_id", "unique_id", 50, 50)], "caption": "photo_file"},
|
||||
{"sticker": Sticker("sticker_id", "unique_id", 50, 50, True, False)},
|
||||
{"sticker": Sticker("sticker_id", "unique_id", 50, 50, True, False, Sticker.REGULAR)},
|
||||
{"video": Video("video_id", "unique_id", 12, 12, 12), "caption": "video_file"},
|
||||
{"voice": Voice("voice_id", "unique_id", 5)},
|
||||
{"video_note": VideoNote("video_note_id", "unique_id", 20, 12)},
|
||||
@@ -542,6 +542,36 @@ class TestMessage:
|
||||
)
|
||||
assert expected == message.text_markdown
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"type_",
|
||||
argvalues=[
|
||||
"text_html",
|
||||
"text_html_urled",
|
||||
"text_markdown",
|
||||
"text_markdown_urled",
|
||||
"text_markdown_v2",
|
||||
"text_markdown_v2_urled",
|
||||
],
|
||||
)
|
||||
def test_text_custom_emoji(self, type_):
|
||||
text = "Look a custom emoji: 😎"
|
||||
expected = "Look a custom emoji: 😎"
|
||||
emoji_entity = MessageEntity(
|
||||
type=MessageEntity.CUSTOM_EMOJI,
|
||||
offset=21,
|
||||
length=2,
|
||||
custom_emoji_id="5472409228461217725",
|
||||
)
|
||||
message = Message(
|
||||
1,
|
||||
from_user=self.from_user,
|
||||
date=self.date,
|
||||
chat=self.chat,
|
||||
text=text,
|
||||
entities=[emoji_entity],
|
||||
)
|
||||
assert expected == message[type_]
|
||||
|
||||
def test_caption_html_simple(self):
|
||||
test_html_string = (
|
||||
"<u>Test</u> for <<b>bold</b>, <i>ita_lic</i>, "
|
||||
@@ -651,6 +681,36 @@ class TestMessage:
|
||||
)
|
||||
assert expected == message.caption_markdown
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"type_",
|
||||
argvalues=[
|
||||
"caption_html",
|
||||
"caption_html_urled",
|
||||
"caption_markdown",
|
||||
"caption_markdown_urled",
|
||||
"caption_markdown_v2",
|
||||
"caption_markdown_v2_urled",
|
||||
],
|
||||
)
|
||||
def test_caption_custom_emoji(self, type_):
|
||||
caption = "Look a custom emoji: 😎"
|
||||
expected = "Look a custom emoji: 😎"
|
||||
emoji_entity = MessageEntity(
|
||||
type=MessageEntity.CUSTOM_EMOJI,
|
||||
offset=21,
|
||||
length=2,
|
||||
custom_emoji_id="5472409228461217725",
|
||||
)
|
||||
message = Message(
|
||||
1,
|
||||
from_user=self.from_user,
|
||||
date=self.date,
|
||||
chat=self.chat,
|
||||
caption=caption,
|
||||
caption_entities=[emoji_entity],
|
||||
)
|
||||
assert expected == message[type_]
|
||||
|
||||
async def test_parse_entities_url_emoji(self):
|
||||
url = b"http://github.com/?unicode=\\u2713\\U0001f469".decode("unicode-escape")
|
||||
text = "some url"
|
||||
|
||||
@@ -24,7 +24,7 @@ Currently this only means that cryptography is not installed.
|
||||
Because imports in pytest are intricate, we just run
|
||||
pytest -k test_no_passport.py
|
||||
|
||||
with the TEST_NO_PASSPORT environment variable set in addition to the regular test suite.
|
||||
with the TEST_PASSPORT environment variable set to False in addition to the regular test suite.
|
||||
Because actually uninstalling the optional dependencies would lead to errors in the test suite we
|
||||
just mock the import to raise the expected exception.
|
||||
|
||||
@@ -41,9 +41,9 @@ from telegram import _bot as bot
|
||||
from telegram._passport import credentials as credentials
|
||||
from tests.conftest import env_var_2_bool
|
||||
|
||||
TEST_NO_PASSPORT = env_var_2_bool(os.getenv("TEST_NO_PASSPORT", False))
|
||||
TEST_PASSPORT = env_var_2_bool(os.getenv("TEST_PASSPORT", True))
|
||||
|
||||
if TEST_NO_PASSPORT:
|
||||
if not TEST_PASSPORT:
|
||||
orig_import = __import__
|
||||
|
||||
def import_mock(module_name, *args, **kwargs):
|
||||
@@ -58,24 +58,24 @@ if TEST_NO_PASSPORT:
|
||||
|
||||
class TestNoPassport:
|
||||
"""
|
||||
The monkeypatches simulate cryptography not being installed even when TEST_NO_PASSPORT is
|
||||
False, though that doesn't test the actual imports
|
||||
The monkeypatches simulate cryptography not being installed even when TEST_PASSPORT is
|
||||
True, though that doesn't test the actual imports
|
||||
"""
|
||||
|
||||
def test_bot_init(self, bot_info, monkeypatch):
|
||||
if not TEST_NO_PASSPORT:
|
||||
if TEST_PASSPORT:
|
||||
monkeypatch.setattr(bot, "CRYPTO_INSTALLED", False)
|
||||
with pytest.raises(RuntimeError, match="passport"):
|
||||
bot.Bot(bot_info["token"], private_key=1, private_key_password=2)
|
||||
|
||||
def test_credentials_decrypt(self, monkeypatch):
|
||||
if not TEST_NO_PASSPORT:
|
||||
if TEST_PASSPORT:
|
||||
monkeypatch.setattr(credentials, "CRYPTO_INSTALLED", False)
|
||||
with pytest.raises(RuntimeError, match="passport"):
|
||||
credentials.decrypt(1, 1, 1)
|
||||
|
||||
def test_encrypted_credentials_decrypted_secret(self, monkeypatch):
|
||||
if not TEST_NO_PASSPORT:
|
||||
if TEST_PASSPORT:
|
||||
monkeypatch.setattr(credentials, "CRYPTO_INSTALLED", False)
|
||||
ec = credentials.EncryptedCredentials("data", "hash", "secret")
|
||||
with pytest.raises(RuntimeError, match="passport"):
|
||||
|
||||
+9
-1
@@ -468,7 +468,15 @@ class TestPhoto:
|
||||
b = PhotoSize("", photo.file_unique_id, self.width, self.height)
|
||||
c = PhotoSize(photo.file_id, photo.file_unique_id, 0, 0)
|
||||
d = PhotoSize("", "", self.width, self.height)
|
||||
e = Sticker(photo.file_id, photo.file_unique_id, self.width, self.height, False, False)
|
||||
e = Sticker(
|
||||
photo.file_id,
|
||||
photo.file_unique_id,
|
||||
self.width,
|
||||
self.height,
|
||||
False,
|
||||
False,
|
||||
Sticker.REGULAR,
|
||||
)
|
||||
|
||||
assert a == b
|
||||
assert hash(a) == hash(b)
|
||||
|
||||
@@ -0,0 +1,364 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# A library that provides a Python interface to the Telegram Bot API
|
||||
# Copyright (C) 2015-2022
|
||||
# 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/].
|
||||
|
||||
"""
|
||||
We mostly test on directly on AIORateLimiter here, b/c BaseRateLimiter doesn't contain anything
|
||||
notable
|
||||
"""
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
import platform
|
||||
import time
|
||||
from datetime import datetime
|
||||
from http import HTTPStatus
|
||||
|
||||
import pytest
|
||||
from flaky import flaky
|
||||
|
||||
from telegram import BotCommand, Chat, Message, User
|
||||
from telegram.constants import ParseMode
|
||||
from telegram.error import RetryAfter
|
||||
from telegram.ext import AIORateLimiter, BaseRateLimiter, Defaults, ExtBot
|
||||
from telegram.request import BaseRequest, RequestData
|
||||
from tests.conftest import env_var_2_bool
|
||||
|
||||
TEST_RATE_LIMITER = env_var_2_bool(os.getenv("TEST_RATE_LIMITER", True))
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
TEST_RATE_LIMITER, reason="Only relevant if the optional dependency is not installed"
|
||||
)
|
||||
class TestNoRateLimiter:
|
||||
def test_init(self):
|
||||
with pytest.raises(RuntimeError, match=r"python-telegram-bot\[rate-limiter\]"):
|
||||
AIORateLimiter()
|
||||
|
||||
|
||||
class TestBaseRateLimiter:
|
||||
rl_received = None
|
||||
request_received = None
|
||||
|
||||
async def test_no_rate_limiter(self, bot):
|
||||
with pytest.raises(ValueError, match="if a `ExtBot.rate_limiter` is set"):
|
||||
await bot.send_message(chat_id=42, text="test", rate_limit_args="something")
|
||||
|
||||
async def test_argument_passing(self, bot_info, monkeypatch, bot):
|
||||
class TestRateLimiter(BaseRateLimiter):
|
||||
async def initialize(self) -> None:
|
||||
pass
|
||||
|
||||
async def shutdown(self) -> None:
|
||||
pass
|
||||
|
||||
async def process_request(
|
||||
self,
|
||||
callback,
|
||||
args,
|
||||
kwargs,
|
||||
endpoint,
|
||||
data,
|
||||
rate_limit_args,
|
||||
):
|
||||
if TestBaseRateLimiter.rl_received is None:
|
||||
TestBaseRateLimiter.rl_received = []
|
||||
TestBaseRateLimiter.rl_received.append((endpoint, data, rate_limit_args))
|
||||
return await callback(*args, **kwargs)
|
||||
|
||||
class TestRequest(BaseRequest):
|
||||
async def initialize(self) -> None:
|
||||
pass
|
||||
|
||||
async def shutdown(self) -> None:
|
||||
pass
|
||||
|
||||
async def do_request(self, *args, **kwargs):
|
||||
if TestBaseRateLimiter.request_received is None:
|
||||
TestBaseRateLimiter.request_received = []
|
||||
TestBaseRateLimiter.request_received.append((args, kwargs))
|
||||
# return bot.bot.to_dict() for the `get_me` call in `Bot.initialize`
|
||||
return 200, json.dumps({"ok": True, "result": bot.bot.to_dict()}).encode()
|
||||
|
||||
defaults = Defaults(parse_mode=ParseMode.HTML)
|
||||
test_request = TestRequest()
|
||||
standard_bot = ExtBot(token=bot.token, defaults=defaults, request=test_request)
|
||||
rl_bot = ExtBot(
|
||||
token=bot.token,
|
||||
defaults=defaults,
|
||||
request=test_request,
|
||||
rate_limiter=TestRateLimiter(),
|
||||
)
|
||||
|
||||
async with standard_bot:
|
||||
await standard_bot.set_my_commands(
|
||||
commands=[BotCommand("test", "test")],
|
||||
language_code="en",
|
||||
api_kwargs={"api": "kwargs"},
|
||||
)
|
||||
async with rl_bot:
|
||||
await rl_bot.set_my_commands(
|
||||
commands=[BotCommand("test", "test")],
|
||||
language_code="en",
|
||||
rate_limit_args=(43, "test-1"),
|
||||
api_kwargs={"api": "kwargs"},
|
||||
)
|
||||
|
||||
assert len(self.rl_received) == 2
|
||||
assert self.rl_received[0] == ("getMe", {}, None)
|
||||
assert self.rl_received[1] == (
|
||||
"setMyCommands",
|
||||
dict(commands=[BotCommand("test", "test")], language_code="en", api="kwargs"),
|
||||
(43, "test-1"),
|
||||
)
|
||||
assert len(self.request_received) == 4
|
||||
# self.request_received[i] = i-th received request
|
||||
# self.request_received[i][0] = i-th received request's args
|
||||
# self.request_received[i][1] = i-th received request's kwargs
|
||||
assert self.request_received[0][1]["url"].endswith("getMe")
|
||||
assert self.request_received[2][1]["url"].endswith("getMe")
|
||||
assert self.request_received[1][0] == self.request_received[3][0]
|
||||
assert self.request_received[1][1].keys() == self.request_received[3][1].keys()
|
||||
for key, value in self.request_received[1][1].items():
|
||||
if isinstance(value, RequestData):
|
||||
assert value.parameters == self.request_received[3][1][key].parameters
|
||||
assert value.parameters["api"] == "kwargs"
|
||||
else:
|
||||
assert value == self.request_received[3][1][key]
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not TEST_RATE_LIMITER, reason="Only relevant if the optional dependency is installed"
|
||||
)
|
||||
@pytest.mark.skipif(
|
||||
os.getenv("GITHUB_ACTIONS", False) and platform.system() == "Darwin",
|
||||
reason="The timings are apparently rather inaccurate on MacOS.",
|
||||
)
|
||||
@flaky(10, 1) # Timings aren't quite perfect
|
||||
class TestAIORateLimiter:
|
||||
count = 0
|
||||
call_times = []
|
||||
|
||||
class CountRequest(BaseRequest):
|
||||
def __init__(self, retry_after=None):
|
||||
self.retry_after = retry_after
|
||||
|
||||
async def initialize(self) -> None:
|
||||
pass
|
||||
|
||||
async def shutdown(self) -> None:
|
||||
pass
|
||||
|
||||
async def do_request(self, *args, **kwargs):
|
||||
TestAIORateLimiter.count += 1
|
||||
TestAIORateLimiter.call_times.append(time.time())
|
||||
if self.retry_after:
|
||||
raise RetryAfter(retry_after=1)
|
||||
|
||||
url = kwargs.get("url").lower()
|
||||
if url.endswith("getme"):
|
||||
return (
|
||||
HTTPStatus.OK,
|
||||
json.dumps(
|
||||
{"ok": True, "result": User(id=1, first_name="bot", is_bot=True).to_dict()}
|
||||
).encode(),
|
||||
)
|
||||
if url.endswith("sendmessage"):
|
||||
return (
|
||||
HTTPStatus.OK,
|
||||
json.dumps(
|
||||
{
|
||||
"ok": True,
|
||||
"result": Message(
|
||||
message_id=1, date=datetime.now(), chat=Chat(1, "chat")
|
||||
).to_dict(),
|
||||
}
|
||||
).encode(),
|
||||
)
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def reset(self):
|
||||
self.count = 0
|
||||
TestAIORateLimiter.count = 0
|
||||
self.call_times = []
|
||||
TestAIORateLimiter.call_times = []
|
||||
|
||||
@pytest.mark.parametrize("max_retries", [0, 1, 4])
|
||||
async def test_max_retries(self, bot, max_retries):
|
||||
|
||||
bot = ExtBot(
|
||||
token=bot.token,
|
||||
request=self.CountRequest(retry_after=1),
|
||||
rate_limiter=AIORateLimiter(
|
||||
max_retries=max_retries, overall_max_rate=0, group_max_rate=0
|
||||
),
|
||||
)
|
||||
with pytest.raises(RetryAfter):
|
||||
await bot.get_me()
|
||||
|
||||
# Check that we retried the request the correct number of times
|
||||
assert TestAIORateLimiter.count == max_retries + 1
|
||||
|
||||
# Check that the retries were delayed correctly
|
||||
times = TestAIORateLimiter.call_times
|
||||
if len(times) <= 1:
|
||||
return
|
||||
delays = [j - i for i, j in zip(times[:-1], times[1:])]
|
||||
assert delays == pytest.approx([1.1 for _ in range(max_retries)], rel=0.05)
|
||||
|
||||
async def test_delay_all_pending_on_retry(self, bot):
|
||||
# Makes sure that a RetryAfter blocks *all* pending requests
|
||||
bot = ExtBot(
|
||||
token=bot.token,
|
||||
request=self.CountRequest(retry_after=1),
|
||||
rate_limiter=AIORateLimiter(max_retries=1, overall_max_rate=0, group_max_rate=0),
|
||||
)
|
||||
task_1 = asyncio.create_task(bot.get_me())
|
||||
await asyncio.sleep(0.1)
|
||||
task_2 = asyncio.create_task(bot.get_me())
|
||||
|
||||
assert not task_1.done()
|
||||
assert not task_2.done()
|
||||
|
||||
await asyncio.sleep(1.1)
|
||||
assert isinstance(task_1.exception(), RetryAfter)
|
||||
assert not task_2.done()
|
||||
|
||||
await asyncio.sleep(1.1)
|
||||
assert isinstance(task_2.exception(), RetryAfter)
|
||||
|
||||
@pytest.mark.parametrize("group_id", [-1, "-1", "@username"])
|
||||
@pytest.mark.parametrize("chat_id", [1, "1"])
|
||||
async def test_basic_rate_limiting(self, bot, group_id, chat_id):
|
||||
try:
|
||||
rl_bot = ExtBot(
|
||||
token=bot.token,
|
||||
request=self.CountRequest(retry_after=None),
|
||||
rate_limiter=AIORateLimiter(
|
||||
overall_max_rate=1,
|
||||
overall_time_period=1 / 4,
|
||||
group_max_rate=1,
|
||||
group_time_period=1 / 2,
|
||||
),
|
||||
)
|
||||
|
||||
async with rl_bot:
|
||||
non_group_tasks = {}
|
||||
group_tasks = {}
|
||||
for i in range(4):
|
||||
group_tasks[i] = asyncio.create_task(
|
||||
rl_bot.send_message(chat_id=group_id, text="test")
|
||||
)
|
||||
for i in range(8):
|
||||
non_group_tasks[i] = asyncio.create_task(
|
||||
rl_bot.send_message(chat_id=chat_id, text="test")
|
||||
)
|
||||
|
||||
await asyncio.sleep(0.85)
|
||||
# We expect 5 requests:
|
||||
# 1: `get_me` from `async with rl_bot`
|
||||
# 2: `send_message` at time 0.00
|
||||
# 3: `send_message` at time 0.25
|
||||
# 4: `send_message` at time 0.50
|
||||
# 5: `send_message` at time 0.75
|
||||
assert TestAIORateLimiter.count == 5
|
||||
assert sum(1 for task in non_group_tasks.values() if task.done()) < 8
|
||||
assert sum(1 for task in group_tasks.values() if task.done()) < 4
|
||||
|
||||
# 3 seconds after start
|
||||
await asyncio.sleep(3.1 - 0.85)
|
||||
assert all(task.done() for task in non_group_tasks.values())
|
||||
assert all(task.done() for task in group_tasks.values())
|
||||
|
||||
finally:
|
||||
# cleanup
|
||||
await asyncio.gather(*non_group_tasks.values(), *group_tasks.values())
|
||||
TestAIORateLimiter.count = 0
|
||||
TestAIORateLimiter.call_times = []
|
||||
|
||||
async def test_rate_limiting_no_chat_id(self, bot):
|
||||
try:
|
||||
rl_bot = ExtBot(
|
||||
token=bot.token,
|
||||
request=self.CountRequest(retry_after=None),
|
||||
rate_limiter=AIORateLimiter(
|
||||
overall_max_rate=1,
|
||||
overall_time_period=1 / 2,
|
||||
),
|
||||
)
|
||||
|
||||
async with rl_bot:
|
||||
non_chat_tasks = {}
|
||||
chat_tasks = {}
|
||||
for i in range(4):
|
||||
chat_tasks[i] = asyncio.create_task(
|
||||
rl_bot.send_message(chat_id=-1, text="test")
|
||||
)
|
||||
for i in range(8):
|
||||
non_chat_tasks[i] = asyncio.create_task(rl_bot.get_me())
|
||||
|
||||
await asyncio.sleep(0.6)
|
||||
# We expect 11 requests:
|
||||
# 1: `get_me` from `async with rl_bot`
|
||||
# 2: `send_message` at time 0.00
|
||||
# 3: `send_message` at time 0.05
|
||||
# 4: 8 times `get_me`
|
||||
assert TestAIORateLimiter.count == 11
|
||||
assert sum(1 for task in non_chat_tasks.values() if task.done()) == 8
|
||||
assert sum(1 for task in chat_tasks.values() if task.done()) == 2
|
||||
|
||||
# 1.6 seconds after start
|
||||
await asyncio.sleep(1.6 - 0.6)
|
||||
assert all(task.done() for task in non_chat_tasks.values())
|
||||
assert all(task.done() for task in chat_tasks.values())
|
||||
finally:
|
||||
# cleanup
|
||||
await asyncio.gather(*non_chat_tasks.values(), *chat_tasks.values())
|
||||
TestAIORateLimiter.count = 0
|
||||
TestAIORateLimiter.call_times = []
|
||||
|
||||
@pytest.mark.parametrize("intermediate", [True, False])
|
||||
async def test_group_caching(self, bot, intermediate):
|
||||
try:
|
||||
max_rate = 1000
|
||||
rl_bot = ExtBot(
|
||||
token=bot.token,
|
||||
request=self.CountRequest(retry_after=None),
|
||||
rate_limiter=AIORateLimiter(
|
||||
overall_max_rate=max_rate,
|
||||
overall_time_period=1,
|
||||
group_max_rate=max_rate,
|
||||
group_time_period=1,
|
||||
),
|
||||
)
|
||||
|
||||
# Unfortunately, there is no reliable way to test this without checking the internals
|
||||
assert len(rl_bot.rate_limiter._group_limiters) == 0
|
||||
await asyncio.gather(
|
||||
*(rl_bot.send_message(chat_id=-(i + 1), text=f"{i}") for i in range(513))
|
||||
)
|
||||
if intermediate:
|
||||
await rl_bot.send_message(chat_id=-1, text="999")
|
||||
assert 1 <= len(rl_bot.rate_limiter._group_limiters) <= 513
|
||||
else:
|
||||
await asyncio.sleep(1)
|
||||
await rl_bot.send_message(chat_id=-1, text="999")
|
||||
assert len(rl_bot.rate_limiter._group_limiters) == 1
|
||||
finally:
|
||||
TestAIORateLimiter.count = 0
|
||||
TestAIORateLimiter.call_times = []
|
||||
@@ -240,7 +240,7 @@ class TestRequest:
|
||||
)
|
||||
|
||||
with pytest.raises(exception_class, match="Test Message"):
|
||||
await httpx_request.post(None, None, None)
|
||||
await httpx_request.post("", None, None)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
["exception", "catch_class", "match"],
|
||||
|
||||
+113
-14
@@ -87,6 +87,8 @@ class TestSticker:
|
||||
thumb_width = 319
|
||||
thumb_height = 320
|
||||
thumb_file_size = 21472
|
||||
type = Sticker.REGULAR
|
||||
custom_emoji_id = "ThisIsSuchACustomEmojiID"
|
||||
|
||||
sticker_file_id = "5a3128a4d2a04750b5b58397f3b5e812"
|
||||
sticker_file_unique_id = "adc3145fd2e84d95b64d68eaa22aa33e"
|
||||
@@ -120,6 +122,7 @@ class TestSticker:
|
||||
assert sticker.thumb.width == self.thumb_width
|
||||
assert sticker.thumb.height == self.thumb_height
|
||||
assert sticker.thumb.file_size == self.thumb_file_size
|
||||
assert sticker.type == self.type
|
||||
# we need to be a premium TG user to send a premium sticker, so the below is not tested
|
||||
# assert sticker.premium_animation == self.premium_animation
|
||||
|
||||
@@ -139,6 +142,8 @@ class TestSticker:
|
||||
assert message.sticker.is_animated == sticker.is_animated
|
||||
assert message.sticker.is_video == sticker.is_video
|
||||
assert message.sticker.file_size == sticker.file_size
|
||||
assert message.sticker.type == sticker.type
|
||||
assert message.has_protected_content
|
||||
# we need to be a premium TG user to send a premium sticker, so the below is not tested
|
||||
# assert message.sticker.premium_animation == sticker.premium_animation
|
||||
|
||||
@@ -150,7 +155,6 @@ class TestSticker:
|
||||
assert message.sticker.thumb.width == sticker.thumb.width
|
||||
assert message.sticker.thumb.height == sticker.thumb.height
|
||||
assert message.sticker.thumb.file_size == sticker.thumb.file_size
|
||||
assert message.has_protected_content
|
||||
|
||||
@flaky(3, 1)
|
||||
async def test_get_and_download(self, bot, sticker):
|
||||
@@ -197,6 +201,7 @@ class TestSticker:
|
||||
assert message.sticker.is_animated == sticker.is_animated
|
||||
assert message.sticker.is_video == sticker.is_video
|
||||
assert message.sticker.file_size == sticker.file_size
|
||||
assert message.sticker.type == sticker.type
|
||||
|
||||
assert isinstance(message.sticker.thumb, PhotoSize)
|
||||
assert isinstance(message.sticker.thumb.file_id, str)
|
||||
@@ -219,6 +224,8 @@ class TestSticker:
|
||||
"emoji": self.emoji,
|
||||
"file_size": self.file_size,
|
||||
"premium_animation": self.premium_animation.to_dict(),
|
||||
"type": self.type,
|
||||
"custom_emoji_id": self.custom_emoji_id,
|
||||
}
|
||||
json_sticker = Sticker.de_json(json_dict, bot)
|
||||
|
||||
@@ -232,6 +239,8 @@ class TestSticker:
|
||||
assert json_sticker.file_size == self.file_size
|
||||
assert json_sticker.thumb == sticker.thumb
|
||||
assert json_sticker.premium_animation == self.premium_animation
|
||||
assert json_sticker.type == self.type
|
||||
assert json_sticker.custom_emoji_id == self.custom_emoji_id
|
||||
|
||||
async def test_send_with_sticker(self, monkeypatch, bot, chat_id, sticker):
|
||||
async def make_assertion(url, request_data: RequestData, *args, **kwargs):
|
||||
@@ -310,6 +319,7 @@ class TestSticker:
|
||||
assert sticker_dict["is_video"] == sticker.is_video
|
||||
assert sticker_dict["file_size"] == sticker.file_size
|
||||
assert sticker_dict["thumb"] == sticker.thumb.to_dict()
|
||||
assert sticker_dict["type"] == sticker.type
|
||||
|
||||
@flaky(3, 1)
|
||||
async def test_error_send_empty_file(self, bot, chat_id):
|
||||
@@ -343,6 +353,16 @@ class TestSticker:
|
||||
}
|
||||
assert premium_sticker.premium_animation.to_dict() == premium_sticker_dict
|
||||
|
||||
@flaky(3, 1)
|
||||
async def test_custom_emoji(self, bot):
|
||||
# testing custom emoji stickers is as much of an annoyance as the premium animation, see
|
||||
# in test_premium_animation
|
||||
custom_emoji_set = await bot.get_sticker_set("PTBStaticEmojiTestPack")
|
||||
# the first one to appear here is a sticker with unique file id of AQADjBsAAkKD0Uty
|
||||
# this could change in the future ofc.
|
||||
custom_emoji_sticker = custom_emoji_set.stickers[0]
|
||||
assert custom_emoji_sticker.custom_emoji_id == "6046140249875156202"
|
||||
|
||||
def test_equality(self, sticker):
|
||||
a = Sticker(
|
||||
sticker.file_id,
|
||||
@@ -351,14 +371,41 @@ class TestSticker:
|
||||
self.height,
|
||||
self.is_animated,
|
||||
self.is_video,
|
||||
self.type,
|
||||
)
|
||||
b = Sticker(
|
||||
"", sticker.file_unique_id, self.width, self.height, self.is_animated, self.is_video
|
||||
"",
|
||||
sticker.file_unique_id,
|
||||
self.width,
|
||||
self.height,
|
||||
self.is_animated,
|
||||
self.is_video,
|
||||
self.type,
|
||||
)
|
||||
c = Sticker(
|
||||
sticker.file_id,
|
||||
sticker.file_unique_id,
|
||||
0,
|
||||
0,
|
||||
False,
|
||||
True,
|
||||
self.type,
|
||||
)
|
||||
d = Sticker(
|
||||
"",
|
||||
"",
|
||||
self.width,
|
||||
self.height,
|
||||
self.is_animated,
|
||||
self.is_video,
|
||||
self.type,
|
||||
)
|
||||
c = Sticker(sticker.file_id, sticker.file_unique_id, 0, 0, False, True)
|
||||
d = Sticker("", "", self.width, self.height, self.is_animated, self.is_video)
|
||||
e = PhotoSize(
|
||||
sticker.file_id, sticker.file_unique_id, self.width, self.height, self.is_animated
|
||||
sticker.file_id,
|
||||
sticker.file_unique_id,
|
||||
self.width,
|
||||
self.height,
|
||||
self.is_animated,
|
||||
)
|
||||
|
||||
assert a == b
|
||||
@@ -427,9 +474,9 @@ class TestStickerSet:
|
||||
title = "Test stickers"
|
||||
is_animated = True
|
||||
is_video = True
|
||||
contains_masks = False
|
||||
stickers = [Sticker("file_id", "file_un_id", 512, 512, True, True)]
|
||||
stickers = [Sticker("file_id", "file_un_id", 512, 512, True, True, Sticker.REGULAR)]
|
||||
name = "NOTAREALNAME"
|
||||
sticker_type = Sticker.REGULAR
|
||||
|
||||
def test_de_json(self, bot, sticker):
|
||||
name = f"test_by_{bot.username}"
|
||||
@@ -438,9 +485,9 @@ class TestStickerSet:
|
||||
"title": self.title,
|
||||
"is_animated": self.is_animated,
|
||||
"is_video": self.is_video,
|
||||
"contains_masks": self.contains_masks,
|
||||
"stickers": [x.to_dict() for x in self.stickers],
|
||||
"thumb": sticker.thumb.to_dict(),
|
||||
"sticker_type": self.sticker_type,
|
||||
}
|
||||
sticker_set = StickerSet.de_json(json_dict, bot)
|
||||
|
||||
@@ -448,9 +495,9 @@ class TestStickerSet:
|
||||
assert sticker_set.title == self.title
|
||||
assert sticker_set.is_animated == self.is_animated
|
||||
assert sticker_set.is_video == self.is_video
|
||||
assert sticker_set.contains_masks == self.contains_masks
|
||||
assert sticker_set.stickers == self.stickers
|
||||
assert sticker_set.thumb == sticker.thumb
|
||||
assert sticker_set.sticker_type == self.sticker_type
|
||||
|
||||
async def test_create_sticker_set(
|
||||
self, bot, chat_id, sticker_file, animated_sticker_file, video_sticker_file
|
||||
@@ -536,8 +583,9 @@ class TestStickerSet:
|
||||
assert sticker_set_dict["title"] == sticker_set.title
|
||||
assert sticker_set_dict["is_animated"] == sticker_set.is_animated
|
||||
assert sticker_set_dict["is_video"] == sticker_set.is_video
|
||||
assert sticker_set_dict["contains_masks"] == sticker_set.contains_masks
|
||||
assert sticker_set_dict["stickers"][0] == sticker_set.stickers[0].to_dict()
|
||||
assert sticker_set_dict["thumb"] == sticker_set.thumb.to_dict()
|
||||
assert sticker_set_dict["sticker_type"] == sticker_set.sticker_type
|
||||
|
||||
@flaky(3, 1)
|
||||
async def test_bot_methods_2_png(self, bot, sticker_set):
|
||||
@@ -639,6 +687,32 @@ class TestStickerSet:
|
||||
assert test_flag
|
||||
monkeypatch.delattr(bot, "_post")
|
||||
|
||||
async def test_create_new_sticker_all_params(self, monkeypatch, bot, chat_id, mask_position):
|
||||
async def make_assertion(_, data, *args, **kwargs):
|
||||
assert data["user_id"] == chat_id
|
||||
assert data["name"] == "name"
|
||||
assert data["title"] == "title"
|
||||
assert data["emojis"] == "emoji"
|
||||
assert data["mask_position"] == mask_position
|
||||
assert data["png_sticker"] == "wow.png"
|
||||
assert data["tgs_sticker"] == "wow.tgs"
|
||||
assert data["webm_sticker"] == "wow.webm"
|
||||
assert data["sticker_type"] == Sticker.MASK
|
||||
|
||||
monkeypatch.setattr(bot, "_post", make_assertion)
|
||||
await bot.create_new_sticker_set(
|
||||
chat_id,
|
||||
"name",
|
||||
"title",
|
||||
"emoji",
|
||||
mask_position=mask_position,
|
||||
png_sticker="wow.png",
|
||||
tgs_sticker="wow.tgs",
|
||||
webm_sticker="wow.webm",
|
||||
sticker_type=Sticker.MASK,
|
||||
)
|
||||
monkeypatch.delattr(bot, "_post")
|
||||
|
||||
async def test_add_sticker_to_set_local_files(self, monkeypatch, bot, chat_id):
|
||||
# For just test that the correct paths are passed as we have no local bot API set up
|
||||
test_flag = False
|
||||
@@ -685,21 +759,26 @@ class TestStickerSet:
|
||||
self.name,
|
||||
self.title,
|
||||
self.is_animated,
|
||||
self.contains_masks,
|
||||
self.stickers,
|
||||
self.is_video,
|
||||
self.sticker_type,
|
||||
)
|
||||
b = StickerSet(
|
||||
self.name,
|
||||
self.title,
|
||||
self.is_animated,
|
||||
self.contains_masks,
|
||||
self.stickers,
|
||||
self.is_video,
|
||||
self.sticker_type,
|
||||
)
|
||||
c = StickerSet(self.name, None, None, None, None, None)
|
||||
c = StickerSet(self.name, None, None, None, None, Sticker.CUSTOM_EMOJI)
|
||||
d = StickerSet(
|
||||
"blah", self.title, self.is_animated, self.contains_masks, self.stickers, self.is_video
|
||||
"blah",
|
||||
self.title,
|
||||
self.is_animated,
|
||||
self.stickers,
|
||||
self.is_video,
|
||||
self.sticker_type,
|
||||
)
|
||||
e = Audio(self.name, "", 0, None, None)
|
||||
|
||||
@@ -775,3 +854,23 @@ class TestMaskPosition:
|
||||
|
||||
assert a != e
|
||||
assert hash(a) != hash(e)
|
||||
|
||||
|
||||
class TestGetCustomEmojiSticker:
|
||||
async def test_custom_emoji_sticker(self, bot):
|
||||
# we use the same ID as in test_custom_emoji
|
||||
emoji_sticker_list = await bot.get_custom_emoji_stickers(["6046140249875156202"])
|
||||
assert emoji_sticker_list[0].emoji == "😎"
|
||||
assert emoji_sticker_list[0].height == 100
|
||||
assert emoji_sticker_list[0].width == 100
|
||||
assert not emoji_sticker_list[0].is_animated
|
||||
assert not emoji_sticker_list[0].is_video
|
||||
assert emoji_sticker_list[0].set_name == "PTBStaticEmojiTestPack"
|
||||
assert emoji_sticker_list[0].type == Sticker.CUSTOM_EMOJI
|
||||
assert emoji_sticker_list[0].custom_emoji_id == "6046140249875156202"
|
||||
assert emoji_sticker_list[0].thumb.width == 100
|
||||
assert emoji_sticker_list[0].thumb.height == 100
|
||||
assert emoji_sticker_list[0].thumb.file_size == 3614
|
||||
assert emoji_sticker_list[0].thumb.file_unique_id == "AQAD6gwAAoY06FNy"
|
||||
assert emoji_sticker_list[0].file_size == 3678
|
||||
assert emoji_sticker_list[0].file_unique_id == "AgAD6gwAAoY06FM"
|
||||
|
||||
+1
-1
@@ -534,7 +534,7 @@ class TestUser:
|
||||
|
||||
assert user.mention_markdown() == expected.format(user.full_name, user.id)
|
||||
assert user.mention_markdown("the_name*\u2022") == expected.format(
|
||||
"the\\_name\\*\u2022", user.id
|
||||
"the_name*\u2022", user.id
|
||||
)
|
||||
assert user.mention_markdown(user.username) == expected.format(user.username, user.id)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user