mirror of
https://github.com/python-telegram-bot/python-telegram-bot.git
synced 2026-06-19 15:45:13 +00:00
Compare commits
9 Commits
v22.0
..
feature/fsm
| Author | SHA1 | Date | |
|---|---|---|---|
| b1fff6d90a | |||
| 31af1a9db8 | |||
| 4441543043 | |||
| 646ba37391 | |||
| 817b71d914 | |||
| 434cbfade8 | |||
| 34832d9db9 | |||
| 07225b9a02 | |||
| 0c06ba0a90 |
@@ -1,5 +0,0 @@
|
||||
<b>We've just released {tag}</b>.
|
||||
Thank you to everyone who contributed to this release.
|
||||
As usual, upgrade using <code>pip install -U python-telegram-bot</code>.
|
||||
|
||||
The release notes can be found <a href="https://docs.python-telegram-bot.org/en/{tag}/changelog.html">here</a>.
|
||||
@@ -1,30 +0,0 @@
|
||||
name: Create Chango Fragment
|
||||
on:
|
||||
pull_request:
|
||||
types:
|
||||
- opened
|
||||
- reopened
|
||||
- synchronize
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
create-chango-fragment:
|
||||
permissions:
|
||||
# Give the default GITHUB_TOKEN write permission to commit and push the
|
||||
# added or changed files to the repository.
|
||||
contents: write
|
||||
name: create-chango-fragment
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
# Create the new fragment
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: Bibo-Joshi/chango@9d6bd9d7612eca5fab2c5161687011be59baaf19 # v0.4.0
|
||||
with:
|
||||
github-token: ${{ secrets.CHANGO_PAT }}
|
||||
query-issue-types: true
|
||||
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
name: Test Admonitions Generation
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- telegram/**
|
||||
- docs/**
|
||||
- .github/workflows/docs-admonitions.yml
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
test-admonitions:
|
||||
name: Test Admonitions Generation
|
||||
runs-on: ${{matrix.os}}
|
||||
permissions:
|
||||
# for uploading artifacts
|
||||
actions: write
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ['3.12']
|
||||
os: [ubuntu-latest]
|
||||
fail-fast: False
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5.4.0
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
cache: 'pip'
|
||||
cache-dependency-path: '**/requirements*.txt'
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -W ignore -m pip install --upgrade pip
|
||||
python -W ignore -m pip install -r requirements-dev-all.txt
|
||||
- name: Test autogeneration of admonitions
|
||||
run: pytest -v --tb=short tests/docs/admonition_inserter.py
|
||||
@@ -1,41 +0,0 @@
|
||||
name: Check Links in Documentation
|
||||
on:
|
||||
schedule:
|
||||
# First day of month at 05:46 in every 2nd month
|
||||
- cron: '46 5 1 */2 *'
|
||||
pull_request:
|
||||
paths:
|
||||
- .github/workflows/docs-linkcheck.yml
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
test-sphinx-build:
|
||||
name: test-sphinx-linkcheck
|
||||
runs-on: ${{matrix.os}}
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ['3.12']
|
||||
os: [ubuntu-latest]
|
||||
fail-fast: False
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5.4.0
|
||||
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-dev-all.txt
|
||||
- name: Check Links
|
||||
run: sphinx-build docs/source docs/build/html -W --keep-going -j auto -b linkcheck
|
||||
- name: Upload linkcheck output
|
||||
# Run also if the previous steps failed
|
||||
if: always()
|
||||
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
|
||||
with:
|
||||
name: linkcheck-output
|
||||
path: docs/build/html/output.*
|
||||
@@ -21,13 +21,13 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Install the latest version of uv
|
||||
uses: astral-sh/setup-uv@f94ec6bedd8674c4426838e6b50417d36b6ab231 # v5.3.1
|
||||
uses: astral-sh/setup-uv@4db96194c378173c656ce18a155ffc14a9fc4355 # v5.2.2
|
||||
- name: Run zizmor
|
||||
run: uvx zizmor --persona=pedantic --format sarif . > results.sarif
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Upload SARIF file
|
||||
uses: github/codeql-action/upload-sarif@b56ba49b26e50535fa1e7f7db0f4f7b4bf65d80d # v3.28.10
|
||||
uses: github/codeql-action/upload-sarif@dd746615b3b9d728a6a37ca2045b68ca76d4841a # v3.28.8
|
||||
with:
|
||||
sarif_file: results.sarif
|
||||
category: zizmor
|
||||
@@ -13,7 +13,7 @@ jobs:
|
||||
pull-requests: write # for srvaroa/labeler to add labels in PR
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: srvaroa/labeler@0a20eccb8c94a1ee0bed5f16859aece1c45c3e55 # v1.13.0
|
||||
- uses: srvaroa/labeler@fe4b1c73bb8abf2f14a44a6912a8b4fee835d631 # v1.12.0
|
||||
# Config file at .github/labeler.yml
|
||||
env:
|
||||
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
|
||||
|
||||
@@ -60,7 +60,7 @@ jobs:
|
||||
name: python-package-distributions
|
||||
path: dist/
|
||||
- name: Publish to PyPI
|
||||
uses: pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc # v1.12.4
|
||||
uses: pypa/gh-action-pypi-publish@67339c736fd9354cd4f8cb0b744f2b82a74b5c70 # v1.12.3
|
||||
|
||||
compute-signatures:
|
||||
name: Compute SHA1 Sums and Sign with Sigstore
|
||||
@@ -119,14 +119,13 @@ jobs:
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
TAG: ${{ needs.build.outputs.TAG }}
|
||||
# Create a tag and a GitHub Release. The description is filled by the static template, we
|
||||
# just insert the correct tag in the template.
|
||||
# Create a tag and a GitHub Release. The description can be changed later, as for now
|
||||
# we don't define it through this workflow.
|
||||
run: >-
|
||||
sed "s/{tag}/$TAG/g" .github/workflows/assets/release_template.html |
|
||||
gh release create
|
||||
"$TAG"
|
||||
--repo '${{ github.repository }}'
|
||||
--notes-file -
|
||||
--generate-notes
|
||||
- name: Upload artifact signatures to GitHub Release
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
@@ -138,27 +137,3 @@ jobs:
|
||||
gh release upload
|
||||
"$TAG" dist/**
|
||||
--repo '${{ github.repository }}'
|
||||
|
||||
telegram-channel:
|
||||
name: Publish to Telegram Channel
|
||||
needs:
|
||||
- github-release
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
environment:
|
||||
name: release_pypi
|
||||
permissions: {}
|
||||
|
||||
steps:
|
||||
- name: Publish to Telegram Channel
|
||||
env:
|
||||
TAG: ${{ needs.build.outputs.TAG }}
|
||||
# This secret is configured only for the `pypi-release` branch
|
||||
BOT_TOKEN: ${{ secrets.CHANNEL_BOT_TOKEN }}
|
||||
run: >-
|
||||
sed "s/{tag}/$TAG/g" .github/workflows/assets/release_template.html |
|
||||
curl
|
||||
-X POST "https://api.telegram.org/bot$BOT_TOKEN/sendMessage"
|
||||
-d "chat_id=@pythontelegrambotchannel"
|
||||
-d "parse_mode=HTML"
|
||||
--data-urlencode "text@-"
|
||||
|
||||
@@ -60,7 +60,7 @@ jobs:
|
||||
name: python-package-distributions
|
||||
path: dist/
|
||||
- name: Publish to Test PyPI
|
||||
uses: pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc # v1.12.4
|
||||
uses: pypa/gh-action-pypi-publish@67339c736fd9354cd4f8cb0b744f2b82a74b5c70 # v1.12.3
|
||||
with:
|
||||
repository-url: https://test.pypi.org/legacy/
|
||||
|
||||
@@ -121,15 +121,14 @@ jobs:
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
TAG: ${{ needs.build.outputs.TAG }}
|
||||
# Create a tag and a GitHub Release *draft*. The description is filled by the static
|
||||
# template, we just insert the correct tag in the template.
|
||||
# Create a GitHub Release *draft*. The description can be changed later, as for now
|
||||
# we don't define it through this workflow.
|
||||
run: >-
|
||||
sed "s/{tag}/$TAG/g" .github/workflows/assets/release_template.html |
|
||||
gh release create
|
||||
"$TAG"
|
||||
--repo '${{ github.repository }}'
|
||||
--generate-notes
|
||||
--draft
|
||||
--notes-file -
|
||||
- name: Upload artifact signatures to GitHub Release
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
name: Bot API Tests
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- telegram/**
|
||||
- tests/**
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
schedule:
|
||||
# Run monday and friday morning at 03:07 - odd time to spread load on GitHub Actions
|
||||
- cron: '7 3 * * 1,5'
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
check-conformity:
|
||||
name: check-conformity
|
||||
runs-on: ${{matrix.os}}
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: [3.11]
|
||||
os: [ubuntu-latest]
|
||||
fail-fast: False
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5.4.0
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -W ignore -m pip install --upgrade pip
|
||||
python -W ignore -m pip install .[all]
|
||||
python -W ignore -m pip install -r requirements-unit-tests.txt
|
||||
- name: Compare to official api
|
||||
run: |
|
||||
pytest -v tests/test_official/test_official.py --junit-xml=.test_report_official.xml
|
||||
exit $?
|
||||
env:
|
||||
TEST_OFFICIAL: "true"
|
||||
shell: bash --noprofile --norc {0}
|
||||
|
||||
- name: Test Summary
|
||||
id: test_summary
|
||||
uses: test-summary/action@31493c76ec9e7aa675f1585d3ed6f1da69269a86 # v2.4
|
||||
if: always() # always run, even if tests fail
|
||||
with:
|
||||
paths: .test_report_official.xml
|
||||
@@ -1,23 +0,0 @@
|
||||
name: Check Type Completeness
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- telegram/**
|
||||
- pyproject.toml
|
||||
- .github/workflows/type_completeness.yml
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
test-type-completeness:
|
||||
name: test-type-completeness
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: Bibo-Joshi/pyright-type-completeness@c85a67ff3c66f51dcbb2d06bfcf4fe83a57d69cc # v1.0.1
|
||||
with:
|
||||
package-name: telegram
|
||||
python-version: 3.12
|
||||
pyright-version: ~=1.1.367
|
||||
File diff suppressed because one or more lines are too long
+1
-1
@@ -18,7 +18,7 @@ python:
|
||||
install:
|
||||
- method: pip
|
||||
path: .
|
||||
- requirements: requirements-dev-all.txt
|
||||
- requirements: docs/requirements-docs.txt
|
||||
|
||||
build:
|
||||
os: ubuntu-22.04
|
||||
|
||||
@@ -1,62 +1,8 @@
|
||||
Version 21.11.1
|
||||
===============
|
||||
.. _ptb-changelog:
|
||||
|
||||
*Released 2025-03-01*
|
||||
|
||||
This is the technical changelog for version 21.11. More elaborate release notes can be found in the news channel `@pythontelegrambotchannel <https://t.me/pythontelegrambotchannel>`_.
|
||||
|
||||
Documentation Improvements
|
||||
--------------------------
|
||||
|
||||
- Fix ReadTheDocs Build (:pr:`4695`)
|
||||
|
||||
Version 21.11
|
||||
=============
|
||||
|
||||
*Released 2025-03-01*
|
||||
|
||||
This is the technical changelog for version 21.11. More elaborate release notes can be found in the news channel `@pythontelegrambotchannel <https://t.me/pythontelegrambotchannel>`_.
|
||||
|
||||
Major Changes and New Features
|
||||
------------------------------
|
||||
|
||||
- Full Support for Bot API 8.3 (:pr:`4676` closes :issue:`4677`, :pr:`4682` by `aelkheir <https://github.com/aelkheir>`_, :pr:`4690` by `aelkheir <https://github.com/aelkheir>`_, :pr:`4691` by `aelkheir <https://github.com/aelkheir>`_)
|
||||
- Make ``provider_token`` Argument Optional (:pr:`4689`)
|
||||
- Remove Deprecated ``InlineQueryResultArticle.hide_url`` (:pr:`4640` closes :issue:`4638`)
|
||||
- Accept ``datetime.timedelta`` Input in ``Bot`` Method Parameters (:pr:`4651`)
|
||||
- Extend Customization Support for ``Bot.base_(file_)url`` (:pr:`4632` closes :issue:`3355`)
|
||||
- Support ``allow_paid_broadcast`` in ``AIORateLimiter`` (:pr:`4627` closes :issue:`4578`)
|
||||
- Add ``BaseUpdateProcessor.current_concurrent_updates`` (:pr:`4626` closes :issue:`3984`)
|
||||
|
||||
Minor Changes and Bug Fixes
|
||||
---------------------------
|
||||
|
||||
- Add Bootstrapping Logic to ``Application.run_*`` (:pr:`4673` closes :issue:`4657`)
|
||||
- Fix a Bug in ``edit_user_star_subscription`` (:pr:`4681` by `vavasik800 <https://github.com/vavasik800>`_)
|
||||
- Simplify Handling of Empty Data in ``TelegramObject.de_json`` and Friends (:pr:`4617` closes :issue:`4614`)
|
||||
|
||||
Documentation Improvements
|
||||
--------------------------
|
||||
|
||||
- Documentation Improvements (:pr:`4641`)
|
||||
- Overhaul Admonition Insertion in Documentation (:pr:`4462` closes :issue:`4414`)
|
||||
|
||||
Internal Changes
|
||||
----------------
|
||||
|
||||
- Stabilize Linkcheck Test (:pr:`4693`)
|
||||
- Bump ``pre-commit`` Hooks to Latest Versions (:pr:`4643`)
|
||||
- Refactor Tests for ``TelegramObject`` Classes with Subclasses (:pr:`4654` closes :issue:`4652`)
|
||||
- Use Fine Grained Permissions for GitHub Actions Workflows (:pr:`4668`)
|
||||
|
||||
Dependency Updates
|
||||
------------------
|
||||
|
||||
- Bump ``actions/setup-python`` from 5.3.0 to 5.4.0 (:pr:`4665`)
|
||||
- Bump ``dependabot/fetch-metadata`` from 2.2.0 to 2.3.0 (:pr:`4666`)
|
||||
- Bump ``actions/stale`` from 9.0.0 to 9.1.0 (:pr:`4667`)
|
||||
- Bump ``astral-sh/setup-uv`` from 5.1.0 to 5.2.2 (:pr:`4664`)
|
||||
- Bump ``codecov/test-results-action`` from 1.0.1 to 1.0.2 (:pr:`4663`)
|
||||
=========
|
||||
Changelog
|
||||
=========
|
||||
|
||||
Version 21.10
|
||||
=============
|
||||
+2
-2
@@ -11,7 +11,7 @@
|
||||
:target: https://pypi.org/project/python-telegram-bot/
|
||||
:alt: Supported Python versions
|
||||
|
||||
.. image:: https://img.shields.io/badge/Bot%20API-8.3-blue?logo=telegram
|
||||
.. image:: https://img.shields.io/badge/Bot%20API-8.2-blue?logo=telegram
|
||||
:target: https://core.telegram.org/bots/api-changelog
|
||||
:alt: Supported Bot API version
|
||||
|
||||
@@ -81,7 +81,7 @@ After installing_ the library, be sure to check out the section on `working with
|
||||
Telegram API support
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
All types and methods of the Telegram Bot API **8.3** are natively supported by this library.
|
||||
All types and methods of the Telegram Bot API **8.2** are natively supported by this library.
|
||||
In addition, Bot API functionality not yet natively included can still be used as described `in our wiki <https://github.com/python-telegram-bot/python-telegram-bot/wiki/Bot-API-Forward-Compatibility>`_.
|
||||
|
||||
Notable Features
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
breaking = """This release removes all functionality that was deprecated in v20.x. This is in line with our :ref:`stability policy <stability-policy>`.
|
||||
This includes the following changes:
|
||||
|
||||
- Removed ``filters.CHAT`` (all messages have an associated chat) and ``filters.StatusUpdate.USER_SHARED`` (use ``filters.StatusUpdate.USERS_SHARED`` instead).
|
||||
- Removed ``Defaults.disable_web_page_preview`` and ``Defaults.quote``. Use ``Defaults.link_preview_options`` and ``Defaults.do_quote`` instead.
|
||||
- Removed ``ApplicationBuilder.(get_updates_)proxy_url`` and ``HTTPXRequest.proxy_url``. Use ``ApplicationBuilder.(get_updates_)proxy`` and ``HTTPXRequest.proxy`` instead.
|
||||
- Removed the ``*_timeout`` arguments of ``Application.run_polling`` and ``Updater.start_webhook``. Instead, specify the values via ``ApplicationBuilder.get_updates_*_timeout``.
|
||||
- Removed ``constants.InlineQueryLimit.MIN_SWITCH_PM_TEXT_LENGTH``. Use ``constants.InlineQueryResultsButtonLimit.MAX_START_PARAMETER_LENGTH`` instead.
|
||||
- Removed the argument ``quote`` of ``Message.reply_*``. Use ``do_quote`` instead.
|
||||
- Removed the superfluous ``EncryptedPassportElement.credentials`` without replacement.
|
||||
- Changed attribute value of ``PassportFile.file_date`` from :obj:`int` to :class:`datetime.datetime`. Make sure to adjust your code accordingly.
|
||||
- Changed the attribute value of ``PassportElementErrors.file_hashes`` from :obj:`list` to :obj:`tuple`. Make sure to adjust your code accordingly.
|
||||
- Make ``BaseRequest.read_timeout`` an abstract property. If you subclass ``BaseRequest``, you need to implement this property.
|
||||
- The default value for ``write_timeout`` now defaults to ``DEFAULT_NONE`` also for bot methods that send media. Previously, it was ``20``. If you subclass ``BaseRequest``, make sure to use your desired write timeout if ``RequestData.multipart_data`` is set.
|
||||
"""
|
||||
[[pull_requests]]
|
||||
uid = "4671"
|
||||
author_uid = "Bibo-Joshi"
|
||||
closes_threads = ["4659"]
|
||||
@@ -1,5 +0,0 @@
|
||||
documentation = "Add `chango <https://chango.readthedocs.io/stable/>`_ As Changelog Management Tool"
|
||||
[[pull_requests]]
|
||||
uid = "4672"
|
||||
author_uid = "Bibo-Joshi"
|
||||
closes_threads = ["4321"]
|
||||
@@ -1,5 +0,0 @@
|
||||
internal = "Bump github/codeql-action from 3.28.8 to 3.28.10"
|
||||
[[pull_requests]]
|
||||
uid = "4697"
|
||||
author_uid = "dependabot[bot]"
|
||||
closes_threads = []
|
||||
@@ -1,5 +0,0 @@
|
||||
internal = "Bump srvaroa/labeler from 1.12.0 to 1.13.0"
|
||||
[[pull_requests]]
|
||||
uid = "4698"
|
||||
author_uid = "dependabot[bot]"
|
||||
closes_threads = []
|
||||
@@ -1,5 +0,0 @@
|
||||
internal = "Bump astral-sh/setup-uv from 5.2.2 to 5.3.1"
|
||||
[[pull_requests]]
|
||||
uid = "4699"
|
||||
author_uid = "dependabot[bot]"
|
||||
closes_threads = []
|
||||
@@ -1,5 +0,0 @@
|
||||
internal = "Bump Bibo-Joshi/chango from 0.3.1 to 0.3.2"
|
||||
[[pull_requests]]
|
||||
uid = "4700"
|
||||
author_uid = "dependabot[bot]"
|
||||
closes_threads = []
|
||||
@@ -1,5 +0,0 @@
|
||||
internal = "Bump pypa/gh-action-pypi-publish from 1.12.3 to 1.12.4"
|
||||
[[pull_requests]]
|
||||
uid = "4701"
|
||||
author_uid = "dependabot[bot]"
|
||||
closes_threads = []
|
||||
@@ -1,5 +0,0 @@
|
||||
internal = "Bump pytest from 8.3.4 to 8.3.5"
|
||||
[[pull_requests]]
|
||||
uid = "4709"
|
||||
author_uid = "dependabot[bot]"
|
||||
closes_threads = []
|
||||
@@ -1,5 +0,0 @@
|
||||
internal = "Bump sphinx from 8.1.3 to 8.2.3"
|
||||
[[pull_requests]]
|
||||
uid = "4710"
|
||||
author_uid = "dependabot[bot]"
|
||||
closes_threads = []
|
||||
@@ -1,5 +0,0 @@
|
||||
internal = "Bump Bibo-Joshi/chango from 0.3.2 to 0.4.0"
|
||||
[[pull_requests]]
|
||||
uid = "4712"
|
||||
author_uid = "Bibo-Joshi"
|
||||
closes_threads = []
|
||||
@@ -1,5 +0,0 @@
|
||||
internal = "Bump Version to v22.0"
|
||||
[[pull_requests]]
|
||||
uid = "4719"
|
||||
author_uid = "Bibo-Joshi"
|
||||
closes_threads = []
|
||||
@@ -1,78 +0,0 @@
|
||||
# noqa: INP001
|
||||
# pylint: disable=import-error
|
||||
"""Configuration for the chango changelog tool"""
|
||||
|
||||
from collections.abc import Collection
|
||||
from typing import Optional
|
||||
|
||||
from chango.concrete import DirectoryChanGo, DirectoryVersionScanner, HeaderVersionHistory
|
||||
from chango.concrete.sections import GitHubSectionChangeNote, Section, SectionVersionNote
|
||||
|
||||
version_scanner = DirectoryVersionScanner(base_directory=".", unreleased_directory="unreleased")
|
||||
|
||||
|
||||
class ChangoSectionChangeNote(
|
||||
GitHubSectionChangeNote.with_sections( # type: ignore[misc]
|
||||
[
|
||||
Section(uid="highlights", title="Highlights", sort_order=0),
|
||||
Section(uid="breaking", title="Breaking Changes", sort_order=1),
|
||||
Section(uid="security", title="Security Changes", sort_order=2),
|
||||
Section(uid="deprecations", title="Deprecations", sort_order=3),
|
||||
Section(uid="features", title="New Features", sort_order=4),
|
||||
Section(uid="bugfixes", title="Bug Fixes", sort_order=5),
|
||||
Section(uid="dependencies", title="Dependencies", sort_order=6),
|
||||
Section(uid="other", title="Other Changes", sort_order=7),
|
||||
Section(uid="documentation", title="Documentation", sort_order=8),
|
||||
Section(uid="internal", title="Internal Changes", sort_order=9),
|
||||
]
|
||||
)
|
||||
):
|
||||
"""Custom change note type for PTB. Mainly overrides get_sections to map labels to sections"""
|
||||
|
||||
OWNER = "python-telegram-bot"
|
||||
REPOSITORY = "python-telegram-bot"
|
||||
|
||||
@classmethod
|
||||
def get_sections(
|
||||
cls,
|
||||
labels: Collection[str],
|
||||
issue_types: Optional[Collection[str]],
|
||||
) -> set[str]:
|
||||
"""Override get_sections to have customized auto-detection of relevant sections based on
|
||||
the pull request and linked issues. Certainly not perfect in all cases, but should be a
|
||||
good start for most PRs.
|
||||
"""
|
||||
combined_labels = set(labels) | (set(issue_types or []))
|
||||
|
||||
mapping = {
|
||||
"🐛 bug": "bugfixes",
|
||||
"💡 feature": "features",
|
||||
"🧹 chore": "internal",
|
||||
"⚙️ bot-api": "features",
|
||||
"⚙️ documentation": "documentation",
|
||||
"⚙️ tests": "internal",
|
||||
"⚙️ ci-cd": "internal",
|
||||
"⚙️ security": "security",
|
||||
"⚙️ examples": "documentation",
|
||||
"⚙️ type-hinting": "other",
|
||||
"🛠 refactor": "internal",
|
||||
"🛠 breaking": "breaking",
|
||||
"⚙️ dependencies": "dependencies",
|
||||
"🔗 github-actions": "internal",
|
||||
"🛠 code-quality": "internal",
|
||||
}
|
||||
|
||||
# we want to return *all* from the mapping that are in the combined_labels
|
||||
# removing superfluous sections from the fragment is a tad easier than adding them
|
||||
found = {section for label, section in mapping.items() if label in combined_labels}
|
||||
|
||||
# if we have not found any sections, we default to "other"
|
||||
return found or {"other"}
|
||||
|
||||
|
||||
chango_instance = DirectoryChanGo(
|
||||
change_note_type=ChangoSectionChangeNote,
|
||||
version_note_type=SectionVersionNote,
|
||||
version_history_type=HeaderVersionHistory,
|
||||
scanner=version_scanner,
|
||||
)
|
||||
@@ -47,29 +47,29 @@ keyword_args = [
|
||||
"",
|
||||
]
|
||||
|
||||
media_write_timeout_change_methods = [
|
||||
"add_sticker_to_set",
|
||||
"create_new_sticker_set",
|
||||
"send_animation",
|
||||
media_write_timeout_deprecation_methods = [
|
||||
"send_photo",
|
||||
"send_audio",
|
||||
"send_document",
|
||||
"send_media_group",
|
||||
"send_photo",
|
||||
"send_sticker",
|
||||
"send_video",
|
||||
"send_video_note",
|
||||
"send_animation",
|
||||
"send_voice",
|
||||
"send_media_group",
|
||||
"set_chat_photo",
|
||||
"upload_sticker_file",
|
||||
"add_sticker_to_set",
|
||||
"create_new_sticker_set",
|
||||
]
|
||||
media_write_timeout_change = [
|
||||
media_write_timeout_deprecation = [
|
||||
" write_timeout (:obj:`float` | :obj:`None`, optional): Value to pass to "
|
||||
" :paramref:`telegram.request.BaseRequest.post.write_timeout`. By default, ``20`` "
|
||||
" seconds are used as write timeout."
|
||||
"",
|
||||
"",
|
||||
" .. versionchanged:: 22.0",
|
||||
" The default value changed to "
|
||||
" .. deprecated:: 20.7",
|
||||
" In future versions, the default value will be changed to "
|
||||
" :attr:`~telegram.request.BaseRequest.DEFAULT_NONE`.",
|
||||
"",
|
||||
"",
|
||||
|
||||
@@ -32,8 +32,8 @@ from docs.auxil.kwargs_insertion import (
|
||||
find_insert_pos_for_kwargs,
|
||||
get_updates_read_timeout_addition,
|
||||
keyword_args,
|
||||
media_write_timeout_change,
|
||||
media_write_timeout_change_methods,
|
||||
media_write_timeout_deprecation,
|
||||
media_write_timeout_deprecation_methods,
|
||||
)
|
||||
from docs.auxil.link_code import LINE_NUMBERS
|
||||
|
||||
@@ -116,9 +116,9 @@ def autodoc_process_docstring(
|
||||
|
||||
if (
|
||||
"post.write_timeout`. Defaults to" in to_insert
|
||||
and method_name in media_write_timeout_change_methods
|
||||
and method_name in media_write_timeout_deprecation_methods
|
||||
):
|
||||
effective_insert: list[str] = media_write_timeout_change
|
||||
effective_insert: list[str] = media_write_timeout_deprecation
|
||||
elif get_updates and to_insert.lstrip().startswith("read_timeout"):
|
||||
effective_insert = [to_insert, *get_updates_read_timeout_addition]
|
||||
else:
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
chango~=0.4.0
|
||||
sphinx==8.2.3
|
||||
sphinx==8.1.3
|
||||
furo==2024.8.6
|
||||
furo-sphinx-search @ git+https://github.com/harshil21/furo-sphinx-search@v0.2.0.1
|
||||
sphinx-paramlinks==0.6.0
|
||||
|
||||
@@ -1,9 +1 @@
|
||||
.. _ptb-changelog:
|
||||
|
||||
=========
|
||||
Changelog
|
||||
=========
|
||||
|
||||
.. chango::
|
||||
|
||||
.. include:: ../../changes/LEGACY.rst
|
||||
.. include:: ../../CHANGES.rst
|
||||
@@ -8,11 +8,6 @@ from pathlib import Path
|
||||
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
||||
from sphinx.application import Sphinx
|
||||
|
||||
if sys.version_info < (3, 12):
|
||||
# Due to dependency on chango
|
||||
raise RuntimeError("This documentation needs at least Python 3.12")
|
||||
|
||||
|
||||
sys.path.insert(0, str(Path("../..").resolve().absolute()))
|
||||
|
||||
# -- General configuration ------------------------------------------------
|
||||
@@ -41,7 +36,6 @@ needs_sphinx = "8.1.3"
|
||||
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
||||
# ones.
|
||||
extensions = [
|
||||
"chango.sphinx_ext",
|
||||
"sphinx.ext.autodoc",
|
||||
"sphinx.ext.napoleon",
|
||||
"sphinx.ext.intersphinx",
|
||||
@@ -58,9 +52,6 @@ extensions = [
|
||||
if os.environ.get("READTHEDOCS", "") == "True":
|
||||
extensions.append("sphinx_build_compatibility.extension")
|
||||
|
||||
# Configuration for the chango sphinx directive
|
||||
chango_pyproject_toml_path = Path(__file__).parent.parent.parent
|
||||
|
||||
# For shorter links to Wiki in docstrings
|
||||
extlinks = {
|
||||
"wiki": ("https://github.com/python-telegram-bot/python-telegram-bot/wiki/%s", "%s"),
|
||||
@@ -120,11 +111,6 @@ linkcheck_ignore = [
|
||||
# Anchors are apparently inserted by GitHub dynamically, so let's skip checking them
|
||||
"https://github.com/python-telegram-bot/python-telegram-bot/tree/master/examples#",
|
||||
r"https://github\.com/python-telegram-bot/python-telegram-bot/wiki/[\w\-_,]+\#",
|
||||
# The LGPL license link regularly causes network errors for some reason
|
||||
re.escape("https://www.gnu.org/licenses/lgpl-3.0.html"),
|
||||
# The doc-fixes branch may not always exist - doesn't matter, we only link to it from the
|
||||
# contributing guide
|
||||
re.escape("https://docs.python-telegram-bot.org/en/doc-fixes"),
|
||||
]
|
||||
linkcheck_allowed_redirects = {
|
||||
# Redirects to the default version are okay
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
.. _stability-policy:
|
||||
|
||||
Stability Policy
|
||||
================
|
||||
|
||||
|
||||
@@ -27,7 +27,6 @@ Your bot can accept payments from Telegram users. Please see the `introduction t
|
||||
telegram.successfulpayment
|
||||
telegram.transactionpartner
|
||||
telegram.transactionpartneraffiliateprogram
|
||||
telegram.transactionpartnerchat
|
||||
telegram.transactionpartnerfragment
|
||||
telegram.transactionpartnerother
|
||||
telegram.transactionpartnertelegramads
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
TransactionPartnerChat
|
||||
======================
|
||||
|
||||
.. autoclass:: telegram.TransactionPartnerChat
|
||||
:members:
|
||||
:show-inheritance:
|
||||
:inherited-members: TransactionPartner
|
||||
@@ -98,6 +98,4 @@
|
||||
|
||||
.. |tz-naive-dtms| replace:: For timezone naive :obj:`datetime.datetime` objects, the default timezone of the bot will be used, which is UTC unless :attr:`telegram.ext.Defaults.tzinfo` is used.
|
||||
|
||||
.. |org-verify| replace:: `on behalf of the organization <https://telegram.org/verify#third-party-verification>`__
|
||||
|
||||
.. |time-period-input| replace:: :class:`datetime.timedelta` objects are accepted in addition to plain :obj:`int` values.
|
||||
.. |time-period-input| replace:: :class:`datetime.timedelta` objects are accepted in addition to plain :obj:`int` values.
|
||||
@@ -0,0 +1,203 @@
|
||||
#!/usr/bin/env python
|
||||
# pylint: disable=unused-argument
|
||||
# This program is dedicated to the public domain under the CC0 license.
|
||||
"""Simple state machine to handle user support.
|
||||
One admin is supported. The admin can have one active conversation at a time. Other users
|
||||
are put on hold until the admin finishes the current conversation.
|
||||
In each conversation, the admin and the user take turns to send messages.
|
||||
"""
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from telegram import Update
|
||||
from telegram.ext import (
|
||||
Application,
|
||||
CommandHandler,
|
||||
ContextTypes,
|
||||
FiniteStateMachine,
|
||||
MessageHandler,
|
||||
State,
|
||||
StateInfo,
|
||||
filters,
|
||||
)
|
||||
|
||||
# Enable logging
|
||||
logging.basicConfig(
|
||||
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", level=logging.DEBUG
|
||||
)
|
||||
logging.getLogger("httpx").setLevel(logging.WARNING)
|
||||
logging.getLogger("httpcore").setLevel(logging.WARNING)
|
||||
logging.getLogger("telegram").setLevel(logging.WARNING)
|
||||
logging.getLogger("telegram.ext.Application").setLevel(logging.DEBUG)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class UserSupportMachine(FiniteStateMachine[Optional[int]]):
|
||||
|
||||
HOLD = State("HOLD")
|
||||
WELCOMING = State("WELCOMING")
|
||||
WAITING_FOR_REPLY = State("WAITING_FOR_REPLY")
|
||||
WRITING = State("WRITING")
|
||||
|
||||
def __init__(self, admin_id: int):
|
||||
self.admin_id = admin_id
|
||||
super().__init__()
|
||||
|
||||
def _get_admin_state(self) -> tuple[State, int]:
|
||||
return self._states[self.admin_id]
|
||||
|
||||
def get_state_info(self, update: object) -> StateInfo[Optional[int]]:
|
||||
if not isinstance(update, Update) or not (user := update.effective_user):
|
||||
key = None
|
||||
state, version = self.states[key]
|
||||
return StateInfo(key=key, state=state, version=version)
|
||||
|
||||
# Admin is easy - just return the state
|
||||
admin_state, admin_version = self._get_admin_state()
|
||||
if user.id == self.admin_id:
|
||||
logging.debug("Returning admin state: %s", admin_state)
|
||||
return StateInfo(self.admin_id, admin_state, admin_version)
|
||||
|
||||
# If the user state is active in the conversation, we can just return that state
|
||||
user_state, user_version = self._states[user.id]
|
||||
if user_state.matches(self.WELCOMING | self.WRITING | self.WAITING_FOR_REPLY):
|
||||
logging.debug("Returning user state: %s", user_state)
|
||||
return StateInfo(user.id, user_state, user_version)
|
||||
|
||||
# On first interaction, we need to determine what to do with the user
|
||||
# if the admin is not idle, we put the user on hold. Otherwise, they may send the first
|
||||
# message, and we put the admin in waiting for reply to avoid another user occupying the
|
||||
# admin first
|
||||
effective_user_state = self.HOLD if admin_state != State.IDLE else self.WELCOMING
|
||||
self._do_set_state(user.id, effective_user_state, user_version)
|
||||
if effective_user_state == self.WELCOMING:
|
||||
self._do_set_state(self.admin_id, self.WAITING_FOR_REPLY)
|
||||
|
||||
logging.debug("Returning user state: %s", effective_user_state)
|
||||
return StateInfo(user.id, effective_user_state, user_version)
|
||||
|
||||
|
||||
async def welcome_user(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||
await update.effective_message.forward(context.bot_data["admin_id"])
|
||||
suffix = ""
|
||||
if UserSupportMachine.HOLD in context.fsm.get_state_history(context.fsm_state_info.key)[:-1]:
|
||||
suffix = " Thank you for patiently waiting. We hope you enjoyed the music."
|
||||
|
||||
await update.effective_message.reply_text(
|
||||
"Welcome! Your message has been forwarded to the admin. "
|
||||
f"They will get back to you soon.{suffix}"
|
||||
)
|
||||
await context.set_state(UserSupportMachine.WAITING_FOR_REPLY)
|
||||
await context.fsm.set_state(context.bot_data["admin_id"], UserSupportMachine.WRITING)
|
||||
context.bot_data["active_user"] = update.effective_user.id
|
||||
|
||||
|
||||
async def conversation_timeout(context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||
active_user = context.bot_data.get("active_user")
|
||||
admin_id = context.bot_data["admin_id"]
|
||||
|
||||
async def handle(user_id: int) -> None:
|
||||
await context.bot.send_message(
|
||||
user_id, "The conversation has been stopped due to inactivity."
|
||||
)
|
||||
await context.fsm.set_state(user_id, State.IDLE)
|
||||
|
||||
if active_user:
|
||||
await handle(active_user)
|
||||
await handle(admin_id)
|
||||
|
||||
|
||||
async def handle_reply(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||
if not (active_user := context.bot_data.get("active_user")):
|
||||
logger.warning("No active user found, ignoring message")
|
||||
|
||||
target = (
|
||||
active_user
|
||||
if update.effective_user.id == (admin_id := context.bot_data["admin_id"])
|
||||
else admin_id
|
||||
)
|
||||
await context.bot.send_message(target, update.effective_message.text)
|
||||
logging.debug("Forwarded message to %s", target)
|
||||
await context.set_state(UserSupportMachine.WAITING_FOR_REPLY)
|
||||
logging.debug("Done setting state to WAITING_FOR_REPLY for %s", target)
|
||||
await context.fsm.set_state(target, UserSupportMachine.WRITING)
|
||||
logging.debug("Done setting state to WRITING for %s, context.fsm_key")
|
||||
|
||||
context.fsm.schedule_timeout(
|
||||
when=30,
|
||||
callback=conversation_timeout,
|
||||
cancel_keys=[active_user, admin_id],
|
||||
)
|
||||
|
||||
|
||||
async def stop_conversation(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||
text = "The conversation has been stopped."
|
||||
admin_id = context.bot_data["admin_id"]
|
||||
active_user = context.bot_data.get("active_user")
|
||||
|
||||
await context.bot.send_message(admin_id, text)
|
||||
await context.fsm.set_state(admin_id, State.IDLE)
|
||||
if active_user:
|
||||
await context.bot.send_message(active_user, text)
|
||||
await context.fsm.set_state(active_user, State.IDLE)
|
||||
|
||||
|
||||
async def hold_melody(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||
await update.effective_message.reply_text(
|
||||
"You have been put on hold. The admin will get back to you soon. Please hear some music "
|
||||
"while you wait: https://www.youtube.com/watch?v=dQw4w9WgXcQ"
|
||||
)
|
||||
|
||||
|
||||
async def not_your_turn(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||
await update.effective_message.reply_text(
|
||||
"It's not your turn yet. Please wait for the other party to reply to your message."
|
||||
)
|
||||
|
||||
|
||||
async def unsupported_message(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||
await update.effective_message.reply_text("This message is not supported.")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
application = Application.builder().token("TOKEN").build()
|
||||
application.fsm = UserSupportMachine(admin_id=123456)
|
||||
application.fsm.set_job_queue(application.job_queue)
|
||||
application.bot_data["admin_id"] = application.fsm.admin_id
|
||||
|
||||
# Users are welcomed only if they are in the corresponding state
|
||||
application.add_handler(
|
||||
MessageHandler(~filters.User(application.fsm.admin_id) & filters.TEXT, welcome_user),
|
||||
state=UserSupportMachine.WELCOMING,
|
||||
)
|
||||
|
||||
# Conversation logic:
|
||||
# * forward messages between user and admin
|
||||
# * stop the conversation at any time (admin or user)
|
||||
# * point out that the other party is currently writing
|
||||
# Important: Order matters!
|
||||
application.add_handler(
|
||||
CommandHandler("stop", stop_conversation),
|
||||
state=UserSupportMachine.WAITING_FOR_REPLY | UserSupportMachine.WRITING,
|
||||
)
|
||||
application.add_handler(
|
||||
MessageHandler(filters.TEXT, handle_reply), state=UserSupportMachine.WRITING
|
||||
)
|
||||
application.add_handler(
|
||||
MessageHandler(filters.TEXT, not_your_turn), state=UserSupportMachine.WAITING_FOR_REPLY
|
||||
)
|
||||
|
||||
# If the admin is busy, put the user on hold
|
||||
application.add_handler(
|
||||
MessageHandler(filters.TEXT, hold_melody), state=UserSupportMachine.HOLD
|
||||
)
|
||||
|
||||
# Fallback
|
||||
application.add_handler(MessageHandler(filters.ALL, unsupported_message), state=State.ANY)
|
||||
|
||||
application.run_polling(allowed_updates=Update.ALL_TYPES)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,172 @@
|
||||
#!/usr/bin/env python
|
||||
# pylint: disable=unused-argument
|
||||
# This program is dedicated to the public domain under the CC0 license.
|
||||
"""State machine bot showcasing how concurrency can be handled with FSM.
|
||||
How to use:
|
||||
|
||||
* Use Case 1: Concurrent balance updates
|
||||
- /unsafe_update <balance_update>: Unsafe update of the wallet balance. Send the command
|
||||
multiple times in quick succession (less than 1 second) to see the effect
|
||||
- /safe_update <balance_update>: Safe update of the wallet balance. Send the command
|
||||
multiple times in quick succession (less than 1 second) to see the effect
|
||||
|
||||
* Use Case 2: Declare a winner - who is the fastest?
|
||||
- /unsafe_declare_winner: Unsafe declaration of the user as winner. Send the command
|
||||
multiple times in quick succession (less than 1 second) to see the effect. Needs restart
|
||||
after the winner is declared.
|
||||
- /safe_declare_winner: Safe declaration of the user as winner. Send the command
|
||||
multiple times in quick succession (less than 1 second) to see the effect. Needs restart
|
||||
after the winner is declared.
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from telegram import Update
|
||||
from telegram.constants import ChatAction
|
||||
from telegram.ext import (
|
||||
Application,
|
||||
CommandHandler,
|
||||
ContextTypes,
|
||||
FiniteStateMachine,
|
||||
MessageHandler,
|
||||
State,
|
||||
StateInfo,
|
||||
filters,
|
||||
)
|
||||
|
||||
# Enable logging
|
||||
logging.basicConfig(
|
||||
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", level=logging.DEBUG
|
||||
)
|
||||
logging.getLogger("httpx").setLevel(logging.WARNING)
|
||||
logging.getLogger("httpcore").setLevel(logging.WARNING)
|
||||
logging.getLogger("telegram").setLevel(logging.WARNING)
|
||||
logging.getLogger("telegram.ext.Application").setLevel(logging.DEBUG)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ConcurrentMachine(FiniteStateMachine[None]):
|
||||
"""This FSM only knows a global state for the whole bot"""
|
||||
|
||||
UPDATING_BALANCE = State("UPDATING_BALANCE")
|
||||
WINNER_DECLARED = State("WINNER_DECLARED")
|
||||
|
||||
def get_state_info(self, update: object) -> StateInfo[None]:
|
||||
state, version = self.states[None]
|
||||
return StateInfo(key=None, state=state, version=version)
|
||||
|
||||
|
||||
########################################
|
||||
# Use case 1: Concurrent balance updates
|
||||
########################################
|
||||
|
||||
|
||||
async def update_balance(context: ContextTypes.DEFAULT_TYPE, update: Update) -> None:
|
||||
initial_balance = context.bot_data.get("balance", 0)
|
||||
balance_update = int(context.args[0])
|
||||
# Simulate heavy computation
|
||||
await update.effective_message.reply_text(
|
||||
f"Initiating balance update: {initial_balance}. Updating ..."
|
||||
)
|
||||
await update.effective_chat.send_action(ChatAction.TYPING)
|
||||
await asyncio.sleep(4.5)
|
||||
new_balance = context.bot_data["balance"] = initial_balance + balance_update
|
||||
await update.effective_message.reply_text(f"Balance updated. New balance: {new_balance}")
|
||||
|
||||
|
||||
async def unsafe_update(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||
"""Unsafe update of the wallet balance"""
|
||||
# Simulate heavy computation *before* the update is processed
|
||||
await asyncio.sleep(1)
|
||||
|
||||
await context.fsm.set_state(context.fsm_state_info.key, ConcurrentMachine.UPDATING_BALANCE)
|
||||
|
||||
# At this point, the lock is released such that multiple updates can update
|
||||
# the balance concurrently. This can lead to race conditions.
|
||||
await update_balance(context, update)
|
||||
|
||||
await context.fsm.set_state(context.fsm_state_info.key, State.IDLE)
|
||||
|
||||
|
||||
async def safe_update(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||
"""Safe update of the wallet balance"""
|
||||
# Simulate heavy computation *before* the update is processed
|
||||
await asyncio.sleep(1)
|
||||
|
||||
async with context.as_fsm_state(ConcurrentMachine.UPDATING_BALANCE):
|
||||
# At this point, the lock is acquired such that only one update can update
|
||||
# the balance at a time. This prevents race conditions.
|
||||
await update_balance(context, update)
|
||||
|
||||
|
||||
async def busy(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||
"""Busy state"""
|
||||
await update.effective_message.reply_text("I'm busy, try again later.")
|
||||
|
||||
|
||||
####################################################
|
||||
# Use case 2: Declare a winner - who is the fastest?
|
||||
####################################################
|
||||
|
||||
|
||||
async def declare_winner_unsafe(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||
"""Declare the user as winner"""
|
||||
# Simulate heavy computation *before* the update is processed
|
||||
await asyncio.sleep(1)
|
||||
|
||||
# Unsafe state update: No version check, so the state might have already changed
|
||||
await context.fsm.set_state(context.fsm_state_info.key, ConcurrentMachine.WINNER_DECLARED)
|
||||
await update.effective_message.reply_text("You are the winner!")
|
||||
|
||||
|
||||
async def declare_winner_safe(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||
"""Declare the user as winner"""
|
||||
# Simulate heavy computation *before* the update is processed
|
||||
await asyncio.sleep(1)
|
||||
|
||||
try:
|
||||
await context.set_state(ConcurrentMachine.WINNER_DECLARED)
|
||||
await update.effective_message.reply_text("You are the winner!")
|
||||
except ValueError:
|
||||
await update.effective_message.reply_text(
|
||||
"Sorry, you are too late. Someone else was faster."
|
||||
)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
application = Application.builder().token("TOKEN").concurrent_updates(True).build()
|
||||
application.fsm = ConcurrentMachine()
|
||||
|
||||
# Note: OR-combination of states is used here to allow both use cases to be handled
|
||||
# in parallel. Not really necessary for the showcasing, just a nice touch :)
|
||||
|
||||
# Use case 2: Declare a winner - who is the fastest?
|
||||
application.add_handler(
|
||||
CommandHandler("unsafe_declare_winner", declare_winner_unsafe),
|
||||
state=State.IDLE | ConcurrentMachine.UPDATING_BALANCE,
|
||||
)
|
||||
application.add_handler(
|
||||
CommandHandler("safe_declare_winner", declare_winner_safe),
|
||||
state=State.IDLE | ConcurrentMachine.UPDATING_BALANCE,
|
||||
)
|
||||
|
||||
# Use case 1: Concurrent balance updates
|
||||
application.add_handler(
|
||||
CommandHandler("unsafe_update", unsafe_update, has_args=1),
|
||||
state=State.IDLE | ConcurrentMachine.WINNER_DECLARED,
|
||||
)
|
||||
application.add_handler(
|
||||
CommandHandler("safe_update", safe_update, has_args=1),
|
||||
state=State.IDLE | ConcurrentMachine.WINNER_DECLARED,
|
||||
)
|
||||
# Order matters, so this needs to be added last
|
||||
application.add_handler(
|
||||
MessageHandler(filters.ALL, busy), state=ConcurrentMachine.UPDATING_BALANCE
|
||||
)
|
||||
|
||||
application.run_polling(allowed_updates=Update.ALL_TYPES)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -61,9 +61,9 @@ async def start_with_shipping_callback(update: Update, context: ContextTypes.DEF
|
||||
title,
|
||||
description,
|
||||
payload,
|
||||
PAYMENT_PROVIDER_TOKEN,
|
||||
currency,
|
||||
prices,
|
||||
provider_token=PAYMENT_PROVIDER_TOKEN,
|
||||
need_name=True,
|
||||
need_phone_number=True,
|
||||
need_email=True,
|
||||
@@ -90,13 +90,7 @@ async def start_without_shipping_callback(
|
||||
# optionally pass need_name=True, need_phone_number=True,
|
||||
# need_email=True, need_shipping_address=True, is_flexible=True
|
||||
await context.bot.send_invoice(
|
||||
chat_id,
|
||||
title,
|
||||
description,
|
||||
payload,
|
||||
currency,
|
||||
prices,
|
||||
provider_token=PAYMENT_PROVIDER_TOKEN,
|
||||
chat_id, title, description, payload, PAYMENT_PROVIDER_TOKEN, currency, prices
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -105,11 +105,6 @@ search-paths = ["telegram"]
|
||||
[tool.hatch.build]
|
||||
packages = ["telegram"]
|
||||
|
||||
# CHANGO
|
||||
[tool.chango]
|
||||
sys_path = "changes"
|
||||
chango_instance = { name= "chango_instance", module = "config" }
|
||||
|
||||
# BLACK:
|
||||
[tool.black]
|
||||
line-length = 99
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
build
|
||||
|
||||
# For the test suite
|
||||
pytest==8.3.5
|
||||
pytest==8.3.4
|
||||
|
||||
# needed because pytest doesn't come with native support for coroutines as tests
|
||||
pytest-asyncio==0.21.2
|
||||
|
||||
@@ -238,7 +238,6 @@ __all__ = (
|
||||
"TextQuote",
|
||||
"TransactionPartner",
|
||||
"TransactionPartnerAffiliateProgram",
|
||||
"TransactionPartnerChat",
|
||||
"TransactionPartnerFragment",
|
||||
"TransactionPartnerOther",
|
||||
"TransactionPartnerTelegramAds",
|
||||
@@ -276,7 +275,6 @@ from telegram._payment.stars.startransactions import StarTransaction, StarTransa
|
||||
from telegram._payment.stars.transactionpartner import (
|
||||
TransactionPartner,
|
||||
TransactionPartnerAffiliateProgram,
|
||||
TransactionPartnerChat,
|
||||
TransactionPartnerFragment,
|
||||
TransactionPartnerOther,
|
||||
TransactionPartnerTelegramAds,
|
||||
|
||||
+55
-85
@@ -112,7 +112,7 @@ from telegram.error import EndPointNotFound, InvalidToken
|
||||
from telegram.request import BaseRequest, RequestData
|
||||
from telegram.request._httpxrequest import HTTPXRequest
|
||||
from telegram.request._requestparameter import RequestParameter
|
||||
from telegram.warnings import PTBUserWarning
|
||||
from telegram.warnings import PTBDeprecationWarning, PTBUserWarning
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from telegram import (
|
||||
@@ -245,7 +245,7 @@ class Bot(TelegramObject, contextlib.AbstractAsyncContextManager["Bot"]):
|
||||
Example:
|
||||
``"https://api.telegram.org/bot{token}/test"``
|
||||
|
||||
.. versionchanged:: 21.11
|
||||
.. versionchanged:: NEXT.VERSION
|
||||
Supports callable input and string formatting.
|
||||
base_file_url (:obj:`str`, optional): Telegram Bot API file URL.
|
||||
If the string contains ``{token}``, it will be replaced with the bot's
|
||||
@@ -262,7 +262,7 @@ class Bot(TelegramObject, contextlib.AbstractAsyncContextManager["Bot"]):
|
||||
Example:
|
||||
``"https://api.telegram.org/file/bot{token}/test"``
|
||||
|
||||
.. versionchanged:: 21.11
|
||||
.. versionchanged:: NEXT.VERSION
|
||||
Supports callable input and string formatting.
|
||||
request (:class:`telegram.request.BaseRequest`, optional): Pre initialized
|
||||
:class:`telegram.request.BaseRequest` instances. Will be used for all bot methods
|
||||
@@ -1218,7 +1218,6 @@ class Bot(TelegramObject, contextlib.AbstractAsyncContextManager["Bot"]):
|
||||
disable_notification: ODVInput[bool] = DEFAULT_NONE,
|
||||
protect_content: ODVInput[bool] = DEFAULT_NONE,
|
||||
message_thread_id: Optional[int] = None,
|
||||
video_start_timestamp: Optional[int] = None,
|
||||
*,
|
||||
read_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
write_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
@@ -1243,10 +1242,6 @@ class Bot(TelegramObject, contextlib.AbstractAsyncContextManager["Bot"]):
|
||||
original message was sent (or channel username in the format ``@channelusername``).
|
||||
message_id (:obj:`int`): Message identifier in the chat specified in
|
||||
:paramref:`from_chat_id`.
|
||||
video_start_timestamp (:obj:`int`, optional): New start timestamp for the
|
||||
forwarded video in the message
|
||||
|
||||
.. versionadded:: 21.11
|
||||
disable_notification (:obj:`bool`, optional): |disable_notification|
|
||||
protect_content (:obj:`bool`, optional): |protect_content|
|
||||
|
||||
@@ -1265,7 +1260,6 @@ class Bot(TelegramObject, contextlib.AbstractAsyncContextManager["Bot"]):
|
||||
"chat_id": chat_id,
|
||||
"from_chat_id": from_chat_id,
|
||||
"message_id": message_id,
|
||||
"video_start_timestamp": video_start_timestamp,
|
||||
}
|
||||
|
||||
return await self._send_message(
|
||||
@@ -1564,7 +1558,7 @@ class Bot(TelegramObject, contextlib.AbstractAsyncContextManager["Bot"]):
|
||||
duration (:obj:`int` | :class:`datetime.timedelta`, optional): Duration of sent audio
|
||||
in seconds.
|
||||
|
||||
.. versionchanged:: 21.11
|
||||
.. versionchanged:: NEXT.VERSION
|
||||
|time-period-input|
|
||||
performer (:obj:`str`, optional): Performer.
|
||||
title (:obj:`str`, optional): Track name.
|
||||
@@ -1961,8 +1955,6 @@ class Bot(TelegramObject, contextlib.AbstractAsyncContextManager["Bot"]):
|
||||
message_effect_id: Optional[str] = None,
|
||||
allow_paid_broadcast: Optional[bool] = None,
|
||||
show_caption_above_media: Optional[bool] = None,
|
||||
cover: Optional[FileInput] = None,
|
||||
start_timestamp: Optional[int] = None,
|
||||
*,
|
||||
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
|
||||
reply_to_message_id: Optional[int] = None,
|
||||
@@ -2006,17 +1998,10 @@ class Bot(TelegramObject, contextlib.AbstractAsyncContextManager["Bot"]):
|
||||
duration (:obj:`int` | :class:`datetime.timedelta`, optional): Duration of sent video
|
||||
in seconds.
|
||||
|
||||
.. versionchanged:: 21.11
|
||||
.. versionchanged:: NEXT.VERSION
|
||||
|time-period-input|
|
||||
width (:obj:`int`, optional): Video width.
|
||||
height (:obj:`int`, optional): Video height.
|
||||
cover (:term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | :obj:`str`, \
|
||||
optional): Cover for the video in the message. |fileinputnopath|
|
||||
|
||||
.. versionadded:: 21.11
|
||||
start_timestamp (:obj:`int`, optional): Start timestamp for the video in the message.
|
||||
|
||||
.. versionadded:: 21.11
|
||||
caption (:obj:`str`, optional): Video caption (may also be used when resending videos
|
||||
by file_id), 0-:tg-const:`telegram.constants.MessageLimit.CAPTION_LENGTH`
|
||||
characters after entities parsing.
|
||||
@@ -2103,8 +2088,6 @@ class Bot(TelegramObject, contextlib.AbstractAsyncContextManager["Bot"]):
|
||||
"width": width,
|
||||
"height": height,
|
||||
"supports_streaming": supports_streaming,
|
||||
"cover": self._parse_file_input(cover, attach=True) if cover else None,
|
||||
"start_timestamp": start_timestamp,
|
||||
"thumbnail": self._parse_file_input(thumbnail, attach=True) if thumbnail else None,
|
||||
"has_spoiler": has_spoiler,
|
||||
"show_caption_above_media": show_caption_above_media,
|
||||
@@ -2192,7 +2175,7 @@ class Bot(TelegramObject, contextlib.AbstractAsyncContextManager["Bot"]):
|
||||
duration (:obj:`int` | :class:`datetime.timedelta`, optional): Duration of sent video
|
||||
in seconds.
|
||||
|
||||
.. versionchanged:: 21.11
|
||||
.. versionchanged:: NEXT.VERSION
|
||||
|time-period-input|
|
||||
length (:obj:`int`, optional): Video width and height, i.e. diameter of the video
|
||||
message.
|
||||
@@ -2344,7 +2327,7 @@ class Bot(TelegramObject, contextlib.AbstractAsyncContextManager["Bot"]):
|
||||
duration (:obj:`int` | :class:`datetime.timedelta`, optional): Duration of sent
|
||||
animation in seconds.
|
||||
|
||||
.. versionchanged:: 21.11
|
||||
.. versionchanged:: NEXT.VERSION
|
||||
|time-period-input|
|
||||
width (:obj:`int`, optional): Animation width.
|
||||
height (:obj:`int`, optional): Animation height.
|
||||
@@ -2528,7 +2511,7 @@ class Bot(TelegramObject, contextlib.AbstractAsyncContextManager["Bot"]):
|
||||
duration (:obj:`int` | :class:`datetime.timedelta`, optional): Duration of the voice
|
||||
message in seconds.
|
||||
|
||||
.. versionchanged:: 21.11
|
||||
.. versionchanged:: NEXT.VERSION
|
||||
|time-period-input|
|
||||
disable_notification (:obj:`bool`, optional): |disable_notification|
|
||||
protect_content (:obj:`bool`, optional): |protect_content|
|
||||
@@ -2842,7 +2825,7 @@ class Bot(TelegramObject, contextlib.AbstractAsyncContextManager["Bot"]):
|
||||
:tg-const:`telegram.constants.LocationLimit.LIVE_PERIOD_FOREVER` for live
|
||||
locations that can be edited indefinitely.
|
||||
|
||||
.. versionchanged:: 21.11
|
||||
.. versionchanged:: NEXT.VERSION
|
||||
|time-period-input|
|
||||
heading (:obj:`int`, optional): For live locations, a direction in which the user is
|
||||
moving, in degrees. Must be between
|
||||
@@ -3012,7 +2995,7 @@ class Bot(TelegramObject, contextlib.AbstractAsyncContextManager["Bot"]):
|
||||
|
||||
.. versionadded:: 21.2.
|
||||
|
||||
.. versionchanged:: 21.11
|
||||
.. versionchanged:: NEXT.VERSION
|
||||
|time-period-input|
|
||||
business_connection_id (:obj:`str`, optional): |business_id_str_edit|
|
||||
|
||||
@@ -3709,7 +3692,7 @@ class Bot(TelegramObject, contextlib.AbstractAsyncContextManager["Bot"]):
|
||||
time in seconds that the
|
||||
result of the inline query may be cached on the server. Defaults to ``300``.
|
||||
|
||||
.. versionchanged:: 21.11
|
||||
.. versionchanged:: NEXT.VERSION
|
||||
|time-period-input|
|
||||
is_personal (:obj:`bool`, optional): Pass :obj:`True`, if results may be cached on
|
||||
the server side only for the user that sent the query. By default,
|
||||
@@ -4161,7 +4144,7 @@ class Bot(TelegramObject, contextlib.AbstractAsyncContextManager["Bot"]):
|
||||
time in seconds that the
|
||||
result of the callback query may be cached client-side. Defaults to 0.
|
||||
|
||||
.. versionchanged:: 21.11
|
||||
.. versionchanged:: NEXT.VERSION
|
||||
|time-period-input|
|
||||
|
||||
Returns:
|
||||
@@ -4581,7 +4564,19 @@ class Bot(TelegramObject, contextlib.AbstractAsyncContextManager["Bot"]):
|
||||
if not isinstance(read_timeout, DefaultValue):
|
||||
arg_read_timeout: float = read_timeout or 0
|
||||
else:
|
||||
arg_read_timeout = self._request[0].read_timeout or 0
|
||||
try:
|
||||
arg_read_timeout = self._request[0].read_timeout or 0
|
||||
except NotImplementedError:
|
||||
arg_read_timeout = 2
|
||||
self._warn(
|
||||
PTBDeprecationWarning(
|
||||
"20.7",
|
||||
f"The class {self._request[0].__class__.__name__} does not override "
|
||||
"the property `read_timeout`. Overriding this property will be mandatory "
|
||||
"in future versions. Using 2 seconds as fallback.",
|
||||
),
|
||||
stacklevel=2,
|
||||
)
|
||||
|
||||
# Ideally we'd use an aggressive read timeout for the polling. However,
|
||||
# * Short polling should return within 2 seconds.
|
||||
@@ -4636,11 +4631,8 @@ class Bot(TelegramObject, contextlib.AbstractAsyncContextManager["Bot"]):
|
||||
"""
|
||||
Use this method to specify a url and receive incoming updates via an outgoing webhook.
|
||||
Whenever there is an update for the bot, Telegram will send an HTTPS POST request to the
|
||||
specified url, containing An Update. In case of an unsuccessful request
|
||||
(a request with response
|
||||
`HTTP status code <https://en.wikipedia.org/wiki/List_of_HTTP_status_codes>`_different
|
||||
from ``2XY``),
|
||||
Telegram will repeat the request and give up after a reasonable amount of attempts.
|
||||
specified url, containing An Update. In case of an unsuccessful request,
|
||||
Telegram will give up after a reasonable amount of attempts.
|
||||
|
||||
If you'd like to make sure that the Webhook was set by you, you can specify secret data in
|
||||
the parameter :paramref:`secret_token`. If specified, the request will contain a header
|
||||
@@ -5185,9 +5177,9 @@ class Bot(TelegramObject, contextlib.AbstractAsyncContextManager["Bot"]):
|
||||
title: str,
|
||||
description: str,
|
||||
payload: str,
|
||||
provider_token: Optional[str], # This arg is now optional as of Bot API 7.4
|
||||
currency: str,
|
||||
prices: Sequence["LabeledPrice"],
|
||||
provider_token: Optional[str] = None,
|
||||
start_parameter: Optional[str] = None,
|
||||
photo_url: Optional[str] = None,
|
||||
photo_size: Optional[int] = None,
|
||||
@@ -5240,13 +5232,13 @@ class Bot(TelegramObject, contextlib.AbstractAsyncContextManager["Bot"]):
|
||||
:tg-const:`telegram.Invoice.MIN_PAYLOAD_LENGTH`-
|
||||
:tg-const:`telegram.Invoice.MAX_PAYLOAD_LENGTH` bytes. This will not be
|
||||
displayed to the user, use it for your internal processes.
|
||||
provider_token (:obj:`str`, optional): Payments provider token, obtained via
|
||||
provider_token (:obj:`str`): Payments provider token, obtained via
|
||||
`@BotFather <https://t.me/BotFather>`_. Pass an empty string for payments in
|
||||
|tg_stars|.
|
||||
|
||||
.. versionchanged:: 21.11
|
||||
Bot API 7.4 made this parameter is optional and this is now reflected in the
|
||||
function signature.
|
||||
.. deprecated:: 21.3
|
||||
As of Bot API 7.4, this parameter is now optional and future versions of the
|
||||
library will make it optional as well.
|
||||
|
||||
currency (:obj:`str`): Three-letter ISO 4217 currency code, see `more on currencies
|
||||
<https://core.telegram.org/bots/payments#supported-currencies>`_. Pass ``XTR`` for
|
||||
@@ -6972,7 +6964,7 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
|
||||
:tg-const:`telegram.constants.StickerFormat.STATIC` for a
|
||||
``.WEBP`` or ``.PNG`` image, :tg-const:`telegram.constants.StickerFormat.ANIMATED`
|
||||
for a ``.TGS`` animation, :tg-const:`telegram.constants.StickerFormat.VIDEO` for a
|
||||
``.WEBM`` video.
|
||||
WEBM video.
|
||||
|
||||
.. versionadded:: 21.1
|
||||
|
||||
@@ -6986,7 +6978,7 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
|
||||
:tg-const:`telegram.constants.StickerSetLimit.MAX_ANIMATED_THUMBNAIL_SIZE`
|
||||
kilobytes in size; see
|
||||
`the docs <https://core.telegram.org/stickers#animation-requirements>`_ for
|
||||
animated sticker technical requirements, or a ``.WEBM`` video with the thumbnail up
|
||||
animated sticker technical requirements, or a **.WEBM** video with the thumbnail up
|
||||
to :tg-const:`telegram.constants.StickerSetLimit.MAX_ANIMATED_THUMBNAIL_SIZE`
|
||||
kilobytes in size; see
|
||||
`this <https://core.telegram.org/stickers#video-requirements>`_ for video sticker
|
||||
@@ -7375,7 +7367,7 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
|
||||
:tg-const:`telegram.Poll.MAX_OPEN_PERIOD`. Can't be used together with
|
||||
:paramref:`close_date`.
|
||||
|
||||
.. versionchanged:: 21.11
|
||||
.. versionchanged:: NEXT.VERSION
|
||||
|time-period-input|
|
||||
close_date (:obj:`int` | :obj:`datetime.datetime`, optional): Point in time (Unix
|
||||
timestamp) when the poll will be automatically closed. Must be at least
|
||||
@@ -7982,7 +7974,6 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
|
||||
reply_parameters: Optional["ReplyParameters"] = None,
|
||||
show_caption_above_media: Optional[bool] = None,
|
||||
allow_paid_broadcast: Optional[bool] = None,
|
||||
video_start_timestamp: Optional[int] = None,
|
||||
*,
|
||||
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
|
||||
reply_to_message_id: Optional[int] = None,
|
||||
@@ -8002,10 +7993,6 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
|
||||
from_chat_id (:obj:`int` | :obj:`str`): Unique identifier for the chat where the
|
||||
original message was sent (or channel username in the format ``@channelusername``).
|
||||
message_id (:obj:`int`): Message identifier in the chat specified in from_chat_id.
|
||||
video_start_timestamp (:obj:`int`, optional): New start timestamp for the
|
||||
copied video in the message
|
||||
|
||||
.. versionadded:: 21.11
|
||||
caption (:obj:`str`, optional): New caption for media,
|
||||
0-:tg-const:`telegram.constants.MessageLimit.CAPTION_LENGTH` characters after
|
||||
entities parsing. If not specified, the original caption is kept.
|
||||
@@ -8096,7 +8083,6 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
|
||||
"reply_parameters": reply_parameters,
|
||||
"show_caption_above_media": show_caption_above_media,
|
||||
"allow_paid_broadcast": allow_paid_broadcast,
|
||||
"video_start_timestamp": video_start_timestamp,
|
||||
}
|
||||
|
||||
result = await self._post(
|
||||
@@ -8266,9 +8252,9 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
|
||||
title: str,
|
||||
description: str,
|
||||
payload: str,
|
||||
provider_token: Optional[str], # This arg is now optional as of Bot API 7.4
|
||||
currency: str,
|
||||
prices: Sequence["LabeledPrice"],
|
||||
provider_token: Optional[str] = None,
|
||||
max_tip_amount: Optional[int] = None,
|
||||
suggested_tip_amounts: Optional[Sequence[int]] = None,
|
||||
provider_data: Optional[Union[str, object]] = None,
|
||||
@@ -8310,13 +8296,13 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
|
||||
:tg-const:`telegram.Invoice.MIN_PAYLOAD_LENGTH`-
|
||||
:tg-const:`telegram.Invoice.MAX_PAYLOAD_LENGTH` bytes. This will not be
|
||||
displayed to the user, use it for your internal processes.
|
||||
provider_token (:obj:`str`, optional): Payments provider token, obtained via
|
||||
provider_token (:obj:`str`): Payments provider token, obtained via
|
||||
`@BotFather <https://t.me/BotFather>`_. Pass an empty string for payments in
|
||||
|tg_stars|.
|
||||
|
||||
.. versionchanged:: 21.11
|
||||
Bot API 7.4 made this parameter is optional and this is now reflected in the
|
||||
function signature.
|
||||
.. deprecated:: 21.3
|
||||
As of Bot API 7.4, this parameter is now optional and future versions of the
|
||||
library will make it optional as well.
|
||||
|
||||
currency (:obj:`str`): Three-letter ISO 4217 currency code, see `more on currencies
|
||||
<https://core.telegram.org/bots/payments#supported-currencies>`_. Pass ``XTR`` for
|
||||
@@ -9288,8 +9274,7 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
|
||||
api_kwargs: Optional[JSONDict] = None,
|
||||
) -> bool:
|
||||
"""
|
||||
Use this method to change the chosen reactions on a message. Service messages of some types
|
||||
can't be
|
||||
Use this method to change the chosen reactions on a message. Service messages can't be
|
||||
reacted to. Automatically forwarded messages from a channel to its discussion group have
|
||||
the same available reactions as messages in the channel. Bots can't use paid reactions.
|
||||
|
||||
@@ -9576,7 +9561,7 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
|
||||
"is_canceled": is_canceled,
|
||||
}
|
||||
return await self._post(
|
||||
"editUserStarSubscription",
|
||||
"editUserStartSubscription",
|
||||
data,
|
||||
read_timeout=read_timeout,
|
||||
write_timeout=write_timeout,
|
||||
@@ -9722,7 +9707,7 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
|
||||
active for before the next payment. Currently, it must always be
|
||||
:tg-const:`telegram.constants.ChatSubscriptionLimit.SUBSCRIPTION_PERIOD` (30 days).
|
||||
|
||||
.. versionchanged:: 21.11
|
||||
.. versionchanged:: NEXT.VERSION
|
||||
|time-period-input|
|
||||
subscription_price (:obj:`int`): The number of Telegram Stars a user must pay initially
|
||||
and after each subsequent subscription period to be a member of the chat;
|
||||
@@ -9819,7 +9804,7 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
|
||||
pool_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
api_kwargs: Optional[JSONDict] = None,
|
||||
) -> Gifts:
|
||||
"""Returns the list of gifts that can be sent by the bot to users and channel chats.
|
||||
"""Returns the list of gifts that can be sent by the bot to users.
|
||||
Requires no parameters.
|
||||
|
||||
.. versionadded:: 21.8
|
||||
@@ -9843,13 +9828,12 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
|
||||
|
||||
async def send_gift(
|
||||
self,
|
||||
user_id: Optional[int] = None,
|
||||
gift_id: Union[str, Gift] = None, # type: ignore
|
||||
user_id: int,
|
||||
gift_id: Union[str, Gift],
|
||||
text: Optional[str] = None,
|
||||
text_parse_mode: ODVInput[str] = DEFAULT_NONE,
|
||||
text_entities: Optional[Sequence["MessageEntity"]] = None,
|
||||
pay_for_upgrade: Optional[bool] = None,
|
||||
chat_id: Optional[Union[str, int]] = None,
|
||||
*,
|
||||
read_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
write_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
@@ -9857,23 +9841,15 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
|
||||
pool_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
api_kwargs: Optional[JSONDict] = None,
|
||||
) -> bool:
|
||||
"""Sends a gift to the given user or channel chat.
|
||||
The gift can't be converted to Telegram Stars by the receiver.
|
||||
"""Sends a gift to the given user.
|
||||
The gift can't be converted to Telegram Stars by the user
|
||||
|
||||
.. versionadded:: 21.8
|
||||
|
||||
Args:
|
||||
user_id (:obj:`int`, optional): Required if :paramref:`chat_id` is not specified.
|
||||
Unique identifier of the target user that will receive the gift.
|
||||
|
||||
.. versionchanged:: 21.11
|
||||
Now optional.
|
||||
user_id (:obj:`int`): Unique identifier of the target user that will receive the gift
|
||||
gift_id (:obj:`str` | :class:`~telegram.Gift`): Identifier of the gift or a
|
||||
:class:`~telegram.Gift` object
|
||||
chat_id (:obj:`int` | :obj:`str`, optional): Required if :paramref:`user_id`
|
||||
is not specified. |chat_id_channel| It will receive the gift.
|
||||
|
||||
.. versionadded:: 21.11
|
||||
text (:obj:`str`, optional): Text that will be shown along with the gift;
|
||||
0- :tg-const:`telegram.constants.GiftLimit.MAX_TEXT_LENGTH` characters
|
||||
text_parse_mode (:obj:`str`, optional): Mode for parsing entities.
|
||||
@@ -9900,11 +9876,6 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
|
||||
Raises:
|
||||
:class:`telegram.error.TelegramError`
|
||||
"""
|
||||
# TODO: Remove when stability policy allows, tags: deprecated 21.11
|
||||
# also we should raise a deprecation warnung if anything is passed by
|
||||
# position since it will be moved, not sure how
|
||||
if gift_id is None:
|
||||
raise TypeError("Missing required argument `gift_id`.")
|
||||
data: JSONDict = {
|
||||
"user_id": user_id,
|
||||
"gift_id": gift_id.id if isinstance(gift_id, Gift) else gift_id,
|
||||
@@ -9912,7 +9883,6 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
|
||||
"text_parse_mode": text_parse_mode,
|
||||
"text_entities": text_entities,
|
||||
"pay_for_upgrade": pay_for_upgrade,
|
||||
"chat_id": chat_id,
|
||||
}
|
||||
return await self._post(
|
||||
"sendGift",
|
||||
@@ -9935,7 +9905,7 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
|
||||
pool_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
api_kwargs: Optional[JSONDict] = None,
|
||||
) -> bool:
|
||||
"""Verifies a chat |org-verify| which is represented by the bot.
|
||||
"""Verifies a chat on behalf of the organization which is represented by the bot.
|
||||
|
||||
.. versionadded:: 21.10
|
||||
|
||||
@@ -9977,7 +9947,7 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
|
||||
pool_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
api_kwargs: Optional[JSONDict] = None,
|
||||
) -> bool:
|
||||
"""Verifies a user |org-verify| which is represented by the bot.
|
||||
"""Verifies a user on behalf of the organization which is represented by the bot.
|
||||
|
||||
.. versionadded:: 21.10
|
||||
|
||||
@@ -10018,8 +9988,8 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
|
||||
pool_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
api_kwargs: Optional[JSONDict] = None,
|
||||
) -> bool:
|
||||
"""Removes verification from a chat that is currently verified |org-verify|
|
||||
represented by the bot.
|
||||
"""Removes verification from a chat that is currently verified on behalf of the
|
||||
organization represented by the bot.
|
||||
|
||||
|
||||
|
||||
@@ -10057,8 +10027,8 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
|
||||
pool_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
api_kwargs: Optional[JSONDict] = None,
|
||||
) -> bool:
|
||||
"""Removes verification from a user who is currently verified |org-verify|
|
||||
represented by the bot.
|
||||
"""Removes verification from a user who is currently verified on behalf of the
|
||||
organization represented by the bot.
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -831,7 +831,6 @@ class CallbackQuery(TelegramObject):
|
||||
reply_parameters: Optional["ReplyParameters"] = None,
|
||||
show_caption_above_media: Optional[bool] = None,
|
||||
allow_paid_broadcast: Optional[bool] = None,
|
||||
video_start_timestamp: Optional[int] = None,
|
||||
*,
|
||||
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
|
||||
reply_to_message_id: Optional[int] = None,
|
||||
@@ -865,7 +864,6 @@ class CallbackQuery(TelegramObject):
|
||||
chat_id=chat_id,
|
||||
caption=caption,
|
||||
parse_mode=parse_mode,
|
||||
video_start_timestamp=video_start_timestamp,
|
||||
caption_entities=caption_entities,
|
||||
disable_notification=disable_notification,
|
||||
reply_to_message_id=reply_to_message_id,
|
||||
|
||||
+3
-23
@@ -1576,9 +1576,9 @@ class _ChatBase(TelegramObject):
|
||||
title: str,
|
||||
description: str,
|
||||
payload: str,
|
||||
provider_token: Optional[str],
|
||||
currency: str,
|
||||
prices: Sequence["LabeledPrice"],
|
||||
provider_token: Optional[str] = None,
|
||||
start_parameter: Optional[str] = None,
|
||||
photo_url: Optional[str] = None,
|
||||
photo_size: Optional[int] = None,
|
||||
@@ -1940,8 +1940,6 @@ class _ChatBase(TelegramObject):
|
||||
message_effect_id: Optional[str] = None,
|
||||
allow_paid_broadcast: Optional[bool] = None,
|
||||
show_caption_above_media: Optional[bool] = None,
|
||||
cover: Optional[FileInput] = None,
|
||||
start_timestamp: Optional[int] = None,
|
||||
*,
|
||||
reply_to_message_id: Optional[int] = None,
|
||||
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
|
||||
@@ -1980,8 +1978,6 @@ class _ChatBase(TelegramObject):
|
||||
parse_mode=parse_mode,
|
||||
supports_streaming=supports_streaming,
|
||||
thumbnail=thumbnail,
|
||||
cover=cover,
|
||||
start_timestamp=start_timestamp,
|
||||
api_kwargs=api_kwargs,
|
||||
allow_sending_without_reply=allow_sending_without_reply,
|
||||
caption_entities=caption_entities,
|
||||
@@ -2203,7 +2199,6 @@ class _ChatBase(TelegramObject):
|
||||
reply_parameters: Optional["ReplyParameters"] = None,
|
||||
show_caption_above_media: Optional[bool] = None,
|
||||
allow_paid_broadcast: Optional[bool] = None,
|
||||
video_start_timestamp: Optional[int] = None,
|
||||
*,
|
||||
reply_to_message_id: Optional[int] = None,
|
||||
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
|
||||
@@ -2230,7 +2225,6 @@ class _ChatBase(TelegramObject):
|
||||
from_chat_id=from_chat_id,
|
||||
message_id=message_id,
|
||||
caption=caption,
|
||||
video_start_timestamp=video_start_timestamp,
|
||||
parse_mode=parse_mode,
|
||||
caption_entities=caption_entities,
|
||||
disable_notification=disable_notification,
|
||||
@@ -2263,7 +2257,6 @@ class _ChatBase(TelegramObject):
|
||||
reply_parameters: Optional["ReplyParameters"] = None,
|
||||
show_caption_above_media: Optional[bool] = None,
|
||||
allow_paid_broadcast: Optional[bool] = None,
|
||||
video_start_timestamp: Optional[int] = None,
|
||||
*,
|
||||
reply_to_message_id: Optional[int] = None,
|
||||
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
|
||||
@@ -2290,7 +2283,6 @@ class _ChatBase(TelegramObject):
|
||||
chat_id=chat_id,
|
||||
message_id=message_id,
|
||||
caption=caption,
|
||||
video_start_timestamp=video_start_timestamp,
|
||||
parse_mode=parse_mode,
|
||||
caption_entities=caption_entities,
|
||||
disable_notification=disable_notification,
|
||||
@@ -2406,7 +2398,6 @@ class _ChatBase(TelegramObject):
|
||||
disable_notification: ODVInput[bool] = DEFAULT_NONE,
|
||||
protect_content: ODVInput[bool] = DEFAULT_NONE,
|
||||
message_thread_id: Optional[int] = None,
|
||||
video_start_timestamp: Optional[int] = None,
|
||||
*,
|
||||
read_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
write_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
@@ -2432,7 +2423,6 @@ class _ChatBase(TelegramObject):
|
||||
chat_id=self.id,
|
||||
from_chat_id=from_chat_id,
|
||||
message_id=message_id,
|
||||
video_start_timestamp=video_start_timestamp,
|
||||
disable_notification=disable_notification,
|
||||
read_timeout=read_timeout,
|
||||
write_timeout=write_timeout,
|
||||
@@ -2450,7 +2440,6 @@ class _ChatBase(TelegramObject):
|
||||
disable_notification: ODVInput[bool] = DEFAULT_NONE,
|
||||
protect_content: ODVInput[bool] = DEFAULT_NONE,
|
||||
message_thread_id: Optional[int] = None,
|
||||
video_start_timestamp: Optional[int] = None,
|
||||
*,
|
||||
read_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
write_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
@@ -2477,7 +2466,6 @@ class _ChatBase(TelegramObject):
|
||||
from_chat_id=self.id,
|
||||
chat_id=chat_id,
|
||||
message_id=message_id,
|
||||
video_start_timestamp=video_start_timestamp,
|
||||
disable_notification=disable_notification,
|
||||
read_timeout=read_timeout,
|
||||
write_timeout=write_timeout,
|
||||
@@ -3474,25 +3462,18 @@ class _ChatBase(TelegramObject):
|
||||
|
||||
await bot.send_gift(user_id=update.effective_chat.id, *args, **kwargs )
|
||||
|
||||
or::
|
||||
|
||||
await bot.send_gift(chat_id=update.effective_chat.id, *args, **kwargs )
|
||||
|
||||
For the documentation of the arguments, please see :meth:`telegram.Bot.send_gift`.
|
||||
|
||||
Caution:
|
||||
Will only work if the chat is a private or channel chat, see :attr:`type`.
|
||||
Can only work, if the chat is a private chat, see :attr:`type`.
|
||||
|
||||
.. versionadded:: 21.8
|
||||
|
||||
.. versionchanged:: 21.11
|
||||
|
||||
Added support for channel chats.
|
||||
|
||||
Returns:
|
||||
:obj:`bool`: On success, :obj:`True` is returned.
|
||||
"""
|
||||
return await self.get_bot().send_gift(
|
||||
user_id=self.id,
|
||||
gift_id=gift_id,
|
||||
text=text,
|
||||
text_parse_mode=text_parse_mode,
|
||||
@@ -3503,7 +3484,6 @@ class _ChatBase(TelegramObject):
|
||||
connect_timeout=connect_timeout,
|
||||
pool_timeout=pool_timeout,
|
||||
api_kwargs=api_kwargs,
|
||||
**{"chat_id" if self.type == Chat.CHANNEL else "user_id": self.id},
|
||||
)
|
||||
|
||||
async def verify(
|
||||
|
||||
@@ -388,8 +388,8 @@ class BackgroundTypeWallpaper(BackgroundType):
|
||||
|
||||
class BackgroundTypePattern(BackgroundType):
|
||||
"""
|
||||
The background is a ``.PNG`` or ``.TGV`` (gzipped subset of ``SVG`` with ``MIME`` type
|
||||
``"application/x-tgwallpattern"``) pattern to be combined with the background fill
|
||||
The background is a `PNG` or `TGV` (gzipped subset of `SVG` with `MIME` type
|
||||
`"application/x-tgwallpattern"`) pattern to be combined with the background fill
|
||||
chosen by the user.
|
||||
|
||||
Objects of this class are comparable in terms of equality. Two objects of this class are
|
||||
|
||||
@@ -200,9 +200,6 @@ class ChatFullInfo(_ChatBase):
|
||||
sent or forwarded to the channel chat. The field is available only for channel chats.
|
||||
|
||||
.. versionadded:: 21.4
|
||||
can_send_gift (:obj:`bool`, optional): :obj:`True`, if gifts can be sent to the chat.
|
||||
|
||||
.. versionadded:: 21.11
|
||||
|
||||
Attributes:
|
||||
id (:obj:`int`): Unique identifier for this chat.
|
||||
@@ -357,9 +354,6 @@ class ChatFullInfo(_ChatBase):
|
||||
sent or forwarded to the channel chat. The field is available only for channel chats.
|
||||
|
||||
.. versionadded:: 21.4
|
||||
can_send_gift (:obj:`bool`): Optional. :obj:`True`, if gifts can be sent to the chat.
|
||||
|
||||
.. versionadded:: 21.11
|
||||
|
||||
.. _accent colors: https://core.telegram.org/bots/api#accent-colors
|
||||
.. _topics: https://telegram.org/blog/topics-in-groups-collectible-usernames#topics-in-groups
|
||||
@@ -375,7 +369,6 @@ class ChatFullInfo(_ChatBase):
|
||||
"business_intro",
|
||||
"business_location",
|
||||
"business_opening_hours",
|
||||
"can_send_gift",
|
||||
"can_send_paid_media",
|
||||
"can_set_sticker_set",
|
||||
"custom_emoji_sticker_set_name",
|
||||
@@ -452,7 +445,6 @@ class ChatFullInfo(_ChatBase):
|
||||
linked_chat_id: Optional[int] = None,
|
||||
location: Optional[ChatLocation] = None,
|
||||
can_send_paid_media: Optional[bool] = None,
|
||||
can_send_gift: Optional[bool] = None,
|
||||
*,
|
||||
api_kwargs: Optional[JSONDict] = None,
|
||||
):
|
||||
@@ -518,7 +510,6 @@ class ChatFullInfo(_ChatBase):
|
||||
self.business_location: Optional[BusinessLocation] = business_location
|
||||
self.business_opening_hours: Optional[BusinessOpeningHours] = business_opening_hours
|
||||
self.can_send_paid_media: Optional[bool] = can_send_paid_media
|
||||
self.can_send_gift: Optional[bool] = can_send_gift
|
||||
|
||||
@classmethod
|
||||
def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "ChatFullInfo":
|
||||
|
||||
@@ -24,7 +24,6 @@ from typing import TYPE_CHECKING, Final, Optional
|
||||
from telegram import constants
|
||||
from telegram._telegramobject import TelegramObject
|
||||
from telegram._user import User
|
||||
from telegram._utils import enum
|
||||
from telegram._utils.argumentparsing import de_json_optional
|
||||
from telegram._utils.datetime import extract_tzinfo_from_defaults, from_timestamp
|
||||
from telegram._utils.types import JSONDict
|
||||
@@ -100,7 +99,7 @@ class ChatMember(TelegramObject):
|
||||
super().__init__(api_kwargs=api_kwargs)
|
||||
# Required by all subclasses
|
||||
self.user: User = user
|
||||
self.status: str = enum.get_member(constants.ChatMemberStatus, status, status)
|
||||
self.status: str = status
|
||||
|
||||
self._id_attrs = (self.user, self.status)
|
||||
|
||||
|
||||
@@ -214,13 +214,6 @@ class InputPaidMediaVideo(InputPaidMedia):
|
||||
Lastly you can pass an existing :class:`telegram.Video` object to send.
|
||||
thumbnail (:term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | :obj:`str`, \
|
||||
optional): |thumbdocstringnopath|
|
||||
cover (:term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | :obj:`str`, \
|
||||
optional): Cover for the video in the message. |fileinputnopath|
|
||||
|
||||
.. versionchanged:: 21.11
|
||||
start_timestamp (:obj:`int`, optional): Start timestamp for the video in the message
|
||||
|
||||
.. versionchanged:: 21.11
|
||||
width (:obj:`int`, optional): Video width.
|
||||
height (:obj:`int`, optional): Video height.
|
||||
duration (:obj:`int`, optional): Video duration in seconds.
|
||||
@@ -232,13 +225,6 @@ class InputPaidMediaVideo(InputPaidMedia):
|
||||
:tg-const:`telegram.constants.InputPaidMediaType.VIDEO`.
|
||||
media (:obj:`str` | :class:`telegram.InputFile`): Video to send.
|
||||
thumbnail (:class:`telegram.InputFile`): Optional. |thumbdocstringbase|
|
||||
cover (:class:`telegram.InputFile`): Optional. Cover for the video in the message.
|
||||
|fileinputnopath|
|
||||
|
||||
.. versionchanged:: 21.11
|
||||
start_timestamp (:obj:`int`): Optional. Start timestamp for the video in the message
|
||||
|
||||
.. versionchanged:: 21.11
|
||||
width (:obj:`int`): Optional. Video width.
|
||||
height (:obj:`int`): Optional. Video height.
|
||||
duration (:obj:`int`): Optional. Video duration in seconds.
|
||||
@@ -246,15 +232,7 @@ class InputPaidMediaVideo(InputPaidMedia):
|
||||
suitable for streaming.
|
||||
"""
|
||||
|
||||
__slots__ = (
|
||||
"cover",
|
||||
"duration",
|
||||
"height",
|
||||
"start_timestamp",
|
||||
"supports_streaming",
|
||||
"thumbnail",
|
||||
"width",
|
||||
)
|
||||
__slots__ = ("duration", "height", "supports_streaming", "thumbnail", "width")
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -264,8 +242,6 @@ class InputPaidMediaVideo(InputPaidMedia):
|
||||
height: Optional[int] = None,
|
||||
duration: Optional[int] = None,
|
||||
supports_streaming: Optional[bool] = None,
|
||||
cover: Optional[FileInput] = None,
|
||||
start_timestamp: Optional[int] = None,
|
||||
*,
|
||||
api_kwargs: Optional[JSONDict] = None,
|
||||
):
|
||||
@@ -288,10 +264,6 @@ class InputPaidMediaVideo(InputPaidMedia):
|
||||
self.height: Optional[int] = height
|
||||
self.duration: Optional[int] = duration
|
||||
self.supports_streaming: Optional[bool] = supports_streaming
|
||||
self.cover: Optional[Union[InputFile, str]] = (
|
||||
parse_file_input(cover, attach=True, local_mode=True) if cover else None
|
||||
)
|
||||
self.start_timestamp: Optional[int] = start_timestamp
|
||||
|
||||
|
||||
class InputMediaAnimation(InputMedia):
|
||||
@@ -564,13 +536,6 @@ class InputMediaVideo(InputMedia):
|
||||
optional): |thumbdocstringnopath|
|
||||
|
||||
.. versionadded:: 20.2
|
||||
cover (:term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | :obj:`str`, \
|
||||
optional): Cover for the video in the message. |fileinputnopath|
|
||||
|
||||
.. versionchanged:: 21.11
|
||||
start_timestamp (:obj:`int`, optional): Start timestamp for the video in the message
|
||||
|
||||
.. versionchanged:: 21.11
|
||||
show_caption_above_media (:obj:`bool`, optional): Pass |show_cap_above_med|
|
||||
|
||||
.. versionadded:: 21.3
|
||||
@@ -603,22 +568,13 @@ class InputMediaVideo(InputMedia):
|
||||
show_caption_above_media (:obj:`bool`): Optional. |show_cap_above_med|
|
||||
|
||||
.. versionadded:: 21.3
|
||||
cover (:class:`telegram.InputFile`): Optional. Cover for the video in the message.
|
||||
|fileinputnopath|
|
||||
|
||||
.. versionchanged:: 21.11
|
||||
start_timestamp (:obj:`int`): Optional. Start timestamp for the video in the message
|
||||
|
||||
.. versionchanged:: 21.11
|
||||
"""
|
||||
|
||||
__slots__ = (
|
||||
"cover",
|
||||
"duration",
|
||||
"has_spoiler",
|
||||
"height",
|
||||
"show_caption_above_media",
|
||||
"start_timestamp",
|
||||
"supports_streaming",
|
||||
"thumbnail",
|
||||
"width",
|
||||
@@ -638,8 +594,6 @@ class InputMediaVideo(InputMedia):
|
||||
has_spoiler: Optional[bool] = None,
|
||||
thumbnail: Optional[FileInput] = None,
|
||||
show_caption_above_media: Optional[bool] = None,
|
||||
cover: Optional[FileInput] = None,
|
||||
start_timestamp: Optional[int] = None,
|
||||
*,
|
||||
api_kwargs: Optional[JSONDict] = None,
|
||||
):
|
||||
@@ -671,10 +625,6 @@ class InputMediaVideo(InputMedia):
|
||||
self.supports_streaming: Optional[bool] = supports_streaming
|
||||
self.has_spoiler: Optional[bool] = has_spoiler
|
||||
self.show_caption_above_media: Optional[bool] = show_caption_above_media
|
||||
self.cover: Optional[Union[InputFile, str]] = (
|
||||
parse_file_input(cover, attach=True, local_mode=True) if cover else None
|
||||
)
|
||||
self.start_timestamp: Optional[int] = start_timestamp
|
||||
|
||||
|
||||
class InputMediaAudio(InputMedia):
|
||||
|
||||
@@ -61,8 +61,8 @@ class InputSticker(TelegramObject):
|
||||
format (:obj:`str`): Format of the added sticker, must be one of
|
||||
:tg-const:`telegram.constants.StickerFormat.STATIC` for a
|
||||
``.WEBP`` or ``.PNG`` image, :tg-const:`telegram.constants.StickerFormat.ANIMATED`
|
||||
for a ``.TGS`` animation, :tg-const:`telegram.constants.StickerFormat.VIDEO` for a
|
||||
``.WEBM`` video.
|
||||
for a ``.TGS`` animation, :tg-const:`telegram.constants.StickerFormat.VIDEO` for a WEBM
|
||||
video.
|
||||
|
||||
.. versionadded:: 21.1
|
||||
|
||||
@@ -84,8 +84,8 @@ class InputSticker(TelegramObject):
|
||||
format (:obj:`str`): Format of the added sticker, must be one of
|
||||
:tg-const:`telegram.constants.StickerFormat.STATIC` for a
|
||||
``.WEBP`` or ``.PNG`` image, :tg-const:`telegram.constants.StickerFormat.ANIMATED`
|
||||
for a ``.TGS`` animation, :tg-const:`telegram.constants.StickerFormat.VIDEO` for a
|
||||
``.WEBM`` video.
|
||||
for a ``.TGS`` animation, :tg-const:`telegram.constants.StickerFormat.VIDEO` for a WEBM
|
||||
video.
|
||||
|
||||
.. versionadded:: 21.1
|
||||
"""
|
||||
|
||||
@@ -17,17 +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/].
|
||||
"""This module contains an object that represents a Telegram Video."""
|
||||
from collections.abc import Sequence
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
from typing import Optional
|
||||
|
||||
from telegram._files._basethumbedmedium import _BaseThumbedMedium
|
||||
from telegram._files.photosize import PhotoSize
|
||||
from telegram._utils.argumentparsing import de_list_optional, parse_sequence_arg
|
||||
from telegram._utils.types import JSONDict
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from telegram import Bot
|
||||
|
||||
|
||||
class Video(_BaseThumbedMedium):
|
||||
"""This object represents a video file.
|
||||
@@ -53,13 +48,6 @@ class Video(_BaseThumbedMedium):
|
||||
thumbnail (:class:`telegram.PhotoSize`, optional): Video thumbnail.
|
||||
|
||||
.. versionadded:: 20.2
|
||||
cover (Sequence[:class:`telegram.PhotoSize`], optional): Available sizes of the cover of
|
||||
the video in the message.
|
||||
|
||||
.. versionadded:: 21.11
|
||||
start_timestamp (:obj:`int`, optional): Timestamp in seconds from which the video
|
||||
will play in the message
|
||||
.. versionadded:: 21.11
|
||||
|
||||
Attributes:
|
||||
file_id (:obj:`str`): Identifier for this file, which can be used to download
|
||||
@@ -76,24 +64,9 @@ class Video(_BaseThumbedMedium):
|
||||
thumbnail (:class:`telegram.PhotoSize`): Optional. Video thumbnail.
|
||||
|
||||
.. versionadded:: 20.2
|
||||
cover (tuple[:class:`telegram.PhotoSize`]): Optional, Available sizes of the cover of
|
||||
the video in the message.
|
||||
|
||||
.. versionadded:: 21.11
|
||||
start_timestamp (:obj:`int`): Optional, Timestamp in seconds from which the video
|
||||
will play in the message
|
||||
.. versionadded:: 21.11
|
||||
"""
|
||||
|
||||
__slots__ = (
|
||||
"cover",
|
||||
"duration",
|
||||
"file_name",
|
||||
"height",
|
||||
"mime_type",
|
||||
"start_timestamp",
|
||||
"width",
|
||||
)
|
||||
__slots__ = ("duration", "file_name", "height", "mime_type", "width")
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -106,8 +79,6 @@ class Video(_BaseThumbedMedium):
|
||||
file_size: Optional[int] = None,
|
||||
file_name: Optional[str] = None,
|
||||
thumbnail: Optional[PhotoSize] = None,
|
||||
cover: Optional[Sequence[PhotoSize]] = None,
|
||||
start_timestamp: Optional[int] = None,
|
||||
*,
|
||||
api_kwargs: Optional[JSONDict] = None,
|
||||
):
|
||||
@@ -126,14 +97,3 @@ class Video(_BaseThumbedMedium):
|
||||
# Optional
|
||||
self.mime_type: Optional[str] = mime_type
|
||||
self.file_name: Optional[str] = file_name
|
||||
self.cover: Optional[Sequence[PhotoSize]] = parse_sequence_arg(cover)
|
||||
self.start_timestamp: Optional[int] = start_timestamp
|
||||
|
||||
@classmethod
|
||||
def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "Video":
|
||||
"""See :meth:`telegram.TelegramObject.de_json`."""
|
||||
data = cls._parse_data(data)
|
||||
|
||||
data["cover"] = de_list_optional(data.get("cover"), PhotoSize, bot)
|
||||
|
||||
return super().de_json(data=data, bot=bot)
|
||||
|
||||
@@ -62,12 +62,6 @@ class InlineQuery(TelegramObject):
|
||||
``auto_pagination``. Use a named argument for those,
|
||||
and notice that some positional arguments changed position as a result.
|
||||
|
||||
.. versionchanged:: 22.0
|
||||
Removed constants ``MIN_START_PARAMETER_LENGTH`` and ``MAX_START_PARAMETER_LENGTH``.
|
||||
Use :attr:`telegram.constants.InlineQueryResultsButtonLimit.MIN_START_PARAMETER_LENGTH` and
|
||||
:attr:`telegram.constants.InlineQueryResultsButtonLimit.MAX_START_PARAMETER_LENGTH`
|
||||
instead.
|
||||
|
||||
Args:
|
||||
id (:obj:`str`): Unique identifier for this query.
|
||||
from_user (:class:`telegram.User`): Sender.
|
||||
@@ -208,6 +202,16 @@ class InlineQuery(TelegramObject):
|
||||
|
||||
.. versionadded:: 13.2
|
||||
"""
|
||||
MIN_SWITCH_PM_TEXT_LENGTH: Final[int] = constants.InlineQueryLimit.MIN_SWITCH_PM_TEXT_LENGTH
|
||||
""":const:`telegram.constants.InlineQueryLimit.MIN_SWITCH_PM_TEXT_LENGTH`
|
||||
|
||||
.. versionadded:: 20.0
|
||||
"""
|
||||
MAX_SWITCH_PM_TEXT_LENGTH: Final[int] = constants.InlineQueryLimit.MAX_SWITCH_PM_TEXT_LENGTH
|
||||
""":const:`telegram.constants.InlineQueryLimit.MAX_SWITCH_PM_TEXT_LENGTH`
|
||||
|
||||
.. versionadded:: 20.0
|
||||
"""
|
||||
MAX_OFFSET_LENGTH: Final[int] = constants.InlineQueryLimit.MAX_OFFSET_LENGTH
|
||||
""":const:`telegram.constants.InlineQueryLimit.MAX_OFFSET_LENGTH`
|
||||
|
||||
|
||||
@@ -23,7 +23,9 @@ from typing import TYPE_CHECKING, Optional
|
||||
from telegram._inline.inlinekeyboardmarkup import InlineKeyboardMarkup
|
||||
from telegram._inline.inlinequeryresult import InlineQueryResult
|
||||
from telegram._utils.types import JSONDict
|
||||
from telegram._utils.warnings import warn
|
||||
from telegram.constants import InlineQueryResultType
|
||||
from telegram.warnings import PTBDeprecationWarning
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from telegram import InputMessageContent
|
||||
@@ -38,9 +40,6 @@ class InlineQueryResultArticle(InlineQueryResult):
|
||||
.. versionchanged:: 20.5
|
||||
Removed the deprecated arguments and attributes ``thumb_*``.
|
||||
|
||||
.. versionchanged:: 21.11
|
||||
Removed the deprecated argument and attribute ``hide_url``.
|
||||
|
||||
Args:
|
||||
id (:obj:`str`): Unique identifier for this result,
|
||||
:tg-const:`telegram.InlineQueryResult.MIN_ID_LENGTH`-
|
||||
@@ -51,9 +50,12 @@ class InlineQueryResultArticle(InlineQueryResult):
|
||||
reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): Inline keyboard attached
|
||||
to the message.
|
||||
url (:obj:`str`, optional): URL of the result.
|
||||
hide_url (:obj:`bool`, optional): Pass :obj:`True`, if you don't want the URL to be shown
|
||||
in the message.
|
||||
|
||||
Tip:
|
||||
Pass an empty string as URL if you don't want the URL to be shown in the message.
|
||||
.. deprecated:: 21.10
|
||||
This attribute will be removed in future PTB versions. Pass an empty string as URL
|
||||
instead.
|
||||
description (:obj:`str`, optional): Short description of the result.
|
||||
thumbnail_url (:obj:`str`, optional): Url of the thumbnail for the result.
|
||||
|
||||
@@ -76,6 +78,12 @@ class InlineQueryResultArticle(InlineQueryResult):
|
||||
reply_markup (:class:`telegram.InlineKeyboardMarkup`): Optional. Inline keyboard attached
|
||||
to the message.
|
||||
url (:obj:`str`): Optional. URL of the result.
|
||||
hide_url (:obj:`bool`): Optional. Pass :obj:`True`, if you don't want the URL to be shown
|
||||
in the message.
|
||||
|
||||
.. deprecated:: 21.10
|
||||
This attribute will be removed in future PTB versions. Pass an empty string as URL
|
||||
instead.
|
||||
description (:obj:`str`): Optional. Short description of the result.
|
||||
thumbnail_url (:obj:`str`): Optional. Url of the thumbnail for the result.
|
||||
|
||||
@@ -91,6 +99,7 @@ class InlineQueryResultArticle(InlineQueryResult):
|
||||
|
||||
__slots__ = (
|
||||
"description",
|
||||
"hide_url",
|
||||
"input_message_content",
|
||||
"reply_markup",
|
||||
"thumbnail_height",
|
||||
@@ -107,6 +116,7 @@ class InlineQueryResultArticle(InlineQueryResult):
|
||||
input_message_content: "InputMessageContent",
|
||||
reply_markup: Optional[InlineKeyboardMarkup] = None,
|
||||
url: Optional[str] = None,
|
||||
hide_url: Optional[bool] = None,
|
||||
description: Optional[str] = None,
|
||||
thumbnail_url: Optional[str] = None,
|
||||
thumbnail_width: Optional[int] = None,
|
||||
@@ -123,6 +133,16 @@ class InlineQueryResultArticle(InlineQueryResult):
|
||||
# Optional
|
||||
self.reply_markup: Optional[InlineKeyboardMarkup] = reply_markup
|
||||
self.url: Optional[str] = url
|
||||
if hide_url is not None:
|
||||
warn(
|
||||
PTBDeprecationWarning(
|
||||
"21.10",
|
||||
"The argument `hide_url` will be removed in future PTB"
|
||||
"versions. Pass an empty string as URL instead.",
|
||||
),
|
||||
stacklevel=2,
|
||||
)
|
||||
self.hide_url: Optional[bool] = hide_url
|
||||
self.description: Optional[str] = description
|
||||
self.thumbnail_url: Optional[str] = thumbnail_url
|
||||
self.thumbnail_width: Optional[int] = thumbnail_width
|
||||
|
||||
@@ -47,7 +47,7 @@ class InlineQueryResultGif(InlineQueryResult):
|
||||
id (:obj:`str`): Unique identifier for this result,
|
||||
:tg-const:`telegram.InlineQueryResult.MIN_ID_LENGTH`-
|
||||
:tg-const:`telegram.InlineQueryResult.MAX_ID_LENGTH` Bytes.
|
||||
gif_url (:obj:`str`): A valid URL for the GIF file.
|
||||
gif_url (:obj:`str`): A valid URL for the GIF file. File size must not exceed 1MB.
|
||||
gif_width (:obj:`int`, optional): Width of the GIF.
|
||||
gif_height (:obj:`int`, optional): Height of the GIF.
|
||||
gif_duration (:obj:`int`, optional): Duration of the GIF in seconds.
|
||||
@@ -86,7 +86,7 @@ class InlineQueryResultGif(InlineQueryResult):
|
||||
id (:obj:`str`): Unique identifier for this result,
|
||||
:tg-const:`telegram.InlineQueryResult.MIN_ID_LENGTH`-
|
||||
:tg-const:`telegram.InlineQueryResult.MAX_ID_LENGTH` Bytes.
|
||||
gif_url (:obj:`str`): A valid URL for the GIF file.
|
||||
gif_url (:obj:`str`): A valid URL for the GIF file. File size must not exceed 1MB.
|
||||
gif_width (:obj:`int`): Optional. Width of the GIF.
|
||||
gif_height (:obj:`int`): Optional. Height of the GIF.
|
||||
gif_duration (:obj:`int`): Optional. Duration of the GIF in seconds.
|
||||
|
||||
@@ -48,7 +48,7 @@ class InlineQueryResultMpeg4Gif(InlineQueryResult):
|
||||
id (:obj:`str`): Unique identifier for this result,
|
||||
:tg-const:`telegram.InlineQueryResult.MIN_ID_LENGTH`-
|
||||
:tg-const:`telegram.InlineQueryResult.MAX_ID_LENGTH` Bytes.
|
||||
mpeg4_url (:obj:`str`): A valid URL for the MP4 file.
|
||||
mpeg4_url (:obj:`str`): A valid URL for the MP4 file. File size must not exceed 1MB.
|
||||
mpeg4_width (:obj:`int`, optional): Video width.
|
||||
mpeg4_height (:obj:`int`, optional): Video height.
|
||||
mpeg4_duration (:obj:`int`, optional): Video duration in seconds.
|
||||
@@ -88,7 +88,7 @@ class InlineQueryResultMpeg4Gif(InlineQueryResult):
|
||||
id (:obj:`str`): Unique identifier for this result,
|
||||
:tg-const:`telegram.InlineQueryResult.MIN_ID_LENGTH`-
|
||||
:tg-const:`telegram.InlineQueryResult.MAX_ID_LENGTH` Bytes.
|
||||
mpeg4_url (:obj:`str`): A valid URL for the MP4 file.
|
||||
mpeg4_url (:obj:`str`): A valid URL for the MP4 file. File size must not exceed 1MB.
|
||||
mpeg4_width (:obj:`int`): Optional. Video width.
|
||||
mpeg4_height (:obj:`int`): Optional. Video height.
|
||||
mpeg4_duration (:obj:`int`): Optional. Video duration in seconds.
|
||||
|
||||
@@ -47,10 +47,9 @@ class InlineQueryResultsButton(TelegramObject):
|
||||
inside the Web App.
|
||||
start_parameter (:obj:`str`, optional): Deep-linking parameter for the
|
||||
:guilabel:`/start` message sent to the bot when user presses the switch button.
|
||||
:tg-const:`telegram.constants.InlineQueryResultsButtonLimit.MIN_START_PARAMETER_LENGTH`
|
||||
-
|
||||
:tg-const:`telegram.constants.InlineQueryResultsButtonLimit.MAX_START_PARAMETER_LENGTH`
|
||||
characters, only ``A-Z``, ``a-z``, ``0-9``, ``_`` and ``-`` are allowed.
|
||||
:tg-const:`telegram.InlineQuery.MIN_SWITCH_PM_TEXT_LENGTH`-
|
||||
:tg-const:`telegram.InlineQuery.MAX_SWITCH_PM_TEXT_LENGTH` characters,
|
||||
only ``A-Z``, ``a-z``, ``0-9``, ``_`` and ``-`` are allowed.
|
||||
|
||||
Example:
|
||||
An inline bot that sends YouTube videos can ask the user to connect the bot to
|
||||
@@ -68,10 +67,10 @@ class InlineQueryResultsButton(TelegramObject):
|
||||
user presses the button. The Web App will be able to switch back to the inline mode
|
||||
using the method ``web_app_switch_inline_query`` inside the Web App.
|
||||
start_parameter (:obj:`str`): Optional. Deep-linking parameter for the
|
||||
:tg-const:`telegram.constants.InlineQueryResultsButtonLimit.MIN_START_PARAMETER_LENGTH`
|
||||
-
|
||||
:tg-const:`telegram.constants.InlineQueryResultsButtonLimit.MAX_START_PARAMETER_LENGTH`
|
||||
characters, only ``A-Z``, ``a-z``, ``0-9``, ``_`` and ``-`` are allowed.
|
||||
:guilabel:`/start` message sent to the bot when user presses the switch button.
|
||||
:tg-const:`telegram.InlineQuery.MIN_SWITCH_PM_TEXT_LENGTH`-
|
||||
:tg-const:`telegram.InlineQuery.MAX_SWITCH_PM_TEXT_LENGTH` characters,
|
||||
only ``A-Z``, ``a-z``, ``0-9``, ``_`` and ``-`` are allowed.
|
||||
|
||||
"""
|
||||
|
||||
|
||||
@@ -35,11 +35,9 @@ class InputInvoiceMessageContent(InputMessageContent):
|
||||
|
||||
Objects of this class are comparable in terms of equality. Two objects of this class are
|
||||
considered equal, if their :attr:`title`, :attr:`description`, :attr:`payload`,
|
||||
:attr:`currency` and :attr:`prices` are equal.
|
||||
:attr:`provider_token`, :attr:`currency` and :attr:`prices` are equal.
|
||||
|
||||
.. versionadded:: 13.5
|
||||
.. versionchanged:: 21.11
|
||||
:attr:`provider_token` is no longer considered for equality comparison.
|
||||
|
||||
Args:
|
||||
title (:obj:`str`): Product name. :tg-const:`telegram.Invoice.MIN_TITLE_LENGTH`-
|
||||
@@ -51,13 +49,13 @@ class InputInvoiceMessageContent(InputMessageContent):
|
||||
:tg-const:`telegram.Invoice.MIN_PAYLOAD_LENGTH`-
|
||||
:tg-const:`telegram.Invoice.MAX_PAYLOAD_LENGTH` bytes. This will not be displayed
|
||||
to the user, use it for your internal processes.
|
||||
provider_token (:obj:`str`, optional): Payment provider token, obtained via
|
||||
provider_token (:obj:`str`): Payment provider token, obtained via
|
||||
`@Botfather <https://t.me/Botfather>`_. Pass an empty string for payments in
|
||||
|tg_stars|.
|
||||
|
||||
.. versionchanged:: 21.11
|
||||
Bot API 7.4 made this parameter is optional and this is now reflected in the
|
||||
class signature.
|
||||
.. deprecated:: 21.3
|
||||
As of Bot API 7.4, this parameter is now optional and future versions of the
|
||||
library will make it optional as well.
|
||||
currency (:obj:`str`): Three-letter ISO 4217 currency code, see more on
|
||||
`currencies <https://core.telegram.org/bots/payments#supported-currencies>`_.
|
||||
Pass ``XTR`` for payments in |tg_stars|.
|
||||
@@ -201,9 +199,9 @@ class InputInvoiceMessageContent(InputMessageContent):
|
||||
title: str,
|
||||
description: str,
|
||||
payload: str,
|
||||
provider_token: Optional[str], # This arg is now optional since Bot API 7.4
|
||||
currency: str,
|
||||
prices: Sequence[LabeledPrice],
|
||||
provider_token: Optional[str] = None,
|
||||
max_tip_amount: Optional[int] = None,
|
||||
suggested_tip_amounts: Optional[Sequence[int]] = None,
|
||||
provider_data: Optional[str] = None,
|
||||
@@ -227,10 +225,10 @@ class InputInvoiceMessageContent(InputMessageContent):
|
||||
self.title: str = title
|
||||
self.description: str = description
|
||||
self.payload: str = payload
|
||||
self.provider_token: Optional[str] = provider_token
|
||||
self.currency: str = currency
|
||||
self.prices: tuple[LabeledPrice, ...] = parse_sequence_arg(prices)
|
||||
# Optionals
|
||||
self.provider_token: Optional[str] = provider_token
|
||||
self.max_tip_amount: Optional[int] = max_tip_amount
|
||||
self.suggested_tip_amounts: tuple[int, ...] = parse_sequence_arg(suggested_tip_amounts)
|
||||
self.provider_data: Optional[str] = provider_data
|
||||
@@ -250,6 +248,7 @@ class InputInvoiceMessageContent(InputMessageContent):
|
||||
self.title,
|
||||
self.description,
|
||||
self.payload,
|
||||
self.provider_token,
|
||||
self.currency,
|
||||
self.prices,
|
||||
)
|
||||
|
||||
+157
-107
@@ -930,9 +930,6 @@ class Message(MaybeInaccessibleMessage):
|
||||
|
||||
.. |reply_same_thread| replace:: If :paramref:`message_thread_id` is not provided,
|
||||
this will reply to the same thread (topic) of the original message.
|
||||
|
||||
.. |quote_removed| replace:: Removed deprecated parameter ``quote``. Use :paramref:`do_quote`
|
||||
instead.
|
||||
"""
|
||||
|
||||
# fmt: on
|
||||
@@ -1487,17 +1484,22 @@ class Message(MaybeInaccessibleMessage):
|
||||
|
||||
return self._effective_attachment # type: ignore[return-value]
|
||||
|
||||
def _do_quote(self, do_quote: Optional[bool]) -> Optional[ReplyParameters]:
|
||||
def _quote(
|
||||
self, quote: Optional[bool], reply_to_message_id: Optional[int] = None
|
||||
) -> Optional[ReplyParameters]:
|
||||
"""Modify kwargs for replying with or without quoting."""
|
||||
if do_quote is not None:
|
||||
if do_quote:
|
||||
if reply_to_message_id is not None:
|
||||
return ReplyParameters(reply_to_message_id)
|
||||
|
||||
if quote is not None:
|
||||
if quote:
|
||||
return ReplyParameters(self.message_id)
|
||||
|
||||
else:
|
||||
# Unfortunately we need some ExtBot logic here because it's hard to move shortcut
|
||||
# logic into ExtBot
|
||||
if hasattr(self.get_bot(), "defaults") and self.get_bot().defaults: # type: ignore
|
||||
default_quote = self.get_bot().defaults.do_quote # type: ignore[attr-defined]
|
||||
default_quote = self.get_bot().defaults.quote # type: ignore[attr-defined]
|
||||
else:
|
||||
default_quote = None
|
||||
if (default_quote is None and self.chat.type != Chat.PRIVATE) or default_quote:
|
||||
@@ -1673,14 +1675,29 @@ class Message(MaybeInaccessibleMessage):
|
||||
async def _parse_quote_arguments(
|
||||
self,
|
||||
do_quote: Optional[Union[bool, _ReplyKwargs]],
|
||||
quote: Optional[bool],
|
||||
reply_to_message_id: Optional[int],
|
||||
reply_parameters: Optional["ReplyParameters"],
|
||||
) -> tuple[Union[str, int], ReplyParameters]:
|
||||
if quote and do_quote:
|
||||
raise ValueError("The arguments `quote` and `do_quote` are mutually exclusive")
|
||||
|
||||
if reply_to_message_id is not None and reply_parameters is not None:
|
||||
raise ValueError(
|
||||
"`reply_to_message_id` and `reply_parameters` are mutually exclusive."
|
||||
)
|
||||
|
||||
if quote is not None:
|
||||
warn(
|
||||
PTBDeprecationWarning(
|
||||
"20.8",
|
||||
"The `quote` parameter is deprecated in favor of the `do_quote` parameter. "
|
||||
"Please update your code to use `do_quote` instead.",
|
||||
),
|
||||
stacklevel=2,
|
||||
)
|
||||
|
||||
effective_do_quote = quote or do_quote
|
||||
chat_id: Union[str, int] = self.chat_id
|
||||
|
||||
# reply_parameters and reply_to_message_id overrule the do_quote parameter
|
||||
@@ -1688,11 +1705,11 @@ class Message(MaybeInaccessibleMessage):
|
||||
effective_reply_parameters = reply_parameters
|
||||
elif reply_to_message_id is not None:
|
||||
effective_reply_parameters = ReplyParameters(message_id=reply_to_message_id)
|
||||
elif isinstance(do_quote, dict):
|
||||
effective_reply_parameters = do_quote["reply_parameters"]
|
||||
chat_id = do_quote["chat_id"]
|
||||
elif isinstance(effective_do_quote, dict):
|
||||
effective_reply_parameters = effective_do_quote["reply_parameters"]
|
||||
chat_id = effective_do_quote["chat_id"]
|
||||
else:
|
||||
effective_reply_parameters = self._do_quote(do_quote)
|
||||
effective_reply_parameters = self._quote(effective_do_quote)
|
||||
|
||||
return chat_id, effective_reply_parameters
|
||||
|
||||
@@ -1733,6 +1750,7 @@ class Message(MaybeInaccessibleMessage):
|
||||
reply_to_message_id: Optional[int] = None,
|
||||
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
|
||||
disable_web_page_preview: Optional[bool] = None,
|
||||
quote: Optional[bool] = None,
|
||||
do_quote: Optional[Union[bool, _ReplyKwargs]] = None,
|
||||
read_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
write_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
@@ -1755,10 +1773,11 @@ class Message(MaybeInaccessibleMessage):
|
||||
.. versionchanged:: 21.1
|
||||
|reply_same_thread|
|
||||
|
||||
.. versionchanged:: 22.0
|
||||
|quote_removed|
|
||||
|
||||
Keyword Args:
|
||||
quote (:obj:`bool`, optional): |reply_quote|
|
||||
|
||||
.. deprecated:: 20.8
|
||||
This argument is deprecated in favor of :paramref:`do_quote`
|
||||
do_quote (:obj:`bool` | :obj:`dict`, optional): |do_quote|
|
||||
Mutually exclusive with :paramref:`quote`.
|
||||
|
||||
@@ -1769,7 +1788,7 @@ class Message(MaybeInaccessibleMessage):
|
||||
|
||||
"""
|
||||
chat_id, effective_reply_parameters = await self._parse_quote_arguments(
|
||||
do_quote, reply_to_message_id, reply_parameters
|
||||
do_quote, quote, reply_to_message_id, reply_parameters
|
||||
)
|
||||
message_thread_id = self._parse_message_thread_id(chat_id, message_thread_id)
|
||||
return await self.get_bot().send_message(
|
||||
@@ -1811,6 +1830,7 @@ class Message(MaybeInaccessibleMessage):
|
||||
reply_to_message_id: Optional[int] = None,
|
||||
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
|
||||
disable_web_page_preview: Optional[bool] = None,
|
||||
quote: Optional[bool] = None,
|
||||
do_quote: Optional[Union[bool, _ReplyKwargs]] = None,
|
||||
read_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
write_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
@@ -1836,14 +1856,15 @@ class Message(MaybeInaccessibleMessage):
|
||||
.. versionchanged:: 21.1
|
||||
|reply_same_thread|
|
||||
|
||||
.. versionchanged:: 22.0
|
||||
|quote_removed|
|
||||
|
||||
Note:
|
||||
:tg-const:`telegram.constants.ParseMode.MARKDOWN` is a legacy mode, retained by
|
||||
Telegram for backward compatibility. You should use :meth:`reply_markdown_v2` instead.
|
||||
|
||||
Keyword Args:
|
||||
quote (:obj:`bool`, optional): |reply_quote|
|
||||
|
||||
.. deprecated:: 20.8
|
||||
This argument is deprecated in favor of :paramref:`do_quote`
|
||||
do_quote (:obj:`bool` | :obj:`dict`, optional): |do_quote|
|
||||
Mutually exclusive with :paramref:`quote`.
|
||||
|
||||
@@ -1853,7 +1874,7 @@ class Message(MaybeInaccessibleMessage):
|
||||
:class:`telegram.Message`: On success, instance representing the message posted.
|
||||
"""
|
||||
chat_id, effective_reply_parameters = await self._parse_quote_arguments(
|
||||
do_quote, reply_to_message_id, reply_parameters
|
||||
do_quote, quote, reply_to_message_id, reply_parameters
|
||||
)
|
||||
message_thread_id = self._parse_message_thread_id(chat_id, message_thread_id)
|
||||
return await self.get_bot().send_message(
|
||||
@@ -1895,6 +1916,7 @@ class Message(MaybeInaccessibleMessage):
|
||||
reply_to_message_id: Optional[int] = None,
|
||||
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
|
||||
disable_web_page_preview: Optional[bool] = None,
|
||||
quote: Optional[bool] = None,
|
||||
do_quote: Optional[Union[bool, _ReplyKwargs]] = None,
|
||||
read_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
write_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
@@ -1920,10 +1942,11 @@ class Message(MaybeInaccessibleMessage):
|
||||
.. versionchanged:: 21.1
|
||||
|reply_same_thread|
|
||||
|
||||
.. versionchanged:: 22.0
|
||||
|quote_removed|
|
||||
|
||||
Keyword Args:
|
||||
quote (:obj:`bool`, optional): |reply_quote|
|
||||
|
||||
.. deprecated:: 20.8
|
||||
This argument is deprecated in favor of :paramref:`do_quote`
|
||||
do_quote (:obj:`bool` | :obj:`dict`, optional): |do_quote|
|
||||
Mutually exclusive with :paramref:`quote`.
|
||||
|
||||
@@ -1933,7 +1956,7 @@ class Message(MaybeInaccessibleMessage):
|
||||
:class:`telegram.Message`: On success, instance representing the message posted.
|
||||
"""
|
||||
chat_id, effective_reply_parameters = await self._parse_quote_arguments(
|
||||
do_quote, reply_to_message_id, reply_parameters
|
||||
do_quote, quote, reply_to_message_id, reply_parameters
|
||||
)
|
||||
message_thread_id = self._parse_message_thread_id(chat_id, message_thread_id)
|
||||
return await self.get_bot().send_message(
|
||||
@@ -1975,6 +1998,7 @@ class Message(MaybeInaccessibleMessage):
|
||||
reply_to_message_id: Optional[int] = None,
|
||||
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
|
||||
disable_web_page_preview: Optional[bool] = None,
|
||||
quote: Optional[bool] = None,
|
||||
do_quote: Optional[Union[bool, _ReplyKwargs]] = None,
|
||||
read_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
write_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
@@ -2000,10 +2024,11 @@ class Message(MaybeInaccessibleMessage):
|
||||
.. versionchanged:: 21.1
|
||||
|reply_same_thread|
|
||||
|
||||
.. versionchanged:: 22.0
|
||||
|quote_removed|
|
||||
|
||||
Keyword Args:
|
||||
quote (:obj:`bool`, optional): |reply_quote|
|
||||
|
||||
.. deprecated:: 20.8
|
||||
This argument is deprecated in favor of :paramref:`do_quote`
|
||||
do_quote (:obj:`bool` | :obj:`dict`, optional): |do_quote|
|
||||
Mutually exclusive with :paramref:`quote`.
|
||||
|
||||
@@ -2013,7 +2038,7 @@ class Message(MaybeInaccessibleMessage):
|
||||
:class:`telegram.Message`: On success, instance representing the message posted.
|
||||
"""
|
||||
chat_id, effective_reply_parameters = await self._parse_quote_arguments(
|
||||
do_quote, reply_to_message_id, reply_parameters
|
||||
do_quote, quote, reply_to_message_id, reply_parameters
|
||||
)
|
||||
message_thread_id = self._parse_message_thread_id(chat_id, message_thread_id)
|
||||
return await self.get_bot().send_message(
|
||||
@@ -2053,6 +2078,7 @@ class Message(MaybeInaccessibleMessage):
|
||||
*,
|
||||
reply_to_message_id: Optional[int] = None,
|
||||
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
|
||||
quote: Optional[bool] = None,
|
||||
do_quote: Optional[Union[bool, _ReplyKwargs]] = None,
|
||||
read_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
write_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
@@ -2078,10 +2104,11 @@ class Message(MaybeInaccessibleMessage):
|
||||
.. versionchanged:: 21.1
|
||||
|reply_same_thread|
|
||||
|
||||
.. versionchanged:: 22.0
|
||||
|quote_removed|
|
||||
|
||||
Keyword Args:
|
||||
quote (:obj:`bool`, optional): |reply_quote|
|
||||
|
||||
.. deprecated:: 20.8
|
||||
This argument is deprecated in favor of :paramref:`do_quote`
|
||||
do_quote (:obj:`bool` | :obj:`dict`, optional): |do_quote|
|
||||
Mutually exclusive with :paramref:`quote`.
|
||||
|
||||
@@ -2094,7 +2121,7 @@ class Message(MaybeInaccessibleMessage):
|
||||
:class:`telegram.error.TelegramError`
|
||||
"""
|
||||
chat_id, effective_reply_parameters = await self._parse_quote_arguments(
|
||||
do_quote, reply_to_message_id, reply_parameters
|
||||
do_quote, quote, reply_to_message_id, reply_parameters
|
||||
)
|
||||
message_thread_id = self._parse_message_thread_id(chat_id, message_thread_id)
|
||||
return await self.get_bot().send_media_group(
|
||||
@@ -2137,6 +2164,7 @@ class Message(MaybeInaccessibleMessage):
|
||||
reply_to_message_id: Optional[int] = None,
|
||||
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
|
||||
filename: Optional[str] = None,
|
||||
quote: Optional[bool] = None,
|
||||
do_quote: Optional[Union[bool, _ReplyKwargs]] = None,
|
||||
read_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
write_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
@@ -2159,10 +2187,11 @@ class Message(MaybeInaccessibleMessage):
|
||||
.. versionchanged:: 21.1
|
||||
|reply_same_thread|
|
||||
|
||||
.. versionchanged:: 22.0
|
||||
|quote_removed|
|
||||
|
||||
Keyword Args:
|
||||
quote (:obj:`bool`, optional): |reply_quote|
|
||||
|
||||
.. deprecated:: 20.8
|
||||
This argument is deprecated in favor of :paramref:`do_quote`
|
||||
do_quote (:obj:`bool` | :obj:`dict`, optional): |do_quote|
|
||||
Mutually exclusive with :paramref:`quote`.
|
||||
|
||||
@@ -2173,7 +2202,7 @@ class Message(MaybeInaccessibleMessage):
|
||||
|
||||
"""
|
||||
chat_id, effective_reply_parameters = await self._parse_quote_arguments(
|
||||
do_quote, reply_to_message_id, reply_parameters
|
||||
do_quote, quote, reply_to_message_id, reply_parameters
|
||||
)
|
||||
message_thread_id = self._parse_message_thread_id(chat_id, message_thread_id)
|
||||
return await self.get_bot().send_photo(
|
||||
@@ -2222,6 +2251,7 @@ class Message(MaybeInaccessibleMessage):
|
||||
reply_to_message_id: Optional[int] = None,
|
||||
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
|
||||
filename: Optional[str] = None,
|
||||
quote: Optional[bool] = None,
|
||||
do_quote: Optional[Union[bool, _ReplyKwargs]] = None,
|
||||
read_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
write_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
@@ -2244,10 +2274,11 @@ class Message(MaybeInaccessibleMessage):
|
||||
.. versionchanged:: 21.1
|
||||
|reply_same_thread|
|
||||
|
||||
.. versionchanged:: 22.0
|
||||
|quote_removed|
|
||||
|
||||
Keyword Args:
|
||||
quote (:obj:`bool`, optional): |reply_quote|
|
||||
|
||||
.. deprecated:: 20.8
|
||||
This argument is deprecated in favor of :paramref:`do_quote`
|
||||
do_quote (:obj:`bool` | :obj:`dict`, optional): |do_quote|
|
||||
Mutually exclusive with :paramref:`quote`.
|
||||
|
||||
@@ -2258,7 +2289,7 @@ class Message(MaybeInaccessibleMessage):
|
||||
|
||||
"""
|
||||
chat_id, effective_reply_parameters = await self._parse_quote_arguments(
|
||||
do_quote, reply_to_message_id, reply_parameters
|
||||
do_quote, quote, reply_to_message_id, reply_parameters
|
||||
)
|
||||
message_thread_id = self._parse_message_thread_id(chat_id, message_thread_id)
|
||||
return await self.get_bot().send_audio(
|
||||
@@ -2307,6 +2338,7 @@ class Message(MaybeInaccessibleMessage):
|
||||
reply_to_message_id: Optional[int] = None,
|
||||
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
|
||||
filename: Optional[str] = None,
|
||||
quote: Optional[bool] = None,
|
||||
do_quote: Optional[Union[bool, _ReplyKwargs]] = None,
|
||||
read_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
write_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
@@ -2329,10 +2361,11 @@ class Message(MaybeInaccessibleMessage):
|
||||
.. versionchanged:: 21.1
|
||||
|reply_same_thread|
|
||||
|
||||
.. versionchanged:: 22.0
|
||||
|quote_removed|
|
||||
|
||||
Keyword Args:
|
||||
quote (:obj:`bool`, optional): |reply_quote|
|
||||
|
||||
.. deprecated:: 20.8
|
||||
This argument is deprecated in favor of :paramref:`do_quote`
|
||||
do_quote (:obj:`bool` | :obj:`dict`, optional): |do_quote|
|
||||
Mutually exclusive with :paramref:`quote`.
|
||||
|
||||
@@ -2343,7 +2376,7 @@ class Message(MaybeInaccessibleMessage):
|
||||
|
||||
"""
|
||||
chat_id, effective_reply_parameters = await self._parse_quote_arguments(
|
||||
do_quote, reply_to_message_id, reply_parameters
|
||||
do_quote, quote, reply_to_message_id, reply_parameters
|
||||
)
|
||||
message_thread_id = self._parse_message_thread_id(chat_id, message_thread_id)
|
||||
return await self.get_bot().send_document(
|
||||
@@ -2394,6 +2427,7 @@ class Message(MaybeInaccessibleMessage):
|
||||
reply_to_message_id: Optional[int] = None,
|
||||
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
|
||||
filename: Optional[str] = None,
|
||||
quote: Optional[bool] = None,
|
||||
do_quote: Optional[Union[bool, _ReplyKwargs]] = None,
|
||||
read_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
write_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
@@ -2416,10 +2450,11 @@ class Message(MaybeInaccessibleMessage):
|
||||
.. versionchanged:: 21.1
|
||||
|reply_same_thread|
|
||||
|
||||
.. versionchanged:: 22.0
|
||||
|quote_removed|
|
||||
|
||||
Keyword Args:
|
||||
quote (:obj:`bool`, optional): |reply_quote|
|
||||
|
||||
.. deprecated:: 20.8
|
||||
This argument is deprecated in favor of :paramref:`do_quote`
|
||||
do_quote (:obj:`bool` | :obj:`dict`, optional): |do_quote|
|
||||
Mutually exclusive with :paramref:`quote`.
|
||||
|
||||
@@ -2430,7 +2465,7 @@ class Message(MaybeInaccessibleMessage):
|
||||
|
||||
"""
|
||||
chat_id, effective_reply_parameters = await self._parse_quote_arguments(
|
||||
do_quote, reply_to_message_id, reply_parameters
|
||||
do_quote, quote, reply_to_message_id, reply_parameters
|
||||
)
|
||||
message_thread_id = self._parse_message_thread_id(chat_id, message_thread_id)
|
||||
return await self.get_bot().send_animation(
|
||||
@@ -2476,6 +2511,7 @@ class Message(MaybeInaccessibleMessage):
|
||||
*,
|
||||
reply_to_message_id: Optional[int] = None,
|
||||
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
|
||||
quote: Optional[bool] = None,
|
||||
do_quote: Optional[Union[bool, _ReplyKwargs]] = None,
|
||||
read_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
write_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
@@ -2498,10 +2534,11 @@ class Message(MaybeInaccessibleMessage):
|
||||
.. versionchanged:: 21.1
|
||||
|reply_same_thread|
|
||||
|
||||
.. versionchanged:: 22.0
|
||||
|quote_removed|
|
||||
|
||||
Keyword Args:
|
||||
quote (:obj:`bool`, optional): |reply_quote|
|
||||
|
||||
.. deprecated:: 20.8
|
||||
This argument is deprecated in favor of :paramref:`do_quote`
|
||||
do_quote (:obj:`bool` | :obj:`dict`, optional): |do_quote|
|
||||
Mutually exclusive with :paramref:`quote`.
|
||||
|
||||
@@ -2512,7 +2549,7 @@ class Message(MaybeInaccessibleMessage):
|
||||
|
||||
"""
|
||||
chat_id, effective_reply_parameters = await self._parse_quote_arguments(
|
||||
do_quote, reply_to_message_id, reply_parameters
|
||||
do_quote, quote, reply_to_message_id, reply_parameters
|
||||
)
|
||||
message_thread_id = self._parse_message_thread_id(chat_id, message_thread_id)
|
||||
return await self.get_bot().send_sticker(
|
||||
@@ -2555,12 +2592,11 @@ class Message(MaybeInaccessibleMessage):
|
||||
message_effect_id: Optional[str] = None,
|
||||
allow_paid_broadcast: Optional[bool] = None,
|
||||
show_caption_above_media: Optional[bool] = None,
|
||||
cover: Optional[FileInput] = None,
|
||||
start_timestamp: Optional[int] = None,
|
||||
*,
|
||||
reply_to_message_id: Optional[int] = None,
|
||||
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
|
||||
filename: Optional[str] = None,
|
||||
quote: Optional[bool] = None,
|
||||
do_quote: Optional[Union[bool, _ReplyKwargs]] = None,
|
||||
read_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
write_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
@@ -2583,10 +2619,11 @@ class Message(MaybeInaccessibleMessage):
|
||||
.. versionchanged:: 21.1
|
||||
|reply_same_thread|
|
||||
|
||||
.. versionchanged:: 22.0
|
||||
|quote_removed|
|
||||
|
||||
Keyword Args:
|
||||
quote (:obj:`bool`, optional): |reply_quote|
|
||||
|
||||
.. deprecated:: 20.8
|
||||
This argument is deprecated in favor of :paramref:`do_quote`
|
||||
do_quote (:obj:`bool` | :obj:`dict`, optional): |do_quote|
|
||||
Mutually exclusive with :paramref:`quote`.
|
||||
|
||||
@@ -2597,7 +2634,7 @@ class Message(MaybeInaccessibleMessage):
|
||||
|
||||
"""
|
||||
chat_id, effective_reply_parameters = await self._parse_quote_arguments(
|
||||
do_quote, reply_to_message_id, reply_parameters
|
||||
do_quote, quote, reply_to_message_id, reply_parameters
|
||||
)
|
||||
message_thread_id = self._parse_message_thread_id(chat_id, message_thread_id)
|
||||
return await self.get_bot().send_video(
|
||||
@@ -2624,8 +2661,6 @@ class Message(MaybeInaccessibleMessage):
|
||||
message_thread_id=message_thread_id,
|
||||
has_spoiler=has_spoiler,
|
||||
thumbnail=thumbnail,
|
||||
cover=cover,
|
||||
start_timestamp=start_timestamp,
|
||||
business_connection_id=self.business_connection_id,
|
||||
message_effect_id=message_effect_id,
|
||||
allow_paid_broadcast=allow_paid_broadcast,
|
||||
@@ -2649,6 +2684,7 @@ class Message(MaybeInaccessibleMessage):
|
||||
reply_to_message_id: Optional[int] = None,
|
||||
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
|
||||
filename: Optional[str] = None,
|
||||
quote: Optional[bool] = None,
|
||||
do_quote: Optional[Union[bool, _ReplyKwargs]] = None,
|
||||
read_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
write_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
@@ -2671,10 +2707,11 @@ class Message(MaybeInaccessibleMessage):
|
||||
.. versionchanged:: 21.1
|
||||
|reply_same_thread|
|
||||
|
||||
.. versionchanged:: 22.0
|
||||
|quote_removed|
|
||||
|
||||
Keyword Args:
|
||||
quote (:obj:`bool`, optional): |reply_quote|
|
||||
|
||||
.. deprecated:: 20.8
|
||||
This argument is deprecated in favor of :paramref:`do_quote`
|
||||
do_quote (:obj:`bool` | :obj:`dict`, optional): |do_quote|
|
||||
Mutually exclusive with :paramref:`quote`.
|
||||
|
||||
@@ -2685,7 +2722,7 @@ class Message(MaybeInaccessibleMessage):
|
||||
|
||||
"""
|
||||
chat_id, effective_reply_parameters = await self._parse_quote_arguments(
|
||||
do_quote, reply_to_message_id, reply_parameters
|
||||
do_quote, quote, reply_to_message_id, reply_parameters
|
||||
)
|
||||
message_thread_id = self._parse_message_thread_id(chat_id, message_thread_id)
|
||||
return await self.get_bot().send_video_note(
|
||||
@@ -2729,6 +2766,7 @@ class Message(MaybeInaccessibleMessage):
|
||||
reply_to_message_id: Optional[int] = None,
|
||||
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
|
||||
filename: Optional[str] = None,
|
||||
quote: Optional[bool] = None,
|
||||
do_quote: Optional[Union[bool, _ReplyKwargs]] = None,
|
||||
read_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
write_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
@@ -2751,10 +2789,11 @@ class Message(MaybeInaccessibleMessage):
|
||||
.. versionchanged:: 21.1
|
||||
|reply_same_thread|
|
||||
|
||||
.. versionchanged:: 22.0
|
||||
|quote_removed|
|
||||
|
||||
Keyword Args:
|
||||
quote (:obj:`bool`, optional): |reply_quote|
|
||||
|
||||
.. deprecated:: 20.8
|
||||
This argument is deprecated in favor of :paramref:`do_quote`
|
||||
do_quote (:obj:`bool` | :obj:`dict`, optional): |do_quote|
|
||||
Mutually exclusive with :paramref:`quote`.
|
||||
|
||||
@@ -2765,7 +2804,7 @@ class Message(MaybeInaccessibleMessage):
|
||||
|
||||
"""
|
||||
chat_id, effective_reply_parameters = await self._parse_quote_arguments(
|
||||
do_quote, reply_to_message_id, reply_parameters
|
||||
do_quote, quote, reply_to_message_id, reply_parameters
|
||||
)
|
||||
message_thread_id = self._parse_message_thread_id(chat_id, message_thread_id)
|
||||
return await self.get_bot().send_voice(
|
||||
@@ -2811,6 +2850,7 @@ class Message(MaybeInaccessibleMessage):
|
||||
reply_to_message_id: Optional[int] = None,
|
||||
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
|
||||
location: Optional[Location] = None,
|
||||
quote: Optional[bool] = None,
|
||||
do_quote: Optional[Union[bool, _ReplyKwargs]] = None,
|
||||
read_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
write_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
@@ -2833,10 +2873,11 @@ class Message(MaybeInaccessibleMessage):
|
||||
.. versionchanged:: 21.1
|
||||
|reply_same_thread|
|
||||
|
||||
.. versionchanged:: 22.0
|
||||
|quote_removed|
|
||||
|
||||
Keyword Args:
|
||||
quote (:obj:`bool`, optional): |reply_quote|
|
||||
|
||||
.. deprecated:: 20.8
|
||||
This argument is deprecated in favor of :paramref:`do_quote`
|
||||
do_quote (:obj:`bool` | :obj:`dict`, optional): |do_quote|
|
||||
Mutually exclusive with :paramref:`quote`.
|
||||
|
||||
@@ -2847,7 +2888,7 @@ class Message(MaybeInaccessibleMessage):
|
||||
|
||||
"""
|
||||
chat_id, effective_reply_parameters = await self._parse_quote_arguments(
|
||||
do_quote, reply_to_message_id, reply_parameters
|
||||
do_quote, quote, reply_to_message_id, reply_parameters
|
||||
)
|
||||
message_thread_id = self._parse_message_thread_id(chat_id, message_thread_id)
|
||||
return await self.get_bot().send_location(
|
||||
@@ -2896,6 +2937,7 @@ class Message(MaybeInaccessibleMessage):
|
||||
reply_to_message_id: Optional[int] = None,
|
||||
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
|
||||
venue: Optional[Venue] = None,
|
||||
quote: Optional[bool] = None,
|
||||
do_quote: Optional[Union[bool, _ReplyKwargs]] = None,
|
||||
read_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
write_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
@@ -2918,10 +2960,11 @@ class Message(MaybeInaccessibleMessage):
|
||||
.. versionchanged:: 21.1
|
||||
|reply_same_thread|
|
||||
|
||||
.. versionchanged:: 22.0
|
||||
|quote_removed|
|
||||
|
||||
Keyword Args:
|
||||
quote (:obj:`bool`, optional): |reply_quote|
|
||||
|
||||
.. deprecated:: 20.8
|
||||
This argument is deprecated in favor of :paramref:`do_quote`
|
||||
do_quote (:obj:`bool` | :obj:`dict`, optional): |do_quote|
|
||||
Mutually exclusive with :paramref:`quote`.
|
||||
|
||||
@@ -2932,7 +2975,7 @@ class Message(MaybeInaccessibleMessage):
|
||||
|
||||
"""
|
||||
chat_id, effective_reply_parameters = await self._parse_quote_arguments(
|
||||
do_quote, reply_to_message_id, reply_parameters
|
||||
do_quote, quote, reply_to_message_id, reply_parameters
|
||||
)
|
||||
message_thread_id = self._parse_message_thread_id(chat_id, message_thread_id)
|
||||
return await self.get_bot().send_venue(
|
||||
@@ -2979,6 +3022,7 @@ class Message(MaybeInaccessibleMessage):
|
||||
reply_to_message_id: Optional[int] = None,
|
||||
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
|
||||
contact: Optional[Contact] = None,
|
||||
quote: Optional[bool] = None,
|
||||
do_quote: Optional[Union[bool, _ReplyKwargs]] = None,
|
||||
read_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
write_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
@@ -3001,10 +3045,11 @@ class Message(MaybeInaccessibleMessage):
|
||||
.. versionchanged:: 21.1
|
||||
|reply_same_thread|
|
||||
|
||||
.. versionchanged:: 22.0
|
||||
|quote_removed|
|
||||
|
||||
Keyword Args:
|
||||
quote (:obj:`bool`, optional): |reply_quote|
|
||||
|
||||
.. deprecated:: 20.8
|
||||
This argument is deprecated in favor of :paramref:`do_quote`
|
||||
do_quote (:obj:`bool` | :obj:`dict`, optional): |do_quote|
|
||||
Mutually exclusive with :paramref:`quote`.
|
||||
|
||||
@@ -3015,7 +3060,7 @@ class Message(MaybeInaccessibleMessage):
|
||||
|
||||
"""
|
||||
chat_id, effective_reply_parameters = await self._parse_quote_arguments(
|
||||
do_quote, reply_to_message_id, reply_parameters
|
||||
do_quote, quote, reply_to_message_id, reply_parameters
|
||||
)
|
||||
message_thread_id = self._parse_message_thread_id(chat_id, message_thread_id)
|
||||
return await self.get_bot().send_contact(
|
||||
@@ -3067,6 +3112,7 @@ class Message(MaybeInaccessibleMessage):
|
||||
*,
|
||||
reply_to_message_id: Optional[int] = None,
|
||||
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
|
||||
quote: Optional[bool] = None,
|
||||
do_quote: Optional[Union[bool, _ReplyKwargs]] = None,
|
||||
read_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
write_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
@@ -3089,10 +3135,11 @@ class Message(MaybeInaccessibleMessage):
|
||||
.. versionchanged:: 21.1
|
||||
|reply_same_thread|
|
||||
|
||||
.. versionchanged:: 22.0
|
||||
|quote_removed|
|
||||
|
||||
Keyword Args:
|
||||
quote (:obj:`bool`, optional): |reply_quote|
|
||||
|
||||
.. deprecated:: 20.8
|
||||
This argument is deprecated in favor of :paramref:`do_quote`
|
||||
do_quote (:obj:`bool` | :obj:`dict`, optional): |do_quote|
|
||||
Mutually exclusive with :paramref:`quote`.
|
||||
|
||||
@@ -3103,7 +3150,7 @@ class Message(MaybeInaccessibleMessage):
|
||||
|
||||
"""
|
||||
chat_id, effective_reply_parameters = await self._parse_quote_arguments(
|
||||
do_quote, reply_to_message_id, reply_parameters
|
||||
do_quote, quote, reply_to_message_id, reply_parameters
|
||||
)
|
||||
message_thread_id = self._parse_message_thread_id(chat_id, message_thread_id)
|
||||
return await self.get_bot().send_poll(
|
||||
@@ -3151,6 +3198,7 @@ class Message(MaybeInaccessibleMessage):
|
||||
*,
|
||||
reply_to_message_id: Optional[int] = None,
|
||||
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
|
||||
quote: Optional[bool] = None,
|
||||
do_quote: Optional[Union[bool, _ReplyKwargs]] = None,
|
||||
read_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
write_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
@@ -3173,10 +3221,11 @@ class Message(MaybeInaccessibleMessage):
|
||||
.. versionchanged:: 21.1
|
||||
|reply_same_thread|
|
||||
|
||||
.. versionchanged:: 22.0
|
||||
|quote_removed|
|
||||
|
||||
Keyword Args:
|
||||
quote (:obj:`bool`, optional): |reply_quote|
|
||||
|
||||
.. deprecated:: 20.8
|
||||
This argument is deprecated in favor of :paramref:`do_quote`
|
||||
do_quote (:obj:`bool` | :obj:`dict`, optional): |do_quote|
|
||||
Mutually exclusive with :paramref:`quote`.
|
||||
|
||||
@@ -3187,7 +3236,7 @@ class Message(MaybeInaccessibleMessage):
|
||||
|
||||
"""
|
||||
chat_id, effective_reply_parameters = await self._parse_quote_arguments(
|
||||
do_quote, reply_to_message_id, reply_parameters
|
||||
do_quote, quote, reply_to_message_id, reply_parameters
|
||||
)
|
||||
message_thread_id = self._parse_message_thread_id(chat_id, message_thread_id)
|
||||
return await self.get_bot().send_dice(
|
||||
@@ -3266,6 +3315,7 @@ class Message(MaybeInaccessibleMessage):
|
||||
*,
|
||||
reply_to_message_id: Optional[int] = None,
|
||||
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
|
||||
quote: Optional[bool] = None,
|
||||
do_quote: Optional[Union[bool, _ReplyKwargs]] = None,
|
||||
read_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
write_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
@@ -3288,10 +3338,11 @@ class Message(MaybeInaccessibleMessage):
|
||||
.. versionchanged:: 21.1
|
||||
|reply_same_thread|
|
||||
|
||||
.. versionchanged:: 22.0
|
||||
|quote_removed|
|
||||
|
||||
Keyword Args:
|
||||
quote (:obj:`bool`, optional): |reply_quote|
|
||||
|
||||
.. deprecated:: 20.8
|
||||
This argument is deprecated in favor of :paramref:`do_quote`
|
||||
do_quote (:obj:`bool` | :obj:`dict`, optional): |do_quote|
|
||||
Mutually exclusive with :paramref:`quote`.
|
||||
|
||||
@@ -3304,7 +3355,7 @@ class Message(MaybeInaccessibleMessage):
|
||||
|
||||
"""
|
||||
chat_id, effective_reply_parameters = await self._parse_quote_arguments(
|
||||
do_quote, reply_to_message_id, reply_parameters
|
||||
do_quote, quote, reply_to_message_id, reply_parameters
|
||||
)
|
||||
message_thread_id = self._parse_message_thread_id(chat_id, message_thread_id)
|
||||
return await self.get_bot().send_game(
|
||||
@@ -3331,9 +3382,9 @@ class Message(MaybeInaccessibleMessage):
|
||||
title: str,
|
||||
description: str,
|
||||
payload: str,
|
||||
provider_token: Optional[str],
|
||||
currency: str,
|
||||
prices: Sequence["LabeledPrice"],
|
||||
provider_token: Optional[str] = None,
|
||||
start_parameter: Optional[str] = None,
|
||||
photo_url: Optional[str] = None,
|
||||
photo_size: Optional[int] = None,
|
||||
@@ -3359,6 +3410,7 @@ class Message(MaybeInaccessibleMessage):
|
||||
*,
|
||||
reply_to_message_id: Optional[int] = None,
|
||||
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
|
||||
quote: Optional[bool] = None,
|
||||
do_quote: Optional[Union[bool, _ReplyKwargs]] = None,
|
||||
read_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
write_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
@@ -3380,9 +3432,6 @@ class Message(MaybeInaccessibleMessage):
|
||||
.. versionchanged:: 21.1
|
||||
|reply_same_thread|
|
||||
|
||||
.. versionchanged:: 22.0
|
||||
|quote_removed|
|
||||
|
||||
Warning:
|
||||
As of API 5.2 :paramref:`start_parameter <telegram.Bot.send_invoice.start_parameter>`
|
||||
is an optional argument and therefore the
|
||||
@@ -3396,6 +3445,10 @@ class Message(MaybeInaccessibleMessage):
|
||||
:paramref:`start_parameter <telegram.Bot.send_invoice.start_parameter>` is optional.
|
||||
|
||||
Keyword Args:
|
||||
quote (:obj:`bool`, optional): |reply_quote|
|
||||
|
||||
.. deprecated:: 20.8
|
||||
This argument is deprecated in favor of :paramref:`do_quote`
|
||||
do_quote (:obj:`bool` | :obj:`dict`, optional): |do_quote|
|
||||
Mutually exclusive with :paramref:`quote`.
|
||||
|
||||
@@ -3406,7 +3459,7 @@ class Message(MaybeInaccessibleMessage):
|
||||
|
||||
"""
|
||||
chat_id, effective_reply_parameters = await self._parse_quote_arguments(
|
||||
do_quote, reply_to_message_id, reply_parameters
|
||||
do_quote, quote, reply_to_message_id, reply_parameters
|
||||
)
|
||||
message_thread_id = self._parse_message_thread_id(chat_id, message_thread_id)
|
||||
return await self.get_bot().send_invoice(
|
||||
@@ -3453,7 +3506,6 @@ class Message(MaybeInaccessibleMessage):
|
||||
disable_notification: ODVInput[bool] = DEFAULT_NONE,
|
||||
protect_content: ODVInput[bool] = DEFAULT_NONE,
|
||||
message_thread_id: Optional[int] = None,
|
||||
video_start_timestamp: Optional[int] = None,
|
||||
*,
|
||||
read_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
write_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
@@ -3488,7 +3540,6 @@ class Message(MaybeInaccessibleMessage):
|
||||
chat_id=chat_id,
|
||||
from_chat_id=self.chat_id,
|
||||
message_id=self.message_id,
|
||||
video_start_timestamp=video_start_timestamp,
|
||||
disable_notification=disable_notification,
|
||||
protect_content=protect_content,
|
||||
message_thread_id=message_thread_id,
|
||||
@@ -3512,7 +3563,6 @@ class Message(MaybeInaccessibleMessage):
|
||||
reply_parameters: Optional["ReplyParameters"] = None,
|
||||
show_caption_above_media: Optional[bool] = None,
|
||||
allow_paid_broadcast: Optional[bool] = None,
|
||||
video_start_timestamp: Optional[int] = None,
|
||||
*,
|
||||
reply_to_message_id: Optional[int] = None,
|
||||
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
|
||||
@@ -3543,7 +3593,6 @@ class Message(MaybeInaccessibleMessage):
|
||||
from_chat_id=self.chat_id,
|
||||
message_id=self.message_id,
|
||||
caption=caption,
|
||||
video_start_timestamp=video_start_timestamp,
|
||||
parse_mode=parse_mode,
|
||||
caption_entities=caption_entities,
|
||||
disable_notification=disable_notification,
|
||||
@@ -3576,10 +3625,10 @@ class Message(MaybeInaccessibleMessage):
|
||||
reply_parameters: Optional["ReplyParameters"] = None,
|
||||
show_caption_above_media: Optional[bool] = None,
|
||||
allow_paid_broadcast: Optional[bool] = None,
|
||||
video_start_timestamp: Optional[int] = None,
|
||||
*,
|
||||
reply_to_message_id: Optional[int] = None,
|
||||
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
|
||||
quote: Optional[bool] = None,
|
||||
do_quote: Optional[Union[bool, _ReplyKwargs]] = None,
|
||||
read_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
write_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
@@ -3602,10 +3651,12 @@ class Message(MaybeInaccessibleMessage):
|
||||
.. versionchanged:: 21.1
|
||||
|reply_same_thread|
|
||||
|
||||
.. versionchanged:: 22.0
|
||||
|quote_removed|
|
||||
|
||||
Keyword Args:
|
||||
quote (:obj:`bool`, optional): |reply_quote|
|
||||
|
||||
.. versionadded:: 13.1
|
||||
.. deprecated:: 20.8
|
||||
This argument is deprecated in favor of :paramref:`do_quote`
|
||||
do_quote (:obj:`bool` | :obj:`dict`, optional): |do_quote|
|
||||
Mutually exclusive with :paramref:`quote`.
|
||||
|
||||
@@ -3616,7 +3667,7 @@ class Message(MaybeInaccessibleMessage):
|
||||
|
||||
"""
|
||||
chat_id, effective_reply_parameters = await self._parse_quote_arguments(
|
||||
do_quote, reply_to_message_id, reply_parameters
|
||||
do_quote, quote, reply_to_message_id, reply_parameters
|
||||
)
|
||||
message_thread_id = self._parse_message_thread_id(chat_id, message_thread_id)
|
||||
return await self.get_bot().copy_message(
|
||||
@@ -3624,7 +3675,6 @@ class Message(MaybeInaccessibleMessage):
|
||||
from_chat_id=from_chat_id,
|
||||
message_id=message_id,
|
||||
caption=caption,
|
||||
video_start_timestamp=video_start_timestamp,
|
||||
parse_mode=parse_mode,
|
||||
caption_entities=caption_entities,
|
||||
disable_notification=disable_notification,
|
||||
@@ -3688,7 +3738,7 @@ class Message(MaybeInaccessibleMessage):
|
||||
|
||||
"""
|
||||
chat_id, effective_reply_parameters = await self._parse_quote_arguments(
|
||||
do_quote, reply_to_message_id, reply_parameters
|
||||
do_quote, None, reply_to_message_id, reply_parameters
|
||||
)
|
||||
return await self.get_bot().send_paid_media(
|
||||
chat_id=chat_id,
|
||||
|
||||
@@ -164,6 +164,10 @@ class EncryptedPassportElement(TelegramObject):
|
||||
reverse_side: Optional[PassportFile] = None,
|
||||
selfie: Optional[PassportFile] = None,
|
||||
translation: Optional[Sequence[PassportFile]] = None,
|
||||
# TODO: Remove the credentials argument in 22.0 or later
|
||||
credentials: Optional[ # pylint: disable=unused-argument # noqa: ARG002
|
||||
"Credentials"
|
||||
] = None,
|
||||
*,
|
||||
api_kwargs: Optional[JSONDict] = None,
|
||||
):
|
||||
|
||||
@@ -19,12 +19,12 @@
|
||||
# pylint: disable=redefined-builtin
|
||||
"""This module contains the classes that represent Telegram PassportElementError."""
|
||||
|
||||
from collections.abc import Sequence
|
||||
from typing import Optional
|
||||
|
||||
from telegram._telegramobject import TelegramObject
|
||||
from telegram._utils.argumentparsing import parse_sequence_arg
|
||||
from telegram._utils.types import JSONDict
|
||||
from telegram._utils.warnings import warn
|
||||
from telegram.warnings import PTBDeprecationWarning
|
||||
|
||||
|
||||
class PassportElementError(TelegramObject):
|
||||
@@ -168,30 +168,23 @@ class PassportElementErrorFiles(PassportElementError):
|
||||
type (:obj:`str`): The section of the user's Telegram Passport which has the issue, one of
|
||||
``"utility_bill"``, ``"bank_statement"``, ``"rental_agreement"``,
|
||||
``"passport_registration"``, ``"temporary_registration"``.
|
||||
file_hashes (Sequence[:obj:`str`]): List of base64-encoded file hashes.
|
||||
|
||||
.. versionchanged:: 22.0
|
||||
|sequenceargs|
|
||||
file_hashes (list[:obj:`str`]): List of base64-encoded file hashes.
|
||||
message (:obj:`str`): Error message.
|
||||
|
||||
Attributes:
|
||||
type (:obj:`str`): The section of the user's Telegram Passport which has the issue, one of
|
||||
``"utility_bill"``, ``"bank_statement"``, ``"rental_agreement"``,
|
||||
``"passport_registration"``, ``"temporary_registration"``.
|
||||
file_hashes (tuple[:obj:`str`]): List of base64-encoded file hashes.
|
||||
|
||||
.. versionchanged:: 22.0
|
||||
|tupleclassattrs|
|
||||
message (:obj:`str`): Error message.
|
||||
|
||||
"""
|
||||
|
||||
__slots__ = ("file_hashes",)
|
||||
__slots__ = ("_file_hashes",)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
type: str,
|
||||
file_hashes: Sequence[str],
|
||||
file_hashes: list[str],
|
||||
message: str,
|
||||
*,
|
||||
api_kwargs: Optional[JSONDict] = None,
|
||||
@@ -199,9 +192,32 @@ class PassportElementErrorFiles(PassportElementError):
|
||||
# Required
|
||||
super().__init__("files", type, message, api_kwargs=api_kwargs)
|
||||
with self._unfrozen():
|
||||
self.file_hashes: tuple[str, ...] = parse_sequence_arg(file_hashes)
|
||||
self._file_hashes: list[str] = file_hashes
|
||||
|
||||
self._id_attrs = (self.source, self.type, self.message, self.file_hashes)
|
||||
self._id_attrs = (self.source, self.type, self.message, *tuple(file_hashes))
|
||||
|
||||
def to_dict(self, recursive: bool = True) -> JSONDict:
|
||||
"""See :meth:`telegram.TelegramObject.to_dict` for details."""
|
||||
data = super().to_dict(recursive)
|
||||
data["file_hashes"] = self._file_hashes
|
||||
return data
|
||||
|
||||
@property
|
||||
def file_hashes(self) -> list[str]:
|
||||
"""List of base64-encoded file hashes.
|
||||
|
||||
.. deprecated:: 20.6
|
||||
This attribute will return a tuple instead of a list in future major versions.
|
||||
"""
|
||||
warn(
|
||||
PTBDeprecationWarning(
|
||||
"20.6",
|
||||
"The attribute `file_hashes` will return a tuple instead of a list in future major"
|
||||
" versions.",
|
||||
),
|
||||
stacklevel=2,
|
||||
)
|
||||
return self._file_hashes
|
||||
|
||||
|
||||
class PassportElementErrorFrontSide(PassportElementError):
|
||||
@@ -370,10 +386,7 @@ class PassportElementErrorTranslationFiles(PassportElementError):
|
||||
one of ``"passport"``, ``"driver_license"``, ``"identity_card"``,
|
||||
``"internal_passport"``, ``"utility_bill"``, ``"bank_statement"``,
|
||||
``"rental_agreement"``, ``"passport_registration"``, ``"temporary_registration"``.
|
||||
file_hashes (Sequence[:obj:`str`]): List of base64-encoded file hashes.
|
||||
|
||||
.. versionchanged:: 22.0
|
||||
|sequenceargs|
|
||||
file_hashes (list[:obj:`str`]): List of base64-encoded file hashes.
|
||||
message (:obj:`str`): Error message.
|
||||
|
||||
Attributes:
|
||||
@@ -381,20 +394,16 @@ class PassportElementErrorTranslationFiles(PassportElementError):
|
||||
one of ``"passport"``, ``"driver_license"``, ``"identity_card"``,
|
||||
``"internal_passport"``, ``"utility_bill"``, ``"bank_statement"``,
|
||||
``"rental_agreement"``, ``"passport_registration"``, ``"temporary_registration"``.
|
||||
file_hashes (tuple[:obj:`str`]): List of base64-encoded file hashes.
|
||||
|
||||
.. versionchanged:: 22.0
|
||||
|tupleclassattrs|
|
||||
message (:obj:`str`): Error message.
|
||||
|
||||
"""
|
||||
|
||||
__slots__ = ("file_hashes",)
|
||||
__slots__ = ("_file_hashes",)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
type: str,
|
||||
file_hashes: Sequence[str],
|
||||
file_hashes: list[str],
|
||||
message: str,
|
||||
*,
|
||||
api_kwargs: Optional[JSONDict] = None,
|
||||
@@ -402,9 +411,33 @@ class PassportElementErrorTranslationFiles(PassportElementError):
|
||||
# Required
|
||||
super().__init__("translation_files", type, message, api_kwargs=api_kwargs)
|
||||
with self._unfrozen():
|
||||
self.file_hashes: tuple[str, ...] = parse_sequence_arg(file_hashes)
|
||||
self._file_hashes: list[str] = file_hashes
|
||||
|
||||
self._id_attrs = (self.source, self.type, self.message, self.file_hashes)
|
||||
self._id_attrs = (self.source, self.type, self.message, *tuple(file_hashes))
|
||||
|
||||
def to_dict(self, recursive: bool = True) -> JSONDict:
|
||||
"""See :meth:`telegram.TelegramObject.to_dict` for details."""
|
||||
data = super().to_dict(recursive)
|
||||
data["file_hashes"] = self._file_hashes
|
||||
return data
|
||||
|
||||
@property
|
||||
def file_hashes(self) -> list[str]:
|
||||
"""List of base64-encoded file hashes.
|
||||
|
||||
.. deprecated:: 20.6
|
||||
This attribute will return a tuple instead of a list in future major versions.
|
||||
"""
|
||||
warn(
|
||||
PTBDeprecationWarning(
|
||||
"20.6",
|
||||
"The attribute `file_hashes` will return a tuple instead of a list in future major"
|
||||
" versions. See the stability policy:"
|
||||
" https://docs.python-telegram-bot.org/en/stable/stability_policy.html",
|
||||
),
|
||||
stacklevel=2,
|
||||
)
|
||||
return self._file_hashes
|
||||
|
||||
|
||||
class PassportElementErrorUnspecified(PassportElementError):
|
||||
|
||||
@@ -18,13 +18,13 @@
|
||||
# along with this program. If not, see [http://www.gnu.org/licenses/].
|
||||
"""This module contains an object that represents a Encrypted PassportFile."""
|
||||
|
||||
import datetime as dtm
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
from telegram._telegramobject import TelegramObject
|
||||
from telegram._utils.datetime import extract_tzinfo_from_defaults, from_timestamp
|
||||
from telegram._utils.defaultvalue import DEFAULT_NONE
|
||||
from telegram._utils.types import JSONDict, ODVInput
|
||||
from telegram._utils.warnings import warn
|
||||
from telegram.warnings import PTBDeprecationWarning
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from telegram import Bot, File, FileCredentials
|
||||
@@ -45,11 +45,11 @@ class PassportFile(TelegramObject):
|
||||
is supposed to be the same over time and for different bots.
|
||||
Can't be used to download or reuse the file.
|
||||
file_size (:obj:`int`): File size in bytes.
|
||||
file_date (:class:`datetime.datetime`): Time when the file was uploaded.
|
||||
file_date (:obj:`int`): Unix time when the file was uploaded.
|
||||
|
||||
.. versionchanged:: 22.0
|
||||
Accepts only :class:`datetime.datetime` instead of :obj:`int`.
|
||||
|datetime_localization|
|
||||
.. deprecated:: 20.6
|
||||
This argument will only accept a datetime instead of an integer in future
|
||||
major versions.
|
||||
|
||||
Attributes:
|
||||
file_id (:obj:`str`): Identifier for this file, which can be used to download
|
||||
@@ -58,16 +58,11 @@ class PassportFile(TelegramObject):
|
||||
is supposed to be the same over time and for different bots.
|
||||
Can't be used to download or reuse the file.
|
||||
file_size (:obj:`int`): File size in bytes.
|
||||
file_date (:class:`datetime.datetime`): Time when the file was uploaded.
|
||||
|
||||
.. versionchanged:: 22.0
|
||||
Returns :class:`datetime.datetime` instead of :obj:`int`.
|
||||
|datetime_localization|
|
||||
"""
|
||||
|
||||
__slots__ = (
|
||||
"_credentials",
|
||||
"file_date",
|
||||
"_file_date",
|
||||
"file_id",
|
||||
"file_size",
|
||||
"file_unique_id",
|
||||
@@ -77,7 +72,7 @@ class PassportFile(TelegramObject):
|
||||
self,
|
||||
file_id: str,
|
||||
file_unique_id: str,
|
||||
file_date: dtm.datetime,
|
||||
file_date: int,
|
||||
file_size: int,
|
||||
credentials: Optional["FileCredentials"] = None,
|
||||
*,
|
||||
@@ -89,7 +84,7 @@ class PassportFile(TelegramObject):
|
||||
self.file_id: str = file_id
|
||||
self.file_unique_id: str = file_unique_id
|
||||
self.file_size: int = file_size
|
||||
self.file_date: dtm.datetime = file_date
|
||||
self._file_date: int = file_date
|
||||
# Optionals
|
||||
|
||||
self._credentials: Optional[FileCredentials] = credentials
|
||||
@@ -98,16 +93,28 @@ class PassportFile(TelegramObject):
|
||||
|
||||
self._freeze()
|
||||
|
||||
@classmethod
|
||||
def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "PassportFile":
|
||||
"""See :meth:`telegram.TelegramObject.de_json`."""
|
||||
data = cls._parse_data(data)
|
||||
def to_dict(self, recursive: bool = True) -> JSONDict:
|
||||
"""See :meth:`telegram.TelegramObject.to_dict` for details."""
|
||||
data = super().to_dict(recursive)
|
||||
data["file_date"] = self._file_date
|
||||
return data
|
||||
|
||||
# Get the local timezone from the bot if it has defaults
|
||||
loc_tzinfo = extract_tzinfo_from_defaults(bot)
|
||||
data["file_date"] = from_timestamp(data.get("file_date"), tzinfo=loc_tzinfo)
|
||||
@property
|
||||
def file_date(self) -> int:
|
||||
""":obj:`int`: Unix time when the file was uploaded.
|
||||
|
||||
return super().de_json(data=data, bot=bot)
|
||||
.. deprecated:: 20.6
|
||||
This attribute will return a datetime instead of a integer in future major versions.
|
||||
"""
|
||||
warn(
|
||||
PTBDeprecationWarning(
|
||||
"20.6",
|
||||
"The attribute `file_date` will return a datetime instead of an integer in future"
|
||||
" major versions.",
|
||||
),
|
||||
stacklevel=2,
|
||||
)
|
||||
return self._file_date
|
||||
|
||||
@classmethod
|
||||
def de_json_decrypted(
|
||||
|
||||
@@ -36,9 +36,6 @@ if TYPE_CHECKING:
|
||||
|
||||
class StarTransaction(TelegramObject):
|
||||
"""Describes a Telegram Star transaction.
|
||||
Note that if the buyer initiates a chargeback with the payment provider from whom they
|
||||
acquired Stars (e.g., Apple, Google) following this transaction, the refunded Stars will be
|
||||
deducted from the bot's balance. This is outside of Telegram's control.
|
||||
|
||||
Objects of this class are comparable in terms of equality. Two objects of this class are
|
||||
considered equal, if their :attr:`id`, :attr:`source`, and :attr:`receiver` are equal.
|
||||
|
||||
@@ -23,7 +23,6 @@ from collections.abc import Sequence
|
||||
from typing import TYPE_CHECKING, Final, Optional
|
||||
|
||||
from telegram import constants
|
||||
from telegram._chat import Chat
|
||||
from telegram._gifts import Gift
|
||||
from telegram._paidmedia import PaidMedia
|
||||
from telegram._telegramobject import TelegramObject
|
||||
@@ -44,7 +43,6 @@ class TransactionPartner(TelegramObject):
|
||||
transactions. Currently, it can be one of:
|
||||
|
||||
* :class:`TransactionPartnerUser`
|
||||
* :class:`TransactionPartnerChat`
|
||||
* :class:`TransactionPartnerAffiliateProgram`
|
||||
* :class:`TransactionPartnerFragment`
|
||||
* :class:`TransactionPartnerTelegramAds`
|
||||
@@ -56,9 +54,6 @@ class TransactionPartner(TelegramObject):
|
||||
|
||||
.. versionadded:: 21.4
|
||||
|
||||
..versionchanged:: 21.11
|
||||
Added :class:`TransactionPartnerChat`
|
||||
|
||||
Args:
|
||||
type (:obj:`str`): The type of the transaction partner.
|
||||
|
||||
@@ -73,11 +68,6 @@ class TransactionPartner(TelegramObject):
|
||||
|
||||
.. versionadded:: 21.9
|
||||
"""
|
||||
CHAT: Final[str] = constants.TransactionPartnerType.CHAT
|
||||
""":const:`telegram.constants.TransactionPartnerType.CHAT`
|
||||
|
||||
.. versionadded:: 21.11
|
||||
"""
|
||||
FRAGMENT: Final[str] = constants.TransactionPartnerType.FRAGMENT
|
||||
""":const:`telegram.constants.TransactionPartnerType.FRAGMENT`"""
|
||||
OTHER: Final[str] = constants.TransactionPartnerType.OTHER
|
||||
@@ -113,7 +103,6 @@ class TransactionPartner(TelegramObject):
|
||||
|
||||
_class_mapping: dict[str, type[TransactionPartner]] = {
|
||||
cls.AFFILIATE_PROGRAM: TransactionPartnerAffiliateProgram,
|
||||
cls.CHAT: TransactionPartnerChat,
|
||||
cls.FRAGMENT: TransactionPartnerFragment,
|
||||
cls.USER: TransactionPartnerUser,
|
||||
cls.TELEGRAM_ADS: TransactionPartnerTelegramAds,
|
||||
@@ -182,60 +171,6 @@ class TransactionPartnerAffiliateProgram(TransactionPartner):
|
||||
return super().de_json(data=data, bot=bot) # type: ignore[return-value]
|
||||
|
||||
|
||||
class TransactionPartnerChat(TransactionPartner):
|
||||
"""Describes a transaction with a chat.
|
||||
|
||||
Objects of this class are comparable in terms of equality. Two objects of this class are
|
||||
considered equal, if their :attr:`chat` are equal.
|
||||
|
||||
.. versionadded:: 21.11
|
||||
|
||||
Args:
|
||||
chat (:class:`telegram.Chat`): Information about the chat.
|
||||
gift (:class:`telegram.Gift`, optional): The gift sent to the chat by the bot.
|
||||
|
||||
Attributes:
|
||||
type (:obj:`str`): The type of the transaction partner,
|
||||
always :tg-const:`telegram.TransactionPartner.CHAT`.
|
||||
chat (:class:`telegram.Chat`): Information about the chat.
|
||||
gift (:class:`telegram.Gift`): Optional. The gift sent to the user by the bot.
|
||||
|
||||
"""
|
||||
|
||||
__slots__ = (
|
||||
"chat",
|
||||
"gift",
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
chat: Chat,
|
||||
gift: Optional[Gift] = None,
|
||||
*,
|
||||
api_kwargs: Optional[JSONDict] = None,
|
||||
) -> None:
|
||||
super().__init__(type=TransactionPartner.CHAT, api_kwargs=api_kwargs)
|
||||
|
||||
with self._unfrozen():
|
||||
self.chat: Chat = chat
|
||||
self.gift: Optional[Gift] = gift
|
||||
|
||||
self._id_attrs = (
|
||||
self.type,
|
||||
self.chat,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "TransactionPartnerChat":
|
||||
"""See :meth:`telegram.TransactionPartner.de_json`."""
|
||||
data = cls._parse_data(data)
|
||||
|
||||
data["chat"] = de_json_optional(data.get("chat"), Chat, bot)
|
||||
data["gift"] = de_json_optional(data.get("gift"), Gift, bot)
|
||||
|
||||
return super().de_json(data=data, bot=bot) # type: ignore[return-value]
|
||||
|
||||
|
||||
class TransactionPartnerFragment(TransactionPartner):
|
||||
"""Describes a withdrawal transaction with Fragment.
|
||||
|
||||
|
||||
@@ -33,9 +33,6 @@ if TYPE_CHECKING:
|
||||
|
||||
class SuccessfulPayment(TelegramObject):
|
||||
"""This object contains basic information about a successful payment.
|
||||
Note that if the buyer initiates a chargeback with the relevant payment provider following
|
||||
this transaction, the funds may be debited from your balance. This is outside of
|
||||
Telegram's control.
|
||||
|
||||
Objects of this class are comparable in terms of equality. Two objects of this class are
|
||||
considered equal, if their :attr:`telegram_payment_charge_id` and
|
||||
|
||||
+2
-15
@@ -1018,9 +1018,9 @@ class User(TelegramObject):
|
||||
title: str,
|
||||
description: str,
|
||||
payload: str,
|
||||
provider_token: Optional[str],
|
||||
currency: str,
|
||||
prices: Sequence["LabeledPrice"],
|
||||
provider_token: Optional[str] = None,
|
||||
start_parameter: Optional[str] = None,
|
||||
photo_url: Optional[str] = None,
|
||||
photo_size: Optional[int] = None,
|
||||
@@ -1328,8 +1328,6 @@ class User(TelegramObject):
|
||||
message_effect_id: Optional[str] = None,
|
||||
allow_paid_broadcast: Optional[bool] = None,
|
||||
show_caption_above_media: Optional[bool] = None,
|
||||
cover: Optional[FileInput] = None,
|
||||
start_timestamp: Optional[int] = None,
|
||||
*,
|
||||
reply_to_message_id: Optional[int] = None,
|
||||
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
|
||||
@@ -1371,8 +1369,6 @@ class User(TelegramObject):
|
||||
parse_mode=parse_mode,
|
||||
supports_streaming=supports_streaming,
|
||||
thumbnail=thumbnail,
|
||||
cover=cover,
|
||||
start_timestamp=start_timestamp,
|
||||
api_kwargs=api_kwargs,
|
||||
allow_sending_without_reply=allow_sending_without_reply,
|
||||
caption_entities=caption_entities,
|
||||
@@ -1674,7 +1670,7 @@ class User(TelegramObject):
|
||||
) -> bool:
|
||||
"""Shortcut for::
|
||||
|
||||
await bot.send_gift(user_id=update.effective_user.id, *args, **kwargs )
|
||||
await bot.send_gift( user_id=update.effective_user.id, *args, **kwargs )
|
||||
|
||||
For the documentation of the arguments, please see :meth:`telegram.Bot.send_gift`.
|
||||
|
||||
@@ -1684,7 +1680,6 @@ class User(TelegramObject):
|
||||
:obj:`bool`: On success, :obj:`True` is returned.
|
||||
"""
|
||||
return await self.get_bot().send_gift(
|
||||
chat_id=None,
|
||||
user_id=self.id,
|
||||
gift_id=gift_id,
|
||||
text=text,
|
||||
@@ -1712,7 +1707,6 @@ class User(TelegramObject):
|
||||
reply_parameters: Optional["ReplyParameters"] = None,
|
||||
show_caption_above_media: Optional[bool] = None,
|
||||
allow_paid_broadcast: Optional[bool] = None,
|
||||
video_start_timestamp: Optional[int] = None,
|
||||
*,
|
||||
reply_to_message_id: Optional[int] = None,
|
||||
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
|
||||
@@ -1740,7 +1734,6 @@ class User(TelegramObject):
|
||||
from_chat_id=from_chat_id,
|
||||
message_id=message_id,
|
||||
caption=caption,
|
||||
video_start_timestamp=video_start_timestamp,
|
||||
parse_mode=parse_mode,
|
||||
caption_entities=caption_entities,
|
||||
disable_notification=disable_notification,
|
||||
@@ -1773,7 +1766,6 @@ class User(TelegramObject):
|
||||
reply_parameters: Optional["ReplyParameters"] = None,
|
||||
show_caption_above_media: Optional[bool] = None,
|
||||
allow_paid_broadcast: Optional[bool] = None,
|
||||
video_start_timestamp: Optional[int] = None,
|
||||
*,
|
||||
reply_to_message_id: Optional[int] = None,
|
||||
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
|
||||
@@ -1801,7 +1793,6 @@ class User(TelegramObject):
|
||||
chat_id=chat_id,
|
||||
message_id=message_id,
|
||||
caption=caption,
|
||||
video_start_timestamp=video_start_timestamp,
|
||||
parse_mode=parse_mode,
|
||||
caption_entities=caption_entities,
|
||||
disable_notification=disable_notification,
|
||||
@@ -1917,7 +1908,6 @@ class User(TelegramObject):
|
||||
disable_notification: ODVInput[bool] = DEFAULT_NONE,
|
||||
protect_content: ODVInput[bool] = DEFAULT_NONE,
|
||||
message_thread_id: Optional[int] = None,
|
||||
video_start_timestamp: Optional[int] = None,
|
||||
*,
|
||||
read_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
write_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
@@ -1943,7 +1933,6 @@ class User(TelegramObject):
|
||||
chat_id=self.id,
|
||||
from_chat_id=from_chat_id,
|
||||
message_id=message_id,
|
||||
video_start_timestamp=video_start_timestamp,
|
||||
disable_notification=disable_notification,
|
||||
read_timeout=read_timeout,
|
||||
write_timeout=write_timeout,
|
||||
@@ -1961,7 +1950,6 @@ class User(TelegramObject):
|
||||
disable_notification: ODVInput[bool] = DEFAULT_NONE,
|
||||
protect_content: ODVInput[bool] = DEFAULT_NONE,
|
||||
message_thread_id: Optional[int] = None,
|
||||
video_start_timestamp: Optional[int] = None,
|
||||
*,
|
||||
read_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
write_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
@@ -1988,7 +1976,6 @@ class User(TelegramObject):
|
||||
from_chat_id=self.id,
|
||||
chat_id=chat_id,
|
||||
message_id=message_id,
|
||||
video_start_timestamp=video_start_timestamp,
|
||||
disable_notification=disable_notification,
|
||||
read_timeout=read_timeout,
|
||||
write_timeout=write_timeout,
|
||||
|
||||
@@ -51,6 +51,6 @@ class Version(NamedTuple):
|
||||
|
||||
|
||||
__version_info__: Final[Version] = Version(
|
||||
major=22, minor=0, micro=0, releaselevel="final", serial=0
|
||||
major=21, minor=10, micro=0, releaselevel="final", serial=0
|
||||
)
|
||||
__version__: Final[str] = str(__version_info__)
|
||||
|
||||
+23
-21
@@ -155,7 +155,7 @@ class _AccentColor(NamedTuple):
|
||||
#: :data:`telegram.__bot_api_version_info__`.
|
||||
#:
|
||||
#: .. versionadded:: 20.0
|
||||
BOT_API_VERSION_INFO: Final[_BotAPIVersion] = _BotAPIVersion(major=8, minor=3)
|
||||
BOT_API_VERSION_INFO: Final[_BotAPIVersion] = _BotAPIVersion(major=8, minor=2)
|
||||
#: :obj:`str`: Telegram Bot API
|
||||
#: version supported by this version of `python-telegram-bot`. Also available as
|
||||
#: :data:`telegram.__bot_api_version__`.
|
||||
@@ -1236,12 +1236,9 @@ class GiftLimit(IntEnum):
|
||||
|
||||
__slots__ = ()
|
||||
|
||||
MAX_TEXT_LENGTH = 128
|
||||
MAX_TEXT_LENGTH = 255
|
||||
""":obj:`int`: Maximum number of characters in a :obj:`str` passed as the
|
||||
:paramref:`~telegram.Bot.send_gift.text` parameter of :meth:`~telegram.Bot.send_gift`.
|
||||
|
||||
.. versionchanged:: 21.11
|
||||
Updated Value to 128 based on Bot API 8.3
|
||||
"""
|
||||
|
||||
|
||||
@@ -1375,12 +1372,6 @@ class InlineQueryLimit(IntEnum):
|
||||
of :class:`int` and can be treated as such.
|
||||
|
||||
.. versionadded:: 20.0
|
||||
|
||||
.. versionchanged:: 22.0
|
||||
Removed deprecated attributes ``InlineQueryLimit.MIN_SWITCH_PM_TEXT_LENGTH`` and
|
||||
``InlineQueryLimit.MAX_SWITCH_PM_TEXT_LENGTH``. Please instead use
|
||||
:attr:`InlineQueryResultsButtonLimit.MIN_START_PARAMETER_LENGTH` and
|
||||
:attr:`InlineQueryResultsButtonLimit.MAX_START_PARAMETER_LENGTH`.
|
||||
"""
|
||||
|
||||
__slots__ = ()
|
||||
@@ -1395,6 +1386,22 @@ class InlineQueryLimit(IntEnum):
|
||||
MAX_QUERY_LENGTH = 256
|
||||
""":obj:`int`: Maximum number of characters in a :obj:`str` passed as the
|
||||
:paramref:`~telegram.InlineQuery.query` parameter of :class:`telegram.InlineQuery`."""
|
||||
MIN_SWITCH_PM_TEXT_LENGTH = 1
|
||||
""":obj:`int`: Minimum number of characters in a :obj:`str` passed as the
|
||||
:paramref:`~telegram.Bot.answer_inline_query.switch_pm_parameter` parameter of
|
||||
:meth:`telegram.Bot.answer_inline_query`.
|
||||
|
||||
.. deprecated:: 20.3
|
||||
Deprecated in favor of :attr:`InlineQueryResultsButtonLimit.MIN_START_PARAMETER_LENGTH`.
|
||||
"""
|
||||
MAX_SWITCH_PM_TEXT_LENGTH = 64
|
||||
""":obj:`int`: Maximum number of characters in a :obj:`str` passed as the
|
||||
:paramref:`~telegram.Bot.answer_inline_query.switch_pm_parameter` parameter of
|
||||
:meth:`telegram.Bot.answer_inline_query`.
|
||||
|
||||
.. deprecated:: 20.3
|
||||
Deprecated in favor of :attr:`InlineQueryResultsButtonLimit.MAX_START_PARAMETER_LENGTH`.
|
||||
"""
|
||||
|
||||
|
||||
class InlineQueryResultLimit(IntEnum):
|
||||
@@ -1427,12 +1434,12 @@ class InlineQueryResultsButtonLimit(IntEnum):
|
||||
|
||||
__slots__ = ()
|
||||
|
||||
MIN_START_PARAMETER_LENGTH = 1
|
||||
MIN_START_PARAMETER_LENGTH = InlineQueryLimit.MIN_SWITCH_PM_TEXT_LENGTH
|
||||
""":obj:`int`: Minimum number of characters in a :obj:`str` passed as the
|
||||
:paramref:`~telegram.InlineQueryResultsButton.start_parameter` parameter of
|
||||
:meth:`telegram.InlineQueryResultsButton`."""
|
||||
|
||||
MAX_START_PARAMETER_LENGTH = 64
|
||||
MAX_START_PARAMETER_LENGTH = InlineQueryLimit.MAX_SWITCH_PM_TEXT_LENGTH
|
||||
""":obj:`int`: Maximum number of characters in a :obj:`str` passed as the
|
||||
:paramref:`~telegram.InlineQueryResultsButton.start_parameter` parameter of
|
||||
:meth:`telegram.InlineQueryResultsButton`."""
|
||||
@@ -2611,13 +2618,13 @@ class StickerSetLimit(IntEnum):
|
||||
:meth:`telegram.Bot.add_sticker_to_set`.
|
||||
"""
|
||||
MAX_STATIC_THUMBNAIL_SIZE = 128
|
||||
""":obj:`int`: Maximum size of the thumbnail if it is a ``.WEBP`` or ``.PNG`` in kilobytes,
|
||||
""":obj:`int`: Maximum size of the thumbnail if it is a **.WEBP** or **.PNG** in kilobytes,
|
||||
as given in :meth:`telegram.Bot.set_sticker_set_thumbnail`."""
|
||||
MAX_ANIMATED_THUMBNAIL_SIZE = 32
|
||||
""":obj:`int`: Maximum size of the thumbnail if it is a ``.TGS`` or ``.WEBM`` in kilobytes,
|
||||
""":obj:`int`: Maximum size of the thumbnail if it is a **.TGS** or **.WEBM** in kilobytes,
|
||||
as given in :meth:`telegram.Bot.set_sticker_set_thumbnail`."""
|
||||
STATIC_THUMB_DIMENSIONS = 100
|
||||
""":obj:`int`: Exact height and width of the thumbnail if it is a ``.WEBP`` or ``.PNG`` in
|
||||
""":obj:`int`: Exact height and width of the thumbnail if it is a **.WEBP** or **.PNG** in
|
||||
pixels, as given in :meth:`telegram.Bot.set_sticker_set_thumbnail`."""
|
||||
|
||||
|
||||
@@ -2652,11 +2659,6 @@ class TransactionPartnerType(StringEnum):
|
||||
|
||||
.. versionadded:: 21.9
|
||||
"""
|
||||
CHAT = "chat"
|
||||
""":obj:`str`: Transaction with a chat.
|
||||
|
||||
.. versionadded:: 21.11
|
||||
"""
|
||||
FRAGMENT = "fragment"
|
||||
""":obj:`str`: Withdrawal transaction with Fragment."""
|
||||
OTHER = "other"
|
||||
|
||||
@@ -42,6 +42,7 @@ __all__ = (
|
||||
"Defaults",
|
||||
"DictPersistence",
|
||||
"ExtBot",
|
||||
"FiniteStateMachine",
|
||||
"InlineQueryHandler",
|
||||
"InvalidCallbackData",
|
||||
"Job",
|
||||
@@ -57,6 +58,9 @@ __all__ = (
|
||||
"PrefixHandler",
|
||||
"ShippingQueryHandler",
|
||||
"SimpleUpdateProcessor",
|
||||
"SingleStateMachine",
|
||||
"State",
|
||||
"StateInfo",
|
||||
"StringCommandHandler",
|
||||
"StringRegexHandler",
|
||||
"TypeHandler",
|
||||
@@ -77,6 +81,7 @@ from ._contexttypes import ContextTypes
|
||||
from ._defaults import Defaults
|
||||
from ._dictpersistence import DictPersistence
|
||||
from ._extbot import ExtBot
|
||||
from ._fsm import FiniteStateMachine, SingleStateMachine, State, StateInfo
|
||||
from ._handlers.basehandler import BaseHandler
|
||||
from ._handlers.businessconnectionhandler import BusinessConnectionHandler
|
||||
from ._handlers.businessmessagesdeletedhandler import BusinessMessagesDeletedHandler
|
||||
|
||||
@@ -98,7 +98,7 @@ class AIORateLimiter(BaseRateLimiter[int]):
|
||||
:tg-const:`telegram.constants.FloodLimit.PAID_MESSAGES_PER_SECOND` messages per second by
|
||||
paying a fee in Telegram Stars.
|
||||
|
||||
.. versionchanged:: 21.11
|
||||
.. versionchanged:: NEXT.VERSION
|
||||
This class automatically takes the
|
||||
:paramref:`~telegram.Bot.send_message.allow_paid_broadcast` parameter into account and
|
||||
throttles the requests accordingly.
|
||||
|
||||
+111
-48
@@ -48,9 +48,9 @@ from telegram.error import TelegramError
|
||||
from telegram.ext._basepersistence import BasePersistence
|
||||
from telegram.ext._contexttypes import ContextTypes
|
||||
from telegram.ext._extbot import ExtBot
|
||||
from telegram.ext._fsm import SingleStateMachine, State, StateInfo
|
||||
from telegram.ext._handlers.basehandler import BaseHandler
|
||||
from telegram.ext._updater import Updater
|
||||
from telegram.ext._utils.networkloop import network_retry_loop
|
||||
from telegram.ext._utils.stack import was_called_by
|
||||
from telegram.ext._utils.trackingdict import TrackingDict
|
||||
from telegram.ext._utils.types import BD, BT, CCT, CD, JQ, RT, UD, ConversationKey, HandlerCallback
|
||||
@@ -60,7 +60,7 @@ if TYPE_CHECKING:
|
||||
from socket import socket
|
||||
|
||||
from telegram import Message
|
||||
from telegram.ext import ConversationHandler, JobQueue
|
||||
from telegram.ext import ConversationHandler, FiniteStateMachine, JobQueue
|
||||
from telegram.ext._applicationbuilder import InitApplicationBuilder
|
||||
from telegram.ext._baseupdateprocessor import BaseUpdateProcessor
|
||||
from telegram.ext._jobqueue import Job
|
||||
@@ -267,6 +267,7 @@ class Application(
|
||||
"update_queue",
|
||||
"updater",
|
||||
"user_data",
|
||||
"fsm",
|
||||
)
|
||||
# Allowing '__weakref__' creation here since we need it for the JobQueue
|
||||
# Currently the __weakref__ slot is already created
|
||||
@@ -302,11 +303,12 @@ class Application(
|
||||
stacklevel=2,
|
||||
)
|
||||
|
||||
self.fsm: FiniteStateMachine = SingleStateMachine()
|
||||
self.bot: BT = bot
|
||||
self.update_queue: asyncio.Queue[object] = update_queue
|
||||
self.context_types: ContextTypes[CCT, UD, CD, BD] = context_types
|
||||
self.updater: Optional[Updater] = updater
|
||||
self.handlers: dict[int, list[BaseHandler[Any, CCT, Any]]] = {}
|
||||
self.handlers: dict[State, dict[int, list[BaseHandler[Any, CCT, Any]]]] = {}
|
||||
self.error_handlers: dict[
|
||||
HandlerCallback[object, CCT, None], Union[bool, DefaultValue[bool]]
|
||||
] = {}
|
||||
@@ -740,7 +742,11 @@ class Application(
|
||||
self,
|
||||
poll_interval: float = 0.0,
|
||||
timeout: int = 10,
|
||||
bootstrap_retries: int = 0,
|
||||
bootstrap_retries: int = -1,
|
||||
read_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
write_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
connect_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
pool_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
allowed_updates: Optional[Sequence[str]] = None,
|
||||
drop_pending_updates: Optional[bool] = None,
|
||||
close_loop: bool = True,
|
||||
@@ -772,29 +778,50 @@ class Application(
|
||||
|
||||
.. include:: inclusions/application_run_tip.rst
|
||||
|
||||
.. versionchanged::
|
||||
Removed the deprecated parameters ``read_timeout``, ``write_timeout``,
|
||||
``connect_timeout``, and ``pool_timeout``. Use the corresponding methods in
|
||||
:class:`telegram.ext.ApplicationBuilder` instead.
|
||||
|
||||
Args:
|
||||
poll_interval (:obj:`float`, optional): Time to wait between polling updates from
|
||||
Telegram in seconds. Default is ``0.0``.
|
||||
timeout (:obj:`int`, optional): Passed to
|
||||
:paramref:`telegram.Bot.get_updates.timeout`. Default is ``10`` seconds.
|
||||
bootstrap_retries (:obj:`int`, optional): Whether the bootstrapping phase
|
||||
(calling :meth:`initialize` and the boostrapping of
|
||||
:meth:`telegram.ext.Updater.start_polling`)
|
||||
will retry on failures on the Telegram server.
|
||||
bootstrap_retries (:obj:`int`, optional): Whether the bootstrapping phase of the
|
||||
:class:`telegram.ext.Updater` will retry on failures on the Telegram server.
|
||||
|
||||
* < 0 - retry indefinitely
|
||||
* 0 - no retries (default)
|
||||
* < 0 - retry indefinitely (default)
|
||||
* 0 - no retries
|
||||
* > 0 - retry up to X times
|
||||
|
||||
.. versionchanged:: 21.11
|
||||
The default value will be changed to from ``-1`` to ``0``. Indefinite retries
|
||||
during bootstrapping are not recommended.
|
||||
read_timeout (:obj:`float`, optional): Value to pass to
|
||||
:paramref:`telegram.Bot.get_updates.read_timeout`. Defaults to
|
||||
:attr:`~telegram.request.BaseRequest.DEFAULT_NONE`.
|
||||
|
||||
.. versionchanged:: 20.7
|
||||
Defaults to :attr:`~telegram.request.BaseRequest.DEFAULT_NONE` instead of
|
||||
``2``.
|
||||
|
||||
.. deprecated:: 20.7
|
||||
Deprecated in favor of setting the timeout via
|
||||
:meth:`telegram.ext.ApplicationBuilder.get_updates_read_timeout`.
|
||||
write_timeout (:obj:`float` | :obj:`None`, optional): Value to pass to
|
||||
:paramref:`telegram.Bot.get_updates.write_timeout`. Defaults to
|
||||
:attr:`~telegram.request.BaseRequest.DEFAULT_NONE`.
|
||||
|
||||
.. deprecated:: 20.7
|
||||
Deprecated in favor of setting the timeout via
|
||||
:meth:`telegram.ext.ApplicationBuilder.get_updates_write_timeout`.
|
||||
connect_timeout (:obj:`float` | :obj:`None`, optional): Value to pass to
|
||||
:paramref:`telegram.Bot.get_updates.connect_timeout`. Defaults to
|
||||
:attr:`~telegram.request.BaseRequest.DEFAULT_NONE`.
|
||||
|
||||
.. deprecated:: 20.7
|
||||
Deprecated in favor of setting the timeout via
|
||||
:meth:`telegram.ext.ApplicationBuilder.get_updates_connect_timeout`.
|
||||
pool_timeout (:obj:`float` | :obj:`None`, optional): Value to pass to
|
||||
:paramref:`telegram.Bot.get_updates.pool_timeout`. Defaults to
|
||||
:attr:`~telegram.request.BaseRequest.DEFAULT_NONE`.
|
||||
|
||||
.. deprecated:: 20.7
|
||||
Deprecated in favor of setting the timeout via
|
||||
:meth:`telegram.ext.ApplicationBuilder.get_updates_pool_timeout`.
|
||||
drop_pending_updates (:obj:`bool`, optional): Whether to clean any pending updates on
|
||||
Telegram servers before actually starting to poll. Default is :obj:`False`.
|
||||
allowed_updates (Sequence[:obj:`str`], optional): Passed to
|
||||
@@ -826,6 +853,16 @@ class Application(
|
||||
"Application.run_polling is only available if the application has an Updater."
|
||||
)
|
||||
|
||||
if (read_timeout, write_timeout, connect_timeout, pool_timeout) != ((DEFAULT_NONE,) * 4):
|
||||
warn(
|
||||
PTBDeprecationWarning(
|
||||
"20.6",
|
||||
"Setting timeouts via `Application.run_polling` is deprecated. "
|
||||
"Please use `ApplicationBuilder.get_updates_*_timeout` instead.",
|
||||
),
|
||||
stacklevel=2,
|
||||
)
|
||||
|
||||
def error_callback(exc: TelegramError) -> None:
|
||||
self.create_task(self.process_error(error=exc, update=None))
|
||||
|
||||
@@ -834,13 +871,16 @@ class Application(
|
||||
poll_interval=poll_interval,
|
||||
timeout=timeout,
|
||||
bootstrap_retries=bootstrap_retries,
|
||||
read_timeout=read_timeout,
|
||||
write_timeout=write_timeout,
|
||||
connect_timeout=connect_timeout,
|
||||
pool_timeout=pool_timeout,
|
||||
allowed_updates=allowed_updates,
|
||||
drop_pending_updates=drop_pending_updates,
|
||||
error_callback=error_callback, # if there is an error in fetching updates
|
||||
),
|
||||
stop_signals=stop_signals,
|
||||
bootstrap_retries=bootstrap_retries,
|
||||
close_loop=close_loop,
|
||||
stop_signals=stop_signals,
|
||||
)
|
||||
|
||||
def run_webhook(
|
||||
@@ -909,10 +949,8 @@ class Application(
|
||||
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.
|
||||
bootstrap_retries (:obj:`int`, optional): Whether the bootstrapping phase
|
||||
(calling :meth:`initialize` and the boostrapping of
|
||||
:meth:`telegram.ext.Updater.start_polling`)
|
||||
will retry on failures on the Telegram server.
|
||||
bootstrap_retries (:obj:`int`, optional): Whether the bootstrapping phase of the
|
||||
:class:`telegram.ext.Updater` will retry on failures on the Telegram server.
|
||||
|
||||
* < 0 - retry indefinitely
|
||||
* 0 - no retries (default)
|
||||
@@ -998,28 +1036,18 @@ class Application(
|
||||
secret_token=secret_token,
|
||||
unix=unix,
|
||||
),
|
||||
stop_signals=stop_signals,
|
||||
bootstrap_retries=bootstrap_retries,
|
||||
close_loop=close_loop,
|
||||
)
|
||||
|
||||
async def _bootstrap_initialize(self, max_retries: int) -> None:
|
||||
await network_retry_loop(
|
||||
action_cb=self.initialize,
|
||||
description="Bootstrap Initialize Application",
|
||||
max_retries=max_retries,
|
||||
interval=1,
|
||||
stop_signals=stop_signals,
|
||||
)
|
||||
|
||||
def __run(
|
||||
self,
|
||||
updater_coroutine: Coroutine,
|
||||
stop_signals: ODVInput[Sequence[int]],
|
||||
bootstrap_retries: int,
|
||||
close_loop: bool = True,
|
||||
) -> None:
|
||||
# Calling get_event_loop() should still be okay even in py3.10+ as long as there is a
|
||||
# running event loop, or we are in the main thread, which are the intended use cases.
|
||||
# running event loop or we are in the main thread, which are the intended use cases.
|
||||
# See the docs of get_event_loop() and get_running_loop() for more info
|
||||
loop = asyncio.get_event_loop()
|
||||
|
||||
@@ -1039,7 +1067,7 @@ class Application(
|
||||
)
|
||||
|
||||
try:
|
||||
loop.run_until_complete(self._bootstrap_initialize(max_retries=bootstrap_retries))
|
||||
loop.run_until_complete(self.initialize())
|
||||
if self.post_init:
|
||||
loop.run_until_complete(self.post_init(self))
|
||||
if self.__stop_running_marker.is_set():
|
||||
@@ -1253,19 +1281,46 @@ class Application(
|
||||
# Processing updates before initialize() is a problem e.g. if persistence is used
|
||||
self._check_initialized()
|
||||
|
||||
fsm_state_info = self.fsm.get_state_info(update)
|
||||
|
||||
for state, state_handlers in self.handlers.items():
|
||||
if state.matches(fsm_state_info.state):
|
||||
_LOGGER.debug("Processing in state %s", state)
|
||||
was_handled = await self.__process_update_groups(
|
||||
update, state_handlers, fsm_state_info
|
||||
)
|
||||
if was_handled:
|
||||
_LOGGER.debug(
|
||||
"Update was handled in state %s. Stopping further processing", state
|
||||
)
|
||||
return
|
||||
_LOGGER.debug(
|
||||
"No handlers found for key %s in state %s", fsm_state_info.key, fsm_state_info.state
|
||||
)
|
||||
return
|
||||
|
||||
async def __process_update_groups(
|
||||
self,
|
||||
update: object,
|
||||
state_handlers: dict[int, list[BaseHandler]],
|
||||
fsm_state_info: StateInfo,
|
||||
) -> bool:
|
||||
context = None
|
||||
was_handled = False
|
||||
any_blocking = False # Flag which is set to True if any handler specifies block=True
|
||||
|
||||
for handlers in self.handlers.values():
|
||||
for handlers in state_handlers.values():
|
||||
try:
|
||||
for handler in handlers:
|
||||
check = handler.check_update(update) # Should the handler handle this update?
|
||||
if check is None or check is False:
|
||||
continue
|
||||
was_handled = True
|
||||
|
||||
if not context: # build a context if not already built
|
||||
try:
|
||||
context = self.context_types.context.from_update(update, self)
|
||||
context.fsm_state_info = fsm_state_info
|
||||
except Exception as exc:
|
||||
_LOGGER.critical(
|
||||
(
|
||||
@@ -1275,7 +1330,7 @@ class Application(
|
||||
update,
|
||||
exc_info=exc,
|
||||
)
|
||||
return
|
||||
return True
|
||||
await context.refresh_data()
|
||||
coroutine: Coroutine = handler.handle_update(update, self, check, context)
|
||||
|
||||
@@ -1315,7 +1370,14 @@ class Application(
|
||||
# (in __create_task_callback)
|
||||
self._mark_for_persistence_update(update=update)
|
||||
|
||||
def add_handler(self, handler: BaseHandler[Any, CCT, Any], group: int = DEFAULT_GROUP) -> None:
|
||||
return was_handled
|
||||
|
||||
def add_handler(
|
||||
self,
|
||||
handler: BaseHandler[Any, CCT, Any],
|
||||
group: int = DEFAULT_GROUP,
|
||||
state: State = State.IDLE,
|
||||
) -> None:
|
||||
"""Register a handler.
|
||||
|
||||
TL;DR: Order and priority counts. 0 or 1 handlers per group will be used. End handling of
|
||||
@@ -1374,11 +1436,11 @@ class Application(
|
||||
stacklevel=2,
|
||||
)
|
||||
|
||||
if group not in self.handlers:
|
||||
self.handlers[group] = []
|
||||
self.handlers = dict(sorted(self.handlers.items())) # lower -> higher groups
|
||||
state_handlers = self.handlers.setdefault(state, {})
|
||||
if group not in state_handlers:
|
||||
state_handlers[group] = []
|
||||
|
||||
self.handlers[group].append(handler)
|
||||
state_handlers[group].append(handler)
|
||||
|
||||
def add_handlers(
|
||||
self,
|
||||
@@ -1450,10 +1512,11 @@ class Application(
|
||||
group (:obj:`object`, optional): The group identifier. Default is ``0``.
|
||||
|
||||
"""
|
||||
if handler in self.handlers[group]:
|
||||
self.handlers[group].remove(handler)
|
||||
if not self.handlers[group]:
|
||||
del self.handlers[group]
|
||||
for state_handlers in self.handlers.values():
|
||||
if handler in state_handlers[group]:
|
||||
state_handlers[group].remove(handler)
|
||||
if not state_handlers[group]:
|
||||
del state_handlers[group]
|
||||
|
||||
def drop_chat_data(self, chat_id: int) -> None:
|
||||
"""Drops the corresponding entry from the :attr:`chat_data`. Will also be deleted from
|
||||
|
||||
@@ -35,6 +35,7 @@ from telegram._utils.types import (
|
||||
ODVInput,
|
||||
SocketOpt,
|
||||
)
|
||||
from telegram._utils.warnings import warn
|
||||
from telegram.ext._application import Application
|
||||
from telegram.ext._baseupdateprocessor import BaseUpdateProcessor, SimpleUpdateProcessor
|
||||
from telegram.ext._contexttypes import ContextTypes
|
||||
@@ -44,6 +45,7 @@ from telegram.ext._updater import Updater
|
||||
from telegram.ext._utils.types import BD, BT, CCT, CD, JQ, UD
|
||||
from telegram.request import BaseRequest
|
||||
from telegram.request._httpxrequest import HTTPXRequest
|
||||
from telegram.warnings import PTBDeprecationWarning
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from telegram import Update
|
||||
@@ -122,9 +124,6 @@ class ApplicationBuilder(Generic[BT, CCT, UD, CD, BD, JQ]):
|
||||
.. seealso:: :wiki:`Your First Bot <Extensions---Your-first-Bot>`,
|
||||
:wiki:`Builder Pattern <Builder-Pattern>`
|
||||
|
||||
.. versionchanged:: 22.0
|
||||
Removed deprecated methods ``proxy_url`` and ``get_updates_proxy_url``.
|
||||
|
||||
.. _`builder pattern`: https://en.wikipedia.org/wiki/Builder_pattern
|
||||
"""
|
||||
|
||||
@@ -394,7 +393,7 @@ class ApplicationBuilder(Generic[BT, CCT, UD, CD, BD, JQ]):
|
||||
.. seealso:: :paramref:`telegram.Bot.base_url`,
|
||||
:wiki:`Local Bot API Server <Local-Bot-API-Server>`, :meth:`base_file_url`
|
||||
|
||||
.. versionchanged:: 21.11
|
||||
.. versionchanged:: NEXT.VERSION
|
||||
Supports callable input and string formatting.
|
||||
|
||||
Args:
|
||||
@@ -416,7 +415,7 @@ class ApplicationBuilder(Generic[BT, CCT, UD, CD, BD, JQ]):
|
||||
.. seealso:: :paramref:`telegram.Bot.base_file_url`,
|
||||
:wiki:`Local Bot API Server <Local-Bot-API-Server>`, :meth:`base_url`
|
||||
|
||||
.. versionchanged:: 21.11
|
||||
.. versionchanged:: NEXT.VERSION
|
||||
Supports callable input and string formatting.
|
||||
|
||||
Args:
|
||||
@@ -517,6 +516,30 @@ class ApplicationBuilder(Generic[BT, CCT, UD, CD, BD, JQ]):
|
||||
self._connection_pool_size = connection_pool_size
|
||||
return self
|
||||
|
||||
def proxy_url(self: BuilderType, proxy_url: str) -> BuilderType:
|
||||
"""Legacy name for :meth:`proxy`, kept for backward compatibility.
|
||||
|
||||
.. seealso:: :meth:`get_updates_proxy`
|
||||
|
||||
.. deprecated:: 20.7
|
||||
|
||||
Args:
|
||||
proxy_url (:obj:`str` | ``httpx.Proxy`` | ``httpx.URL``): See
|
||||
:paramref:`telegram.ext.ApplicationBuilder.proxy.proxy`.
|
||||
|
||||
Returns:
|
||||
:class:`ApplicationBuilder`: The same builder with the updated argument.
|
||||
"""
|
||||
warn(
|
||||
PTBDeprecationWarning(
|
||||
"20.7",
|
||||
"`ApplicationBuilder.proxy_url` is deprecated. Use `ApplicationBuilder.proxy` "
|
||||
"instead.",
|
||||
),
|
||||
stacklevel=2,
|
||||
)
|
||||
return self.proxy(proxy_url)
|
||||
|
||||
def proxy(self: BuilderType, proxy: Union[str, httpx.Proxy, httpx.URL]) -> BuilderType:
|
||||
"""Sets the proxy for the :paramref:`~telegram.request.HTTPXRequest.proxy`
|
||||
parameter of :attr:`telegram.Bot.request`. Defaults to :obj:`None`.
|
||||
@@ -727,6 +750,30 @@ class ApplicationBuilder(Generic[BT, CCT, UD, CD, BD, JQ]):
|
||||
self._get_updates_connection_pool_size = get_updates_connection_pool_size
|
||||
return self
|
||||
|
||||
def get_updates_proxy_url(self: BuilderType, get_updates_proxy_url: str) -> BuilderType:
|
||||
"""Legacy name for :meth:`get_updates_proxy`, kept for backward compatibility.
|
||||
|
||||
.. seealso:: :meth:`proxy`
|
||||
|
||||
.. deprecated:: 20.7
|
||||
|
||||
Args:
|
||||
get_updates_proxy_url (:obj:`str` | ``httpx.Proxy`` | ``httpx.URL``): See
|
||||
:paramref:`telegram.ext.ApplicationBuilder.get_updates_proxy.get_updates_proxy`.
|
||||
|
||||
Returns:
|
||||
:class:`ApplicationBuilder`: The same builder with the updated argument.
|
||||
"""
|
||||
warn(
|
||||
PTBDeprecationWarning(
|
||||
"20.7",
|
||||
"`ApplicationBuilder.get_updates_proxy_url` is deprecated. Use "
|
||||
"`ApplicationBuilder.get_updates_proxy` instead.",
|
||||
),
|
||||
stacklevel=2,
|
||||
)
|
||||
return self.get_updates_proxy(get_updates_proxy_url)
|
||||
|
||||
def get_updates_proxy(
|
||||
self: BuilderType, get_updates_proxy: Union[str, httpx.Proxy, httpx.URL]
|
||||
) -> BuilderType:
|
||||
|
||||
@@ -113,7 +113,7 @@ class BaseUpdateProcessor(AbstractAsyncContextManager["BaseUpdateProcessor"], AB
|
||||
This value is a snapshot of the current number of updates being processed. It may
|
||||
change immediately after being read.
|
||||
|
||||
.. versionadded:: 21.11
|
||||
.. versionadded:: NEXT.VERSION
|
||||
"""
|
||||
return self.max_concurrent_updates - self._semaphore.current_value
|
||||
|
||||
|
||||
@@ -17,8 +17,9 @@
|
||||
# You should have received a copy of the GNU Lesser Public License
|
||||
# along with this program. If not, see [http://www.gnu.org/licenses/].
|
||||
"""This module contains the CallbackContext class."""
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Awaitable, Generator
|
||||
from contextlib import AbstractAsyncContextManager
|
||||
from re import Match
|
||||
from typing import TYPE_CHECKING, Any, Generic, NoReturn, Optional, TypeVar, Union
|
||||
|
||||
@@ -26,12 +27,13 @@ from telegram._callbackquery import CallbackQuery
|
||||
from telegram._update import Update
|
||||
from telegram._utils.warnings import warn
|
||||
from telegram.ext._extbot import ExtBot
|
||||
from telegram.ext._fsm import FiniteStateMachine, State
|
||||
from telegram.ext._utils.types import BD, BT, CD, UD
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from asyncio import Future, Queue
|
||||
|
||||
from telegram.ext import Application, Job, JobQueue
|
||||
from telegram.ext import Application, Job, JobQueue, StateInfo
|
||||
from telegram.ext._utils.types import CCT
|
||||
|
||||
_STORING_DATA_WIKI = (
|
||||
@@ -121,6 +123,7 @@ class CallbackContext(Generic[BT, UD, CD, BD]):
|
||||
"args",
|
||||
"coroutine",
|
||||
"error",
|
||||
"fsm_state_info",
|
||||
"job",
|
||||
"matches",
|
||||
)
|
||||
@@ -141,6 +144,7 @@ class CallbackContext(Generic[BT, UD, CD, BD]):
|
||||
self.coroutine: Optional[
|
||||
Union[Generator[Optional[Future[object]], None, Any], Awaitable[Any]]
|
||||
] = None
|
||||
self.fsm_state_info: StateInfo = None # type: ignore[assignment]
|
||||
|
||||
@property
|
||||
def application(self) -> "Application[BT, ST, UD, CD, BD, Any]":
|
||||
@@ -269,6 +273,22 @@ class CallbackContext(Generic[BT, UD, CD, BD]):
|
||||
"telegram.Bot does not allow for arbitrary callback data."
|
||||
)
|
||||
|
||||
@property
|
||||
def fsm(self) -> FiniteStateMachine:
|
||||
return self.application.fsm
|
||||
|
||||
def fsm_semaphore(self) -> asyncio.Lock:
|
||||
return self.fsm.get_lock(self.fsm_state_info.key)
|
||||
|
||||
async def set_state(self, state: State) -> None:
|
||||
await self.fsm.set_state(self.fsm_state_info.key, state, self.fsm_state_info.version)
|
||||
|
||||
def set_state_nowait(self, state: State) -> None:
|
||||
self.fsm.set_state_nowait(self.fsm_state_info.key, state, self.fsm_state_info.version)
|
||||
|
||||
def as_fsm_state(self, state: State) -> AbstractAsyncContextManager[None]:
|
||||
return self.fsm.as_state(self.fsm_state_info.key, state)
|
||||
|
||||
@classmethod
|
||||
def from_error(
|
||||
cls: type["CCT"],
|
||||
|
||||
+80
-12
@@ -18,15 +18,14 @@
|
||||
# along with this program. If not, see [http://www.gnu.org/licenses/].
|
||||
"""This module contains the class Defaults, which allows passing default values to Application."""
|
||||
import datetime as dtm
|
||||
from typing import TYPE_CHECKING, Any, NoReturn, Optional, final
|
||||
from typing import Any, NoReturn, Optional, final
|
||||
|
||||
from telegram import LinkPreviewOptions
|
||||
from telegram._utils.datetime import UTC
|
||||
from telegram._utils.types import ODVInput
|
||||
from telegram._utils.warnings import warn
|
||||
from telegram.warnings import PTBDeprecationWarning
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from telegram import LinkPreviewOptions
|
||||
|
||||
|
||||
@final
|
||||
class Defaults:
|
||||
@@ -39,15 +38,23 @@ class Defaults:
|
||||
Removed the argument and attribute ``timeout``. Specify default timeout behavior for the
|
||||
networking backend directly via :class:`telegram.ext.ApplicationBuilder` instead.
|
||||
|
||||
.. versionchanged:: 22.0
|
||||
Removed deprecated arguments and properties ``disable_web_page_preview`` and ``quote``.
|
||||
Use :paramref:`link_preview_options` and :paramref:`do_quote` instead.
|
||||
|
||||
Parameters:
|
||||
parse_mode (:obj:`str`, optional): |parse_mode|
|
||||
disable_notification (:obj:`bool`, optional): |disable_notification|
|
||||
disable_web_page_preview (:obj:`bool`, optional): Disables link previews for links in this
|
||||
message. Mutually exclusive with :paramref:`link_preview_options`.
|
||||
|
||||
.. deprecated:: 20.8
|
||||
Use :paramref:`link_preview_options` instead. This parameter will be removed in
|
||||
future versions.
|
||||
|
||||
allow_sending_without_reply (:obj:`bool`, optional): |allow_sending_without_reply|.
|
||||
Will be used for :attr:`telegram.ReplyParameters.allow_sending_without_reply`.
|
||||
quote (:obj:`bool`, optional): |reply_quote|
|
||||
|
||||
.. deprecated:: 20.8
|
||||
Use :paramref:`do_quote` instead. This parameter will be removed in future
|
||||
versions.
|
||||
tzinfo (:class:`datetime.tzinfo`, optional): A timezone to be used for all date(time)
|
||||
inputs appearing throughout PTB, i.e. if a timezone naive date(time) object is passed
|
||||
somewhere, it will be assumed to be in :paramref:`tzinfo`. Defaults to
|
||||
@@ -128,6 +135,8 @@ class Defaults:
|
||||
self,
|
||||
parse_mode: Optional[str] = None,
|
||||
disable_notification: Optional[bool] = None,
|
||||
disable_web_page_preview: Optional[bool] = None,
|
||||
quote: Optional[bool] = None,
|
||||
tzinfo: dtm.tzinfo = UTC,
|
||||
block: bool = True,
|
||||
allow_sending_without_reply: Optional[bool] = None,
|
||||
@@ -155,9 +164,37 @@ class Defaults:
|
||||
stacklevel=2,
|
||||
)
|
||||
|
||||
self._link_preview_options = link_preview_options
|
||||
self._do_quote = do_quote
|
||||
if disable_web_page_preview is not None and link_preview_options is not None:
|
||||
raise ValueError(
|
||||
"`disable_web_page_preview` and `link_preview_options` are mutually exclusive."
|
||||
)
|
||||
if quote is not None and do_quote is not None:
|
||||
raise ValueError("`quote` and `do_quote` are mutually exclusive")
|
||||
if disable_web_page_preview is not None:
|
||||
warn(
|
||||
PTBDeprecationWarning(
|
||||
"20.8",
|
||||
"`Defaults.disable_web_page_preview` is deprecated. Use "
|
||||
"`Defaults.link_preview_options` instead.",
|
||||
),
|
||||
stacklevel=2,
|
||||
)
|
||||
self._link_preview_options: Optional[LinkPreviewOptions] = LinkPreviewOptions(
|
||||
is_disabled=disable_web_page_preview
|
||||
)
|
||||
else:
|
||||
self._link_preview_options = link_preview_options
|
||||
|
||||
if quote is not None:
|
||||
warn(
|
||||
PTBDeprecationWarning(
|
||||
"20.8", "`Defaults.quote` is deprecated. Use `Defaults.do_quote` instead."
|
||||
),
|
||||
stacklevel=2,
|
||||
)
|
||||
self._do_quote: Optional[bool] = quote
|
||||
else:
|
||||
self._do_quote = do_quote
|
||||
# Gather all defaults that actually have a default value
|
||||
self._api_defaults = {}
|
||||
for kwarg in (
|
||||
@@ -186,9 +223,9 @@ class Defaults:
|
||||
(
|
||||
self._parse_mode,
|
||||
self._disable_notification,
|
||||
self._link_preview_options,
|
||||
self.disable_web_page_preview,
|
||||
self._allow_sending_without_reply,
|
||||
self._do_quote,
|
||||
self.quote,
|
||||
self._tzinfo,
|
||||
self._block,
|
||||
self._protect_content,
|
||||
@@ -292,6 +329,23 @@ class Defaults:
|
||||
"You can not assign a new value to disable_notification after initialization."
|
||||
)
|
||||
|
||||
@property
|
||||
def disable_web_page_preview(self) -> ODVInput[bool]:
|
||||
""":obj:`bool`: Optional. Disables link previews for links in all outgoing
|
||||
messages.
|
||||
|
||||
.. deprecated:: 20.8
|
||||
Use :attr:`link_preview_options` instead. This attribute will be removed in future
|
||||
versions.
|
||||
"""
|
||||
return self._link_preview_options.is_disabled if self._link_preview_options else None
|
||||
|
||||
@disable_web_page_preview.setter
|
||||
def disable_web_page_preview(self, _: object) -> NoReturn:
|
||||
raise AttributeError(
|
||||
"You can not assign a new value to disable_web_page_preview after initialization."
|
||||
)
|
||||
|
||||
@property
|
||||
def allow_sending_without_reply(self) -> Optional[bool]:
|
||||
""":obj:`bool`: Optional. Pass :obj:`True`, if the message
|
||||
@@ -305,6 +359,20 @@ class Defaults:
|
||||
"You can not assign a new value to allow_sending_without_reply after initialization."
|
||||
)
|
||||
|
||||
@property
|
||||
def quote(self) -> Optional[bool]:
|
||||
""":obj:`bool`: Optional. |reply_quote|
|
||||
|
||||
.. deprecated:: 20.8
|
||||
Use :attr:`do_quote` instead. This attribute will be removed in future
|
||||
versions.
|
||||
"""
|
||||
return self._do_quote if self._do_quote is not None else None
|
||||
|
||||
@quote.setter
|
||||
def quote(self, _: object) -> NoReturn:
|
||||
raise AttributeError("You can not assign a new value to quote after initialization.")
|
||||
|
||||
@property
|
||||
def tzinfo(self) -> dtm.tzinfo:
|
||||
""":obj:`tzinfo`: A timezone to be used for all date(time) objects appearing
|
||||
|
||||
+4
-14
@@ -815,7 +815,6 @@ class ExtBot(Bot, Generic[RLARGS]):
|
||||
reply_parameters: Optional["ReplyParameters"] = None,
|
||||
show_caption_above_media: Optional[bool] = None,
|
||||
allow_paid_broadcast: Optional[bool] = None,
|
||||
video_start_timestamp: Optional[int] = None,
|
||||
*,
|
||||
reply_to_message_id: Optional[int] = None,
|
||||
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
|
||||
@@ -832,7 +831,6 @@ class ExtBot(Bot, Generic[RLARGS]):
|
||||
from_chat_id=from_chat_id,
|
||||
message_id=message_id,
|
||||
caption=caption,
|
||||
video_start_timestamp=video_start_timestamp,
|
||||
parse_mode=parse_mode,
|
||||
caption_entities=caption_entities,
|
||||
disable_notification=disable_notification,
|
||||
@@ -1197,9 +1195,9 @@ class ExtBot(Bot, Generic[RLARGS]):
|
||||
title: str,
|
||||
description: str,
|
||||
payload: str,
|
||||
provider_token: Optional[str],
|
||||
currency: str,
|
||||
prices: Sequence["LabeledPrice"],
|
||||
provider_token: Optional[str] = None,
|
||||
max_tip_amount: Optional[int] = None,
|
||||
suggested_tip_amounts: Optional[Sequence[int]] = None,
|
||||
provider_data: Optional[Union[str, object]] = None,
|
||||
@@ -1754,7 +1752,6 @@ class ExtBot(Bot, Generic[RLARGS]):
|
||||
disable_notification: ODVInput[bool] = DEFAULT_NONE,
|
||||
protect_content: ODVInput[bool] = DEFAULT_NONE,
|
||||
message_thread_id: Optional[int] = None,
|
||||
video_start_timestamp: Optional[int] = None,
|
||||
*,
|
||||
read_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
write_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
@@ -1767,7 +1764,6 @@ class ExtBot(Bot, Generic[RLARGS]):
|
||||
chat_id=chat_id,
|
||||
from_chat_id=from_chat_id,
|
||||
message_id=message_id,
|
||||
video_start_timestamp=video_start_timestamp,
|
||||
disable_notification=disable_notification,
|
||||
protect_content=protect_content,
|
||||
message_thread_id=message_thread_id,
|
||||
@@ -2772,9 +2768,9 @@ class ExtBot(Bot, Generic[RLARGS]):
|
||||
title: str,
|
||||
description: str,
|
||||
payload: str,
|
||||
provider_token: Optional[str],
|
||||
currency: str,
|
||||
prices: Sequence["LabeledPrice"],
|
||||
provider_token: Optional[str] = None,
|
||||
start_parameter: Optional[str] = None,
|
||||
photo_url: Optional[str] = None,
|
||||
photo_size: Optional[int] = None,
|
||||
@@ -3244,8 +3240,6 @@ class ExtBot(Bot, Generic[RLARGS]):
|
||||
message_effect_id: Optional[str] = None,
|
||||
allow_paid_broadcast: Optional[bool] = None,
|
||||
show_caption_above_media: Optional[bool] = None,
|
||||
cover: Optional[FileInput] = None,
|
||||
start_timestamp: Optional[int] = None,
|
||||
*,
|
||||
reply_to_message_id: Optional[int] = None,
|
||||
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
|
||||
@@ -3276,8 +3270,6 @@ class ExtBot(Bot, Generic[RLARGS]):
|
||||
business_connection_id=business_connection_id,
|
||||
has_spoiler=has_spoiler,
|
||||
thumbnail=thumbnail,
|
||||
cover=cover,
|
||||
start_timestamp=start_timestamp,
|
||||
filename=filename,
|
||||
reply_parameters=reply_parameters,
|
||||
read_timeout=read_timeout,
|
||||
@@ -4476,13 +4468,12 @@ class ExtBot(Bot, Generic[RLARGS]):
|
||||
|
||||
async def send_gift(
|
||||
self,
|
||||
user_id: Optional[int] = None,
|
||||
gift_id: Union[str, Gift] = None, # type: ignore
|
||||
user_id: int,
|
||||
gift_id: Union[str, Gift],
|
||||
text: Optional[str] = None,
|
||||
text_parse_mode: ODVInput[str] = DEFAULT_NONE,
|
||||
text_entities: Optional[Sequence["MessageEntity"]] = None,
|
||||
pay_for_upgrade: Optional[bool] = None,
|
||||
chat_id: Optional[Union[str, int]] = None,
|
||||
*,
|
||||
read_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
write_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
@@ -4493,7 +4484,6 @@ class ExtBot(Bot, Generic[RLARGS]):
|
||||
) -> bool:
|
||||
return await super().send_gift(
|
||||
user_id=user_id,
|
||||
chat_id=chat_id,
|
||||
gift_id=gift_id,
|
||||
text=text,
|
||||
text_parse_mode=text_parse_mode,
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
"""Private Submbodule for finite state machine implementation."""
|
||||
|
||||
__all__ = ["FiniteStateMachine", "SingleStateMachine", "State", "StateInfo"]
|
||||
|
||||
from .machine import FiniteStateMachine, SingleStateMachine, StateInfo
|
||||
from .states import State
|
||||
@@ -0,0 +1,200 @@
|
||||
"""This Module contains the FiniteStateMachine class and the built-in subclass SingleStateMachine.
|
||||
"""
|
||||
|
||||
import abc
|
||||
import asyncio
|
||||
import contextlib
|
||||
import datetime as dtm
|
||||
import logging
|
||||
import time
|
||||
import weakref
|
||||
from collections import defaultdict, deque
|
||||
from collections.abc import AsyncIterator, Hashable, Mapping, MutableSequence, Sequence
|
||||
from types import MappingProxyType
|
||||
from typing import TYPE_CHECKING, Any, Generic, Literal, Optional, TypeVar, Union, overload
|
||||
|
||||
from telegram.ext._fsm.states import State
|
||||
from telegram.ext._utils.types import JobCallback
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import MutableMapping
|
||||
|
||||
from telegram.ext import JobQueue
|
||||
|
||||
_KT = TypeVar("_KT", bound=Hashable)
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
_LOGGER.setLevel(logging.DEBUG)
|
||||
|
||||
|
||||
class StateInfo(Generic[_KT]):
|
||||
def __init__(self: "StateInfo[_KT]", key: _KT, state: State, version: int) -> None:
|
||||
self.key: _KT = key
|
||||
self.state: State = state
|
||||
self.version: int = version
|
||||
|
||||
|
||||
class FiniteStateMachine(abc.ABC, Generic[_KT]):
|
||||
def __init__(self) -> None:
|
||||
self._locks: MutableMapping[_KT, asyncio.Lock] = weakref.WeakValueDictionary()
|
||||
|
||||
# There is likely litte benefit for a user to customize how exactly the states are stored
|
||||
# and accessed. So we make this private and only provide a read-only view.
|
||||
self.__states: dict[_KT, tuple[State, int]] = defaultdict(
|
||||
lambda: (State.IDLE, time.perf_counter_ns())
|
||||
)
|
||||
self._states = MappingProxyType(self.__states)
|
||||
|
||||
self.__job_queue: Optional[weakref.ReferenceType[JobQueue]] = None
|
||||
self.__history: Mapping[_KT, MutableSequence[State]] = defaultdict(
|
||||
lambda: deque(maxlen=10)
|
||||
)
|
||||
|
||||
@property
|
||||
def states(self) -> Mapping[_KT, tuple[State, int]]:
|
||||
return self._states
|
||||
|
||||
def store_state_history(self, key: _KT, state: State) -> None:
|
||||
# Making this public so that users can override if they want to customize the history
|
||||
# E.g., they could want to store more/fewer states, also depending on the key
|
||||
self.__history[key].append(state)
|
||||
|
||||
def get_state_history(self, key: _KT) -> Sequence[State]:
|
||||
return list(self.__history[key])
|
||||
|
||||
def get_lock(self, key: _KT) -> asyncio.Lock:
|
||||
"""Returns a lock that is unique for this key at runtime.
|
||||
It can be used to prevent concurrent access to resources associated to this key.
|
||||
"""
|
||||
return self._locks.setdefault(key, asyncio.Lock())
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_state_info(self, update: object) -> StateInfo[_KT]:
|
||||
"""Returns exactly one active state for the update.
|
||||
If more than one stored key applies to the update, one must be chosen.
|
||||
It's recommended to select the most specific one.
|
||||
|
||||
Example:
|
||||
The state of a chat, a user or a user in a specific chat could be tracked.
|
||||
For a message in that chat, the state of the user in that chat should be returned if
|
||||
available. Otherwise, the state of the chat should be returned.
|
||||
|
||||
Important:
|
||||
This must be an atomic operation and not e.g. wait for a lock.
|
||||
Instead, if necessary, return a special state indicating that the key is currently
|
||||
busy.
|
||||
"""
|
||||
|
||||
def _do_set_state(
|
||||
self, key: _KT, state: State, version: Optional[int] = None
|
||||
) -> StateInfo[_KT]:
|
||||
"""Protected method to set the state for the specified key.
|
||||
|
||||
The version can be optionally used for optimistic locking. If the version does not match
|
||||
the current version, the state should not be updated.
|
||||
|
||||
Important:
|
||||
This should be used exclusively by methods of this class and subclasses.
|
||||
It should *not* be called directly by users of this class!
|
||||
"""
|
||||
_LOGGER.debug("Setting %s state to %s", key, state)
|
||||
if state is State.ANY:
|
||||
raise ValueError("State.ANY is not supported in set_state")
|
||||
|
||||
if version and version != self._states.get(key, (None, None))[1]:
|
||||
raise ValueError("Optimistic locking failed. Not updating state.")
|
||||
|
||||
if jq := self._get_job_queue(raise_exception=False):
|
||||
# This is a rather tight coupling between FSM and JobQueue
|
||||
# Not sure if we like that. Makes it even harder to replace JobQueue
|
||||
# (or the JQ implementation) with something else.
|
||||
# The upside is that we don't need to maintain any additional internal state
|
||||
# for the jobs and persistence is handled by the JobQueue.
|
||||
cancel_jobs = jq.jobs(pattern=str(hash(key)))
|
||||
for job in cancel_jobs:
|
||||
_LOGGER.debug("Cancelling timeout job %s", job)
|
||||
job.schedule_removal()
|
||||
|
||||
# important to use time.perf_counter_ns() here, as time_ns() is not monotonic
|
||||
self.__states[key] = (state, time.perf_counter_ns())
|
||||
# Doing this *after* do_set_state so that any exceptions are raised before the history
|
||||
# is updated
|
||||
self.store_state_history(key, state)
|
||||
return StateInfo(key, state, self._states[key][1])
|
||||
|
||||
async def set_state(self, key: _KT, state: State, version: Optional[int] = None) -> None:
|
||||
"""Store the state for the specified key."""
|
||||
async with self.get_lock(key):
|
||||
self._do_set_state(key, state, version)
|
||||
|
||||
def set_state_nowait(self, key: _KT, state: State, version: Optional[int] = None) -> None:
|
||||
"""Store the state for the specified key without waiting for a lock."""
|
||||
if self.get_lock(key).locked():
|
||||
raise asyncio.InvalidStateError("Lock is locked")
|
||||
self._do_set_state(key, state, version)
|
||||
|
||||
@contextlib.asynccontextmanager
|
||||
async def as_state(self, key: _KT, state: State) -> AsyncIterator[None]:
|
||||
"""Context manager to set the state for the specified key and reset it afterwards."""
|
||||
async with self.get_lock(key):
|
||||
current_state, current_version = self.states[key]
|
||||
new_version = self._do_set_state(key, state, current_version).version
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
self._do_set_state(key, current_state, new_version)
|
||||
|
||||
@staticmethod
|
||||
def _build_job_name(keys: Sequence[_KT]) -> str:
|
||||
return f"FSM_Job_{'_'.join(str(hash(k)) for k in keys)}"
|
||||
|
||||
def set_job_queue(self, job_queue: "JobQueue") -> None:
|
||||
self.__job_queue = weakref.ref(job_queue)
|
||||
|
||||
@overload
|
||||
def _get_job_queue(self, raise_exception: Literal[False]) -> Optional["JobQueue"]: ...
|
||||
|
||||
@overload
|
||||
def _get_job_queue(self) -> "JobQueue": ...
|
||||
|
||||
def _get_job_queue(self, raise_exception: bool = True) -> Optional["JobQueue"]:
|
||||
if self.__job_queue is None:
|
||||
if raise_exception:
|
||||
raise RuntimeError("JobQueue not set")
|
||||
return None
|
||||
job_queue = self.__job_queue()
|
||||
if job_queue is None:
|
||||
if raise_exception:
|
||||
raise RuntimeError("JobQueue was garbage collected")
|
||||
return None
|
||||
return job_queue
|
||||
|
||||
def schedule_timeout(
|
||||
self,
|
||||
callback: JobCallback,
|
||||
when: Union[float, dtm.timedelta, dtm.datetime, dtm.time],
|
||||
cancel_keys: Optional[Sequence[_KT]] = None,
|
||||
job_kwargs: Optional[dict[str, Any]] = None,
|
||||
) -> None:
|
||||
"""Schedule a timeout job. This is a thin wrapper around JobQueue.run_once.
|
||||
The callback will have to take care of resetting any state if necessary.
|
||||
Pass cancel_keys to automatically cancel the job when a new state is set for any of the
|
||||
keys.
|
||||
"""
|
||||
job_kwargs = job_kwargs or {}
|
||||
if cancel_keys:
|
||||
if "name" in job_kwargs:
|
||||
raise ValueError("job_kwargs must not contain a 'name' key")
|
||||
job_kwargs["name"] = self._build_job_name(cancel_keys)
|
||||
self._get_job_queue().run_once(callback, when, **job_kwargs)
|
||||
_LOGGER.debug(
|
||||
"Scheduled timeout. Will be cancelled when a new set state is for either of: %s",
|
||||
cancel_keys or [],
|
||||
)
|
||||
|
||||
|
||||
class SingleStateMachine(FiniteStateMachine[None]):
|
||||
def get_state_info(self, update: object) -> StateInfo[None]: # noqa: ARG002
|
||||
return StateInfo(None, State.IDLE, 0)
|
||||
|
||||
def do_set_state(self, key: None, state: State) -> None:
|
||||
pass
|
||||
@@ -0,0 +1,114 @@
|
||||
"""This Module contains implementations of State classes for Finite State Machines"""
|
||||
|
||||
import abc
|
||||
import contextlib
|
||||
from typing import ClassVar, Optional
|
||||
from uuid import uuid4
|
||||
|
||||
|
||||
class State(abc.ABC):
|
||||
__knows_uids: ClassVar[set[str]] = set()
|
||||
__not_cache: ClassVar[dict[str, "_NOTState"]] = {}
|
||||
__or_cache: ClassVar[dict[tuple[str, str], "_ORState"]] = {}
|
||||
__and_cache: ClassVar[dict[tuple[str, str], "_ANDState"]] = {}
|
||||
__xor_cache: ClassVar[dict[tuple[str, str], "_XORState"]] = {}
|
||||
|
||||
IDLE: "State"
|
||||
"""Default State for all Finite State Machines"""
|
||||
ANY: "State"
|
||||
"""Special State that matches any other State. Useful to define fallback behavior.
|
||||
*Not* supported in ``set_state`` method of FSMs.
|
||||
"""
|
||||
|
||||
def __init__(self, uid: Optional[str] = None):
|
||||
effective_uid = uid or uuid4().hex
|
||||
if effective_uid in self.__knows_uids:
|
||||
raise ValueError(f"Duplicate UID: {effective_uid} already registered")
|
||||
self._uid = effective_uid
|
||||
self.__knows_uids.add(effective_uid)
|
||||
|
||||
def __invert__(self) -> "_NOTState":
|
||||
with contextlib.suppress(KeyError):
|
||||
return self.__not_cache[self.uid]
|
||||
return self.__not_cache.setdefault(self.uid, _NOTState(self))
|
||||
|
||||
def __or__(self, other: "State") -> "_ORState":
|
||||
key = (self.uid, other.uid)
|
||||
with contextlib.suppress(KeyError):
|
||||
return self.__or_cache[key]
|
||||
return self.__or_cache.setdefault(key, _ORState(self, other))
|
||||
|
||||
def __and__(self, other: "State") -> "_ANDState":
|
||||
key = (self.uid, other.uid)
|
||||
with contextlib.suppress(KeyError):
|
||||
return self.__and_cache[key]
|
||||
return self.__and_cache.setdefault(key, _ANDState(self, other))
|
||||
|
||||
def __xor__(self, other: "State") -> "_XORState":
|
||||
key = (self.uid, other.uid)
|
||||
with contextlib.suppress(KeyError):
|
||||
return self.__xor_cache[key]
|
||||
return self.__xor_cache.setdefault(key, _XORState(self, other))
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<{self.__class__.__name__}: {self.uid}>"
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.uid
|
||||
|
||||
@property
|
||||
def uid(self) -> str:
|
||||
return self._uid
|
||||
|
||||
def matches(self, state: "State") -> bool:
|
||||
if isinstance(state, (_NOTState, _ANDState, _ORState, _XORState)):
|
||||
return state.matches(self)
|
||||
return self.uid == state.uid
|
||||
|
||||
|
||||
class _AnyState(State):
|
||||
def matches(self, state: "State") -> bool: # noqa: ARG002
|
||||
return True
|
||||
|
||||
|
||||
State.IDLE = State("IDLE")
|
||||
State.ANY = _AnyState("ANY")
|
||||
|
||||
|
||||
class _XORState(State):
|
||||
def __init__(self, state_one: State, state_two: State):
|
||||
super().__init__(uid=f"({state_one.uid})^({state_two.uid})")
|
||||
self._state_one = state_one
|
||||
self._state_two = state_two
|
||||
|
||||
def matches(self, state: "State") -> bool:
|
||||
return self._state_one.matches(state) ^ self._state_two.matches(state)
|
||||
|
||||
|
||||
class _ORState(State):
|
||||
def __init__(self, state_one: State, state_two: State):
|
||||
super().__init__(uid=f"({state_one.uid})|({state_two.uid})")
|
||||
self._state_one = state_one
|
||||
self._state_two = state_two
|
||||
|
||||
def matches(self, state: "State") -> bool:
|
||||
return self._state_one.matches(state) or self._state_two.matches(state)
|
||||
|
||||
|
||||
class _ANDState(State):
|
||||
def __init__(self, state_one: State, state_two: State):
|
||||
super().__init__(uid=f"({state_one.uid})&({state_two.uid})")
|
||||
self._state_one = state_one
|
||||
self._state_two = state_two
|
||||
|
||||
def matches(self, state: "State") -> bool:
|
||||
return self._state_one.matches(state) and self._state_two.matches(state)
|
||||
|
||||
|
||||
class _NOTState(State):
|
||||
def __init__(self, state: State):
|
||||
super().__init__(uid=f"!({state.uid})")
|
||||
self._state = state
|
||||
|
||||
def matches(self, state: "State") -> bool:
|
||||
return not self._state.matches(state)
|
||||
@@ -97,7 +97,7 @@ class JobQueue(Generic[CCT]):
|
||||
|
||||
"""
|
||||
|
||||
__slots__ = ("_application", "_executor", "scheduler")
|
||||
__slots__ = ("__weakref__", "_application", "_executor", "scheduler")
|
||||
_CRON_MAPPING = ("sun", "mon", "tue", "wed", "thu", "fri", "sat")
|
||||
|
||||
def __init__(self) -> None:
|
||||
|
||||
+166
-36
@@ -26,12 +26,11 @@ from pathlib import Path
|
||||
from types import TracebackType
|
||||
from typing import TYPE_CHECKING, Any, Callable, Optional, TypeVar, Union
|
||||
|
||||
from telegram._utils.defaultvalue import DEFAULT_80, DEFAULT_IP, DefaultValue
|
||||
from telegram._utils.defaultvalue import DEFAULT_80, DEFAULT_IP, DEFAULT_NONE, DefaultValue
|
||||
from telegram._utils.logging import get_logger
|
||||
from telegram._utils.repr import build_repr_with_selected_attrs
|
||||
from telegram._utils.types import DVType
|
||||
from telegram.error import TelegramError
|
||||
from telegram.ext._utils.networkloop import network_retry_loop
|
||||
from telegram._utils.types import DVType, ODVInput
|
||||
from telegram.error import InvalidToken, RetryAfter, TelegramError, TimedOut
|
||||
|
||||
try:
|
||||
from telegram.ext._utils.webhookhandler import WebhookAppClass, WebhookServer
|
||||
@@ -207,7 +206,11 @@ class Updater(contextlib.AbstractAsyncContextManager["Updater"]):
|
||||
self,
|
||||
poll_interval: float = 0.0,
|
||||
timeout: int = 10,
|
||||
bootstrap_retries: int = 0,
|
||||
bootstrap_retries: int = -1,
|
||||
read_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
write_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
connect_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
pool_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
allowed_updates: Optional[Sequence[str]] = None,
|
||||
drop_pending_updates: Optional[bool] = None,
|
||||
error_callback: Optional[Callable[[TelegramError], None]] = None,
|
||||
@@ -217,27 +220,52 @@ class Updater(contextlib.AbstractAsyncContextManager["Updater"]):
|
||||
.. versionchanged:: 20.0
|
||||
Removed the ``clean`` argument in favor of :paramref:`drop_pending_updates`.
|
||||
|
||||
.. versionchanged:: 22.0
|
||||
Removed the deprecated arguments ``read_timeout``, ``write_timeout``,
|
||||
``connect_timeout``, and ``pool_timeout`` in favor of setting the timeouts via
|
||||
the corresponding methods of :class:`telegram.ext.ApplicationBuilder`. or
|
||||
by specifying the timeout via :paramref:`telegram.Bot.get_updates_request`.
|
||||
|
||||
Args:
|
||||
poll_interval (:obj:`float`, optional): Time to wait between polling updates from
|
||||
Telegram in seconds. Default is ``0.0``.
|
||||
timeout (:obj:`int`, optional): Passed to
|
||||
:paramref:`telegram.Bot.get_updates.timeout`. Defaults to ``10`` seconds.
|
||||
bootstrap_retries (:obj:`int`, optional): Whether the bootstrapping phase of
|
||||
will retry on failures on the Telegram server.
|
||||
bootstrap_retries (:obj:`int`, optional): Whether the bootstrapping phase of the
|
||||
:class:`telegram.ext.Updater` will retry on failures on the Telegram server.
|
||||
|
||||
* < 0 - retry indefinitely
|
||||
* 0 - no retries (default)
|
||||
* < 0 - retry indefinitely (default)
|
||||
* 0 - no retries
|
||||
* > 0 - retry up to X times
|
||||
read_timeout (:obj:`float`, optional): Value to pass to
|
||||
:paramref:`telegram.Bot.get_updates.read_timeout`. Defaults to
|
||||
:attr:`~telegram.request.BaseRequest.DEFAULT_NONE`.
|
||||
|
||||
.. versionchanged:: 21.11
|
||||
The default value will be changed to from ``-1`` to ``0``. Indefinite retries
|
||||
during bootstrapping are not recommended.
|
||||
.. versionchanged:: 20.7
|
||||
Defaults to :attr:`~telegram.request.BaseRequest.DEFAULT_NONE` instead of
|
||||
``2``.
|
||||
.. deprecated:: 20.7
|
||||
Deprecated in favor of setting the timeout via
|
||||
:meth:`telegram.ext.ApplicationBuilder.get_updates_read_timeout` or
|
||||
:paramref:`telegram.Bot.get_updates_request`.
|
||||
write_timeout (:obj:`float` | :obj:`None`, optional): Value to pass to
|
||||
:paramref:`telegram.Bot.get_updates.write_timeout`. Defaults to
|
||||
:attr:`~telegram.request.BaseRequest.DEFAULT_NONE`.
|
||||
|
||||
.. deprecated:: 20.7
|
||||
Deprecated in favor of setting the timeout via
|
||||
:meth:`telegram.ext.ApplicationBuilder.get_updates_write_timeout` or
|
||||
:paramref:`telegram.Bot.get_updates_request`.
|
||||
connect_timeout (:obj:`float` | :obj:`None`, optional): Value to pass to
|
||||
:paramref:`telegram.Bot.get_updates.connect_timeout`. Defaults to
|
||||
:attr:`~telegram.request.BaseRequest.DEFAULT_NONE`.
|
||||
|
||||
.. deprecated:: 20.7
|
||||
Deprecated in favor of setting the timeout via
|
||||
:meth:`telegram.ext.ApplicationBuilder.get_updates_connect_timeout` or
|
||||
:paramref:`telegram.Bot.get_updates_request`.
|
||||
pool_timeout (:obj:`float` | :obj:`None`, optional): Value to pass to
|
||||
:paramref:`telegram.Bot.get_updates.pool_timeout`. Defaults to
|
||||
:attr:`~telegram.request.BaseRequest.DEFAULT_NONE`.
|
||||
|
||||
.. deprecated:: 20.7
|
||||
Deprecated in favor of setting the timeout via
|
||||
:meth:`telegram.ext.ApplicationBuilder.get_updates_pool_timeout` or
|
||||
:paramref:`telegram.Bot.get_updates_request`.
|
||||
allowed_updates (Sequence[:obj:`str`], optional): Passed to
|
||||
:meth:`telegram.Bot.get_updates`.
|
||||
|
||||
@@ -291,6 +319,10 @@ class Updater(contextlib.AbstractAsyncContextManager["Updater"]):
|
||||
await self._start_polling(
|
||||
poll_interval=poll_interval,
|
||||
timeout=timeout,
|
||||
read_timeout=read_timeout,
|
||||
write_timeout=write_timeout,
|
||||
connect_timeout=connect_timeout,
|
||||
pool_timeout=pool_timeout,
|
||||
bootstrap_retries=bootstrap_retries,
|
||||
drop_pending_updates=drop_pending_updates,
|
||||
allowed_updates=allowed_updates,
|
||||
@@ -310,6 +342,10 @@ class Updater(contextlib.AbstractAsyncContextManager["Updater"]):
|
||||
self,
|
||||
poll_interval: float,
|
||||
timeout: int,
|
||||
read_timeout: ODVInput[float],
|
||||
write_timeout: ODVInput[float],
|
||||
connect_timeout: ODVInput[float],
|
||||
pool_timeout: ODVInput[float],
|
||||
bootstrap_retries: int,
|
||||
drop_pending_updates: Optional[bool],
|
||||
allowed_updates: Optional[Sequence[str]],
|
||||
@@ -335,6 +371,10 @@ class Updater(contextlib.AbstractAsyncContextManager["Updater"]):
|
||||
updates = await self.bot.get_updates(
|
||||
offset=self._last_update_id,
|
||||
timeout=timeout,
|
||||
read_timeout=read_timeout,
|
||||
connect_timeout=connect_timeout,
|
||||
write_timeout=write_timeout,
|
||||
pool_timeout=pool_timeout,
|
||||
allowed_updates=allowed_updates,
|
||||
)
|
||||
except TelegramError:
|
||||
@@ -369,14 +409,12 @@ class Updater(contextlib.AbstractAsyncContextManager["Updater"]):
|
||||
# updates from Telegram and inserts them in the update queue of the
|
||||
# Application.
|
||||
self.__polling_task = asyncio.create_task(
|
||||
network_retry_loop(
|
||||
is_running=lambda: self.running,
|
||||
self._network_loop_retry(
|
||||
action_cb=polling_action_cb,
|
||||
on_err_cb=error_callback or default_error_callback,
|
||||
description="Polling Updates",
|
||||
description="getting Updates",
|
||||
interval=poll_interval,
|
||||
stop_event=self.__polling_task_stop_event,
|
||||
max_retries=-1,
|
||||
),
|
||||
name="Updater:start_polling:polling_task",
|
||||
)
|
||||
@@ -395,6 +433,10 @@ class Updater(contextlib.AbstractAsyncContextManager["Updater"]):
|
||||
offset=self._last_update_id,
|
||||
# We don't want to do long polling here!
|
||||
timeout=0,
|
||||
read_timeout=read_timeout,
|
||||
connect_timeout=connect_timeout,
|
||||
write_timeout=write_timeout,
|
||||
pool_timeout=pool_timeout,
|
||||
allowed_updates=allowed_updates,
|
||||
)
|
||||
except TelegramError:
|
||||
@@ -465,8 +507,8 @@ class Updater(contextlib.AbstractAsyncContextManager["Updater"]):
|
||||
Telegram servers before actually starting to poll. Default is :obj:`False`.
|
||||
|
||||
.. versionadded :: 13.4
|
||||
bootstrap_retries (:obj:`int`, optional): Whether the bootstrapping phase of
|
||||
will retry on failures on the Telegram server.
|
||||
bootstrap_retries (:obj:`int`, optional): Whether the bootstrapping phase of the
|
||||
:class:`telegram.ext.Updater` will retry on failures on the Telegram server.
|
||||
|
||||
* < 0 - retry indefinitely
|
||||
* 0 - no retries (default)
|
||||
@@ -656,6 +698,78 @@ class Updater(contextlib.AbstractAsyncContextManager["Updater"]):
|
||||
# say differently!
|
||||
return f"{protocol}://{listen}:{port}{url_path}"
|
||||
|
||||
async def _network_loop_retry(
|
||||
self,
|
||||
action_cb: Callable[..., Coroutine],
|
||||
on_err_cb: Callable[[TelegramError], None],
|
||||
description: str,
|
||||
interval: float,
|
||||
stop_event: Optional[asyncio.Event],
|
||||
) -> None:
|
||||
"""Perform a loop calling `action_cb`, retrying after network errors.
|
||||
|
||||
Stop condition for loop: `self.running` evaluates :obj:`False` or return value of
|
||||
`action_cb` evaluates :obj:`False`.
|
||||
|
||||
Args:
|
||||
action_cb (:term:`coroutine function`): Network oriented callback function to call.
|
||||
on_err_cb (:obj:`callable`): Callback to call when TelegramError is caught. Receives
|
||||
the exception object as a parameter.
|
||||
description (:obj:`str`): Description text to use for logs and exception raised.
|
||||
interval (:obj:`float` | :obj:`int`): Interval to sleep between each call to
|
||||
`action_cb`.
|
||||
stop_event (:class:`asyncio.Event` | :obj:`None`): Event to wait on for stopping the
|
||||
loop. Setting the event will make the loop exit even if `action_cb` is currently
|
||||
running.
|
||||
|
||||
"""
|
||||
|
||||
async def do_action() -> bool:
|
||||
if not stop_event:
|
||||
return await action_cb()
|
||||
|
||||
action_cb_task = asyncio.create_task(action_cb())
|
||||
stop_task = asyncio.create_task(stop_event.wait())
|
||||
done, pending = await asyncio.wait(
|
||||
(action_cb_task, stop_task), return_when=asyncio.FIRST_COMPLETED
|
||||
)
|
||||
with contextlib.suppress(asyncio.CancelledError):
|
||||
for task in pending:
|
||||
task.cancel()
|
||||
|
||||
if stop_task in done:
|
||||
_LOGGER.debug("Network loop retry %s was cancelled", description)
|
||||
return False
|
||||
|
||||
return action_cb_task.result()
|
||||
|
||||
_LOGGER.debug("Start network loop retry %s", description)
|
||||
cur_interval = interval
|
||||
while self.running:
|
||||
try:
|
||||
if not await do_action():
|
||||
break
|
||||
except RetryAfter as exc:
|
||||
_LOGGER.info("%s", exc)
|
||||
cur_interval = 0.5 + exc.retry_after
|
||||
except TimedOut as toe:
|
||||
_LOGGER.debug("Timed out %s: %s", description, toe)
|
||||
# If failure is due to timeout, we should retry asap.
|
||||
cur_interval = 0
|
||||
except InvalidToken:
|
||||
_LOGGER.exception("Invalid token; aborting")
|
||||
raise
|
||||
except TelegramError as telegram_exc:
|
||||
on_err_cb(telegram_exc)
|
||||
|
||||
# increase waiting times on subsequent errors up to 30secs
|
||||
cur_interval = 1 if cur_interval == 0 else min(30, 1.5 * cur_interval)
|
||||
else:
|
||||
cur_interval = interval
|
||||
|
||||
if cur_interval:
|
||||
await asyncio.sleep(cur_interval)
|
||||
|
||||
async def _bootstrap(
|
||||
self,
|
||||
max_retries: int,
|
||||
@@ -672,6 +786,7 @@ class Updater(contextlib.AbstractAsyncContextManager["Updater"]):
|
||||
updates if appropriate. If there are unsuccessful attempts, this will retry as specified by
|
||||
:paramref:`max_retries`.
|
||||
"""
|
||||
retries = 0
|
||||
|
||||
async def bootstrap_del_webhook() -> bool:
|
||||
_LOGGER.debug("Deleting webhook")
|
||||
@@ -695,30 +810,45 @@ class Updater(contextlib.AbstractAsyncContextManager["Updater"]):
|
||||
)
|
||||
return False
|
||||
|
||||
def bootstrap_on_err_cb(exc: Exception) -> None:
|
||||
# We need this since retries is an immutable object otherwise and the changes
|
||||
# wouldn't propagate outside of thi function
|
||||
nonlocal retries
|
||||
|
||||
if not isinstance(exc, InvalidToken) and (max_retries < 0 or retries < max_retries):
|
||||
retries += 1
|
||||
_LOGGER.warning(
|
||||
"Failed bootstrap phase; try=%s max_retries=%s", retries, max_retries
|
||||
)
|
||||
else:
|
||||
_LOGGER.error("Failed bootstrap phase after %s retries (%s)", retries, exc)
|
||||
raise exc
|
||||
|
||||
# Dropping pending updates from TG can be efficiently done with the drop_pending_updates
|
||||
# parameter of delete/start_webhook, even in the case of polling. Also, we want to make
|
||||
# sure that no webhook is configured in case of polling, so we just always call
|
||||
# delete_webhook for polling
|
||||
if drop_pending_updates or not webhook_url:
|
||||
await network_retry_loop(
|
||||
is_running=lambda: self.running,
|
||||
action_cb=bootstrap_del_webhook,
|
||||
description="Bootstrap delete Webhook",
|
||||
interval=bootstrap_interval,
|
||||
await self._network_loop_retry(
|
||||
bootstrap_del_webhook,
|
||||
bootstrap_on_err_cb,
|
||||
"bootstrap del webhook",
|
||||
bootstrap_interval,
|
||||
stop_event=None,
|
||||
max_retries=max_retries,
|
||||
)
|
||||
|
||||
# Reset the retries counter for the next _network_loop_retry call
|
||||
retries = 0
|
||||
|
||||
# Restore/set webhook settings, if needed. Again, we don't know ahead if a webhook is set,
|
||||
# so we set it anyhow.
|
||||
if webhook_url:
|
||||
await network_retry_loop(
|
||||
is_running=lambda: self.running,
|
||||
action_cb=bootstrap_set_webhook,
|
||||
description="Bootstrap Set Webhook",
|
||||
interval=bootstrap_interval,
|
||||
await self._network_loop_retry(
|
||||
bootstrap_set_webhook,
|
||||
bootstrap_on_err_cb,
|
||||
"bootstrap set webhook",
|
||||
bootstrap_interval,
|
||||
stop_event=None,
|
||||
max_retries=max_retries,
|
||||
)
|
||||
|
||||
async def stop(self) -> None:
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
# along with this program. If not, see [http://www.gnu.org/licenses/].
|
||||
"""This module contains helper functions related to the std-lib asyncio module.
|
||||
|
||||
.. versionadded:: 21.11
|
||||
.. versionadded:: NEXT.VERSION
|
||||
|
||||
Warning:
|
||||
Contents of this module are intended to be used internally by the library and *not* by the
|
||||
|
||||
@@ -1,152 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# A library that provides a Python interface to the Telegram Bot API
|
||||
# Copyright (C) 2015-2025
|
||||
# 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 network retry loop implementation.
|
||||
Its specifically tailored to handling the Telegram API and its errors.
|
||||
|
||||
.. versionadded:: 21.11
|
||||
|
||||
Hint:
|
||||
It was originally part of the `Updater` class, but as part of #4657 it was extracted into its
|
||||
own module to be used by other parts of the library.
|
||||
|
||||
Warning:
|
||||
Contents of this module are intended to be used internally by the library and *not* by the
|
||||
user. Changes to this module are not considered breaking changes and may not be documented in
|
||||
the changelog.
|
||||
"""
|
||||
import asyncio
|
||||
import contextlib
|
||||
from collections.abc import Coroutine
|
||||
from typing import Callable, Optional
|
||||
|
||||
from telegram._utils.logging import get_logger
|
||||
from telegram.error import InvalidToken, RetryAfter, TelegramError, TimedOut
|
||||
|
||||
_LOGGER = get_logger(__name__)
|
||||
|
||||
|
||||
async def network_retry_loop(
|
||||
*,
|
||||
action_cb: Callable[..., Coroutine],
|
||||
on_err_cb: Optional[Callable[[TelegramError], None]] = None,
|
||||
description: str,
|
||||
interval: float,
|
||||
stop_event: Optional[asyncio.Event] = None,
|
||||
is_running: Optional[Callable[[], bool]] = None,
|
||||
max_retries: int,
|
||||
) -> None:
|
||||
"""Perform a loop calling `action_cb`, retrying after network errors.
|
||||
|
||||
Stop condition for loop:
|
||||
* `is_running()` evaluates :obj:`False` or
|
||||
* return value of `action_cb` evaluates :obj:`False`
|
||||
* or `stop_event` is set.
|
||||
* or `max_retries` is reached.
|
||||
|
||||
Args:
|
||||
action_cb (:term:`coroutine function`): Network oriented callback function to call.
|
||||
on_err_cb (:obj:`callable`): Optional. Callback to call when TelegramError is caught.
|
||||
Receives the exception object as a parameter.
|
||||
|
||||
Hint:
|
||||
Only required if you want to handle the error in a special way. Logging about
|
||||
the error is already handled by the loop.
|
||||
|
||||
Important:
|
||||
Must not raise exceptions! If it does, the loop will be aborted.
|
||||
description (:obj:`str`): Description text to use for logs and exception raised.
|
||||
interval (:obj:`float` | :obj:`int`): Interval to sleep between each call to
|
||||
`action_cb`.
|
||||
stop_event (:class:`asyncio.Event` | :obj:`None`): Event to wait on for stopping the
|
||||
loop. Setting the event will make the loop exit even if `action_cb` is currently
|
||||
running. Defaults to :obj:`None`.
|
||||
is_running (:obj:`callable`): Function to check if the loop should continue running.
|
||||
Must return a boolean value. Defaults to `lambda: True`.
|
||||
max_retries (:obj:`int`): Maximum number of retries before stopping the loop.
|
||||
|
||||
* < 0: Retry indefinitely.
|
||||
* 0: No retries.
|
||||
* > 0: Number of retries.
|
||||
|
||||
"""
|
||||
log_prefix = f"Network Retry Loop ({description}):"
|
||||
effective_is_running = is_running or (lambda: True)
|
||||
|
||||
async def do_action() -> bool:
|
||||
if not stop_event:
|
||||
return await action_cb()
|
||||
|
||||
action_cb_task = asyncio.create_task(action_cb())
|
||||
stop_task = asyncio.create_task(stop_event.wait())
|
||||
done, pending = await asyncio.wait(
|
||||
(action_cb_task, stop_task), return_when=asyncio.FIRST_COMPLETED
|
||||
)
|
||||
with contextlib.suppress(asyncio.CancelledError):
|
||||
for task in pending:
|
||||
task.cancel()
|
||||
|
||||
if stop_task in done:
|
||||
_LOGGER.debug("%s Cancelled", log_prefix)
|
||||
return False
|
||||
|
||||
return action_cb_task.result()
|
||||
|
||||
_LOGGER.debug("%s Starting", log_prefix)
|
||||
cur_interval = interval
|
||||
retries = 0
|
||||
while effective_is_running():
|
||||
try:
|
||||
if not await do_action():
|
||||
break
|
||||
except RetryAfter as exc:
|
||||
slack_time = 0.5
|
||||
_LOGGER.info(
|
||||
"%s %s. Adding %s seconds to the specified time.", log_prefix, exc, slack_time
|
||||
)
|
||||
cur_interval = slack_time + exc.retry_after
|
||||
except TimedOut as toe:
|
||||
_LOGGER.debug("%s Timed out: %s. Retrying immediately.", log_prefix, toe)
|
||||
# If failure is due to timeout, we should retry asap.
|
||||
cur_interval = 0
|
||||
except InvalidToken:
|
||||
_LOGGER.exception("%s Invalid token. Aborting retry loop.", log_prefix)
|
||||
raise
|
||||
except TelegramError as telegram_exc:
|
||||
if on_err_cb:
|
||||
on_err_cb(telegram_exc)
|
||||
|
||||
if max_retries < 0 or retries < max_retries:
|
||||
_LOGGER.debug(
|
||||
"%s Failed run number %s of %s. Retrying.", log_prefix, retries, max_retries
|
||||
)
|
||||
else:
|
||||
_LOGGER.exception(
|
||||
"%s Failed run number %s of %s. Aborting.", log_prefix, retries, max_retries
|
||||
)
|
||||
raise
|
||||
|
||||
# increase waiting times on subsequent errors up to 30secs
|
||||
cur_interval = 1 if cur_interval == 0 else min(30, 1.5 * cur_interval)
|
||||
else:
|
||||
cur_interval = interval
|
||||
finally:
|
||||
retries += 1
|
||||
|
||||
if cur_interval:
|
||||
await asyncio.sleep(cur_interval)
|
||||
+39
-6
@@ -34,9 +34,6 @@ This module contains filters for use with :class:`telegram.ext.MessageHandler`,
|
||||
* Filters which do both (like ``Filters.text``) are now split as ready-to-use version
|
||||
``filters.TEXT`` and class version ``filters.Text(...)``.
|
||||
|
||||
.. versionchanged:: 22.0
|
||||
Removed deprecated attribute `CHAT`.
|
||||
|
||||
"""
|
||||
|
||||
__all__ = (
|
||||
@@ -46,6 +43,7 @@ __all__ = (
|
||||
"AUDIO",
|
||||
"BOOST_ADDED",
|
||||
"CAPTION",
|
||||
"CHAT",
|
||||
"COMMAND",
|
||||
"CONTACT",
|
||||
"EFFECT_ID",
|
||||
@@ -861,6 +859,21 @@ class Chat(_ChatUserBaseFilter):
|
||||
return super()._remove_chat_ids(chat_id)
|
||||
|
||||
|
||||
class _Chat(MessageFilter):
|
||||
__slots__ = ()
|
||||
|
||||
def filter(self, message: Message) -> bool:
|
||||
return bool(message.chat)
|
||||
|
||||
|
||||
CHAT = _Chat(name="filters.CHAT")
|
||||
"""This filter filters *any* message that has a :attr:`telegram.Message.chat`.
|
||||
|
||||
.. deprecated:: 20.8
|
||||
This filter has no effect since :attr:`telegram.Message.chat` is always present.
|
||||
"""
|
||||
|
||||
|
||||
class ChatType: # A convenience namespace for Chat types.
|
||||
"""Subset for filtering the type of chat.
|
||||
|
||||
@@ -1902,9 +1915,6 @@ class StatusUpdate:
|
||||
|
||||
Caution:
|
||||
``filters.StatusUpdate`` itself is *not* a filter, but just a convenience namespace.
|
||||
|
||||
.. versionchanged:: 22.0
|
||||
Removed deprecated attribute `USER_SHARED`.
|
||||
"""
|
||||
|
||||
__slots__ = ()
|
||||
@@ -1938,6 +1948,7 @@ class StatusUpdate:
|
||||
or StatusUpdate.PROXIMITY_ALERT_TRIGGERED.check_update(update)
|
||||
or StatusUpdate.REFUNDED_PAYMENT.check_update(update)
|
||||
or StatusUpdate.USERS_SHARED.check_update(update)
|
||||
or StatusUpdate.USER_SHARED.check_update(update)
|
||||
or StatusUpdate.VIDEO_CHAT_ENDED.check_update(update)
|
||||
or StatusUpdate.VIDEO_CHAT_PARTICIPANTS_INVITED.check_update(update)
|
||||
or StatusUpdate.VIDEO_CHAT_SCHEDULED.check_update(update)
|
||||
@@ -2193,6 +2204,28 @@ class StatusUpdate:
|
||||
.. versionadded:: 21.4
|
||||
"""
|
||||
|
||||
class _UserShared(MessageFilter):
|
||||
__slots__ = ()
|
||||
|
||||
def filter(self, message: Message) -> bool:
|
||||
return bool(message.api_kwargs.get("user_shared"))
|
||||
|
||||
USER_SHARED = _UserShared(name="filters.StatusUpdate.USER_SHARED")
|
||||
"""Messages that contain ``"user_shared"`` in :attr:`telegram.TelegramObject.api_kwargs`.
|
||||
|
||||
Warning:
|
||||
This will only catch the legacy ``user_shared`` field, not the
|
||||
new :attr:`telegram.Message.users_shared` attribute!
|
||||
|
||||
.. versionchanged:: 21.0
|
||||
Now relies on :attr:`telegram.TelegramObject.api_kwargs` as the native attribute
|
||||
``Message.user_shared`` was removed.
|
||||
|
||||
.. versionadded:: 20.1
|
||||
.. deprecated:: 20.8
|
||||
Use :attr:`USERS_SHARED` instead.
|
||||
"""
|
||||
|
||||
class _UsersShared(MessageFilter):
|
||||
__slots__ = ()
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ from telegram._utils.defaultvalue import DefaultValue
|
||||
from telegram._utils.logging import get_logger
|
||||
from telegram._utils.strings import TextEncoding
|
||||
from telegram._utils.types import JSONDict, ODVInput
|
||||
from telegram._utils.warnings import warn
|
||||
from telegram._version import __version__ as ptb_ver
|
||||
from telegram.error import (
|
||||
BadRequest,
|
||||
@@ -41,6 +42,7 @@ from telegram.error import (
|
||||
TelegramError,
|
||||
)
|
||||
from telegram.request._requestdata import RequestData
|
||||
from telegram.warnings import PTBDeprecationWarning
|
||||
|
||||
RT = TypeVar("RT", bound="BaseRequest")
|
||||
|
||||
@@ -131,19 +133,22 @@ class BaseRequest(
|
||||
await self.shutdown()
|
||||
|
||||
@property
|
||||
@abc.abstractmethod
|
||||
def read_timeout(self) -> Optional[float]:
|
||||
"""This property must return the default read timeout in seconds used by this class.
|
||||
More precisely, the returned value should be the one used when
|
||||
:paramref:`post.read_timeout` of :meth:post` is not passed/equal to :attr:`DEFAULT_NONE`.
|
||||
|
||||
.. versionadded:: 20.7
|
||||
.. versionchanged:: 22.0
|
||||
This property is now required to be implemented by subclasses.
|
||||
|
||||
Warning:
|
||||
For now this property does not need to be implemented by subclasses and will raise
|
||||
:exc:`NotImplementedError` if accessed without being overridden. However, in future
|
||||
versions, this property will be abstract and must be implemented by subclasses.
|
||||
|
||||
Returns:
|
||||
:obj:`float` | :obj:`None`: The read timeout in seconds.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@abc.abstractmethod
|
||||
async def initialize(self) -> None:
|
||||
@@ -300,6 +305,31 @@ class BaseRequest(
|
||||
TelegramError
|
||||
|
||||
"""
|
||||
# Import needs to be here since HTTPXRequest is a subclass of BaseRequest
|
||||
from telegram.request import HTTPXRequest # pylint: disable=import-outside-toplevel
|
||||
|
||||
# 20 is the documented default value for all the media related bot methods and custom
|
||||
# implementations of BaseRequest may explicitly rely on that. Hence, we follow the
|
||||
# standard deprecation policy and deprecate starting with version 20.7.
|
||||
# For our own implementation HTTPXRequest, we can handle that ourselves, so we skip the
|
||||
# warning in that case.
|
||||
has_files = request_data and request_data.multipart_data
|
||||
if (
|
||||
has_files
|
||||
and not isinstance(self, HTTPXRequest)
|
||||
and isinstance(write_timeout, DefaultValue)
|
||||
):
|
||||
warn(
|
||||
PTBDeprecationWarning(
|
||||
"20.7",
|
||||
f"The `write_timeout` parameter passed to {self.__class__.__name__}.do_request"
|
||||
" will default to `BaseRequest.DEFAULT_NONE` instead of 20 in future versions "
|
||||
"for *all* methods of the `Bot` class, including methods sending media.",
|
||||
),
|
||||
stacklevel=3,
|
||||
)
|
||||
write_timeout = 20
|
||||
|
||||
try:
|
||||
code, payload = await self.do_request(
|
||||
url=url,
|
||||
|
||||
@@ -25,9 +25,11 @@ import httpx
|
||||
from telegram._utils.defaultvalue import DefaultValue
|
||||
from telegram._utils.logging import get_logger
|
||||
from telegram._utils.types import HTTPVersion, ODVInput, SocketOpt
|
||||
from telegram._utils.warnings import warn
|
||||
from telegram.error import NetworkError, TimedOut
|
||||
from telegram.request._baserequest import BaseRequest
|
||||
from telegram.request._requestdata import RequestData
|
||||
from telegram.warnings import PTBDeprecationWarning
|
||||
|
||||
# Note to future devs:
|
||||
# Proxies are currently only tested manually. The httpx development docs have a nice guide on that:
|
||||
@@ -43,9 +45,6 @@ class HTTPXRequest(BaseRequest):
|
||||
|
||||
.. versionadded:: 20.0
|
||||
|
||||
.. versionchanged:: 22.0
|
||||
Removed the deprecated parameter ``proxy_url``. Use :paramref:`proxy` instead.
|
||||
|
||||
Args:
|
||||
connection_pool_size (:obj:`int`, optional): Number of connections to keep in the
|
||||
connection pool. Defaults to ``1``.
|
||||
@@ -53,6 +52,10 @@ class HTTPXRequest(BaseRequest):
|
||||
Note:
|
||||
Independent of the value, one additional connection will be reserved for
|
||||
:meth:`telegram.Bot.get_updates`.
|
||||
proxy_url (:obj:`str`, optional): Legacy name for :paramref:`proxy`, kept for backward
|
||||
compatibility. Defaults to :obj:`None`.
|
||||
|
||||
.. deprecated:: 20.7
|
||||
read_timeout (:obj:`float` | :obj:`None`, optional): If passed, specifies the maximum
|
||||
amount of time (in seconds) to wait for a response from Telegram's server.
|
||||
This value is used unless a different value is passed to :meth:`do_request`.
|
||||
@@ -142,6 +145,7 @@ class HTTPXRequest(BaseRequest):
|
||||
def __init__(
|
||||
self,
|
||||
connection_pool_size: int = 1,
|
||||
proxy_url: Optional[Union[str, httpx.Proxy, httpx.URL]] = None,
|
||||
read_timeout: Optional[float] = 5.0,
|
||||
write_timeout: Optional[float] = 5.0,
|
||||
connect_timeout: Optional[float] = 5.0,
|
||||
@@ -152,6 +156,18 @@ class HTTPXRequest(BaseRequest):
|
||||
media_write_timeout: Optional[float] = 20.0,
|
||||
httpx_kwargs: Optional[dict[str, Any]] = None,
|
||||
):
|
||||
if proxy_url is not None and proxy is not None:
|
||||
raise ValueError("The parameters `proxy_url` and `proxy` are mutually exclusive.")
|
||||
|
||||
if proxy_url is not None:
|
||||
proxy = proxy_url
|
||||
warn(
|
||||
PTBDeprecationWarning(
|
||||
"20.7", "The parameter `proxy_url` is deprecated. Use `proxy` instead."
|
||||
),
|
||||
stacklevel=2,
|
||||
)
|
||||
|
||||
self._http_version = http_version
|
||||
self._media_write_timeout = media_write_timeout
|
||||
timeout = httpx.Timeout(
|
||||
|
||||
@@ -58,8 +58,6 @@ def input_media_video(class_thumb_file):
|
||||
parse_mode=InputMediaVideoTestBase.parse_mode,
|
||||
caption_entities=InputMediaVideoTestBase.caption_entities,
|
||||
thumbnail=class_thumb_file,
|
||||
cover=class_thumb_file,
|
||||
start_timestamp=InputMediaVideoTestBase.start_timestamp,
|
||||
supports_streaming=InputMediaVideoTestBase.supports_streaming,
|
||||
has_spoiler=InputMediaVideoTestBase.has_spoiler,
|
||||
show_caption_above_media=InputMediaVideoTestBase.show_caption_above_media,
|
||||
@@ -132,8 +130,6 @@ def input_paid_media_video(class_thumb_file):
|
||||
return InputPaidMediaVideo(
|
||||
media=InputMediaVideoTestBase.media,
|
||||
thumbnail=class_thumb_file,
|
||||
cover=class_thumb_file,
|
||||
start_timestamp=InputMediaVideoTestBase.start_timestamp,
|
||||
width=InputMediaVideoTestBase.width,
|
||||
height=InputMediaVideoTestBase.height,
|
||||
duration=InputMediaVideoTestBase.duration,
|
||||
@@ -148,7 +144,6 @@ class InputMediaVideoTestBase:
|
||||
width = 3
|
||||
height = 4
|
||||
duration = 5
|
||||
start_timestamp = 3
|
||||
parse_mode = "HTML"
|
||||
supports_streaming = True
|
||||
caption_entities = [MessageEntity(MessageEntity.BOLD, 0, 2)]
|
||||
@@ -174,8 +169,6 @@ class TestInputMediaVideoWithoutRequest(InputMediaVideoTestBase):
|
||||
assert input_media_video.caption_entities == tuple(self.caption_entities)
|
||||
assert input_media_video.supports_streaming == self.supports_streaming
|
||||
assert isinstance(input_media_video.thumbnail, InputFile)
|
||||
assert isinstance(input_media_video.cover, InputFile)
|
||||
assert input_media_video.start_timestamp == self.start_timestamp
|
||||
assert input_media_video.has_spoiler == self.has_spoiler
|
||||
assert input_media_video.show_caption_above_media == self.show_caption_above_media
|
||||
|
||||
@@ -201,8 +194,6 @@ class TestInputMediaVideoWithoutRequest(InputMediaVideoTestBase):
|
||||
input_media_video_dict["show_caption_above_media"]
|
||||
== input_media_video.show_caption_above_media
|
||||
)
|
||||
assert input_media_video_dict["cover"] == input_media_video.cover
|
||||
assert input_media_video_dict["start_timestamp"] == input_media_video.start_timestamp
|
||||
|
||||
def test_with_video(self, video):
|
||||
# fixture found in test_video
|
||||
@@ -223,13 +214,10 @@ class TestInputMediaVideoWithoutRequest(InputMediaVideoTestBase):
|
||||
|
||||
def test_with_local_files(self):
|
||||
input_media_video = InputMediaVideo(
|
||||
data_file("telegram.mp4"),
|
||||
thumbnail=data_file("telegram.jpg"),
|
||||
cover=data_file("telegram.jpg"),
|
||||
data_file("telegram.mp4"), thumbnail=data_file("telegram.jpg")
|
||||
)
|
||||
assert input_media_video.media == data_file("telegram.mp4").as_uri()
|
||||
assert input_media_video.thumbnail == data_file("telegram.jpg").as_uri()
|
||||
assert input_media_video.cover == data_file("telegram.jpg").as_uri()
|
||||
|
||||
def test_type_enum_conversion(self):
|
||||
# Since we have a lot of different test classes for all the input media types, we test this
|
||||
@@ -577,8 +565,6 @@ class TestInputPaidMediaVideoWithoutRequest(InputMediaVideoTestBase):
|
||||
assert input_paid_media_video.duration == self.duration
|
||||
assert input_paid_media_video.supports_streaming == self.supports_streaming
|
||||
assert isinstance(input_paid_media_video.thumbnail, InputFile)
|
||||
assert isinstance(input_paid_media_video.cover, InputFile)
|
||||
assert input_paid_media_video.start_timestamp == self.start_timestamp
|
||||
|
||||
def test_to_dict(self, input_paid_media_video):
|
||||
input_paid_media_video_dict = input_paid_media_video.to_dict()
|
||||
@@ -592,11 +578,6 @@ class TestInputPaidMediaVideoWithoutRequest(InputMediaVideoTestBase):
|
||||
== input_paid_media_video.supports_streaming
|
||||
)
|
||||
assert input_paid_media_video_dict["thumbnail"] == input_paid_media_video.thumbnail
|
||||
assert input_paid_media_video_dict["cover"] == input_paid_media_video.cover
|
||||
assert (
|
||||
input_paid_media_video_dict["start_timestamp"]
|
||||
== input_paid_media_video.start_timestamp
|
||||
)
|
||||
|
||||
def test_with_video(self, video):
|
||||
# fixture found in test_video
|
||||
@@ -615,13 +596,10 @@ class TestInputPaidMediaVideoWithoutRequest(InputMediaVideoTestBase):
|
||||
|
||||
def test_with_local_files(self):
|
||||
input_paid_media_video = InputPaidMediaVideo(
|
||||
data_file("telegram.mp4"),
|
||||
thumbnail=data_file("telegram.jpg"),
|
||||
cover=data_file("telegram.jpg"),
|
||||
data_file("telegram.mp4"), thumbnail=data_file("telegram.jpg")
|
||||
)
|
||||
assert input_paid_media_video.media == data_file("telegram.mp4").as_uri()
|
||||
assert input_paid_media_video.thumbnail == data_file("telegram.jpg").as_uri()
|
||||
assert input_paid_media_video.cover == data_file("telegram.jpg").as_uri()
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
|
||||
@@ -46,8 +46,6 @@ class VideoTestBase:
|
||||
mime_type = "video/mp4"
|
||||
supports_streaming = True
|
||||
file_name = "telegram.mp4"
|
||||
start_timestamp = 3
|
||||
cover = (PhotoSize("file_id", "unique_id", 640, 360, file_size=0),)
|
||||
thumb_width = 180
|
||||
thumb_height = 320
|
||||
thumb_file_size = 1767
|
||||
@@ -94,8 +92,6 @@ class TestVideoWithoutRequest(VideoTestBase):
|
||||
"mime_type": self.mime_type,
|
||||
"file_size": self.file_size,
|
||||
"file_name": self.file_name,
|
||||
"start_timestamp": self.start_timestamp,
|
||||
"cover": [photo_size.to_dict() for photo_size in self.cover],
|
||||
}
|
||||
json_video = Video.de_json(json_dict, offline_bot)
|
||||
assert json_video.api_kwargs == {}
|
||||
@@ -108,8 +104,6 @@ class TestVideoWithoutRequest(VideoTestBase):
|
||||
assert json_video.mime_type == self.mime_type
|
||||
assert json_video.file_size == self.file_size
|
||||
assert json_video.file_name == self.file_name
|
||||
assert json_video.start_timestamp == self.start_timestamp
|
||||
assert json_video.cover == self.cover
|
||||
|
||||
def test_to_dict(self, video):
|
||||
video_dict = video.to_dict()
|
||||
@@ -229,9 +223,7 @@ class TestVideoWithoutRequest(VideoTestBase):
|
||||
|
||||
class TestVideoWithRequest(VideoTestBase):
|
||||
@pytest.mark.parametrize("duration", [dtm.timedelta(seconds=5), 5])
|
||||
async def test_send_all_args(
|
||||
self, bot, chat_id, video_file, video, thumb_file, photo_file, duration
|
||||
):
|
||||
async def test_send_all_args(self, bot, chat_id, video_file, video, thumb_file, duration):
|
||||
message = await bot.send_video(
|
||||
chat_id,
|
||||
video_file,
|
||||
@@ -244,8 +236,6 @@ class TestVideoWithRequest(VideoTestBase):
|
||||
height=video.height,
|
||||
parse_mode="Markdown",
|
||||
thumbnail=thumb_file,
|
||||
cover=photo_file,
|
||||
start_timestamp=self.start_timestamp,
|
||||
has_spoiler=True,
|
||||
show_caption_above_media=True,
|
||||
)
|
||||
@@ -266,11 +256,6 @@ class TestVideoWithRequest(VideoTestBase):
|
||||
assert message.video.thumbnail.width == self.thumb_width
|
||||
assert message.video.thumbnail.height == self.thumb_height
|
||||
|
||||
assert message.video.start_timestamp == self.start_timestamp
|
||||
|
||||
assert isinstance(message.video.cover, tuple)
|
||||
assert isinstance(message.video.cover[0], PhotoSize)
|
||||
|
||||
assert message.video.file_name == self.file_name
|
||||
assert message.has_protected_content
|
||||
assert message.has_media_spoiler
|
||||
|
||||
@@ -28,6 +28,7 @@ from telegram import (
|
||||
InputTextMessageContent,
|
||||
)
|
||||
from telegram.constants import InlineQueryResultType
|
||||
from telegram.warnings import PTBDeprecationWarning
|
||||
from tests.auxil.slots import mro_slots
|
||||
|
||||
|
||||
@@ -39,6 +40,7 @@ def inline_query_result_article():
|
||||
input_message_content=InlineQueryResultArticleTestBase.input_message_content,
|
||||
reply_markup=InlineQueryResultArticleTestBase.reply_markup,
|
||||
url=InlineQueryResultArticleTestBase.url,
|
||||
hide_url=InlineQueryResultArticleTestBase.hide_url,
|
||||
description=InlineQueryResultArticleTestBase.description,
|
||||
thumbnail_url=InlineQueryResultArticleTestBase.thumbnail_url,
|
||||
thumbnail_height=InlineQueryResultArticleTestBase.thumbnail_height,
|
||||
@@ -53,6 +55,7 @@ class InlineQueryResultArticleTestBase:
|
||||
input_message_content = InputTextMessageContent("input_message_content")
|
||||
reply_markup = InlineKeyboardMarkup([[InlineKeyboardButton("reply_markup")]])
|
||||
url = "url"
|
||||
hide_url = True
|
||||
description = "description"
|
||||
thumbnail_url = "thumb url"
|
||||
thumbnail_height = 10
|
||||
@@ -76,6 +79,7 @@ class TestInlineQueryResultArticleWithoutRequest(InlineQueryResultArticleTestBas
|
||||
)
|
||||
assert inline_query_result_article.reply_markup.to_dict() == self.reply_markup.to_dict()
|
||||
assert inline_query_result_article.url == self.url
|
||||
assert inline_query_result_article.hide_url == self.hide_url
|
||||
assert inline_query_result_article.description == self.description
|
||||
assert inline_query_result_article.thumbnail_url == self.thumbnail_url
|
||||
assert inline_query_result_article.thumbnail_height == self.thumbnail_height
|
||||
@@ -97,6 +101,7 @@ class TestInlineQueryResultArticleWithoutRequest(InlineQueryResultArticleTestBas
|
||||
== inline_query_result_article.reply_markup.to_dict()
|
||||
)
|
||||
assert inline_query_result_article_dict["url"] == inline_query_result_article.url
|
||||
assert inline_query_result_article_dict["hide_url"] == inline_query_result_article.hide_url
|
||||
assert (
|
||||
inline_query_result_article_dict["description"]
|
||||
== inline_query_result_article.description
|
||||
@@ -153,3 +158,31 @@ class TestInlineQueryResultArticleWithoutRequest(InlineQueryResultArticleTestBas
|
||||
|
||||
assert a != e
|
||||
assert hash(a) != hash(e)
|
||||
|
||||
def test_deprecation_warning_for_hide_url(self):
|
||||
with pytest.warns(PTBDeprecationWarning, match="The argument `hide_url`") as record:
|
||||
InlineQueryResultArticle(
|
||||
self.id_, self.title, self.input_message_content, hide_url=True
|
||||
)
|
||||
|
||||
assert record[0].filename == __file__, "wrong stacklevel!"
|
||||
|
||||
with pytest.warns(PTBDeprecationWarning, match="The argument `hide_url`") as record:
|
||||
InlineQueryResultArticle(
|
||||
self.id_, self.title, self.input_message_content, hide_url=False
|
||||
)
|
||||
|
||||
assert record[0].filename == __file__, "wrong stacklevel!"
|
||||
|
||||
assert (
|
||||
InlineQueryResultArticle(
|
||||
self.id_, self.title, self.input_message_content, hide_url=True
|
||||
).hide_url
|
||||
is True
|
||||
)
|
||||
assert (
|
||||
InlineQueryResultArticle(
|
||||
self.id_, self.title, self.input_message_content, hide_url=False
|
||||
).hide_url
|
||||
is False
|
||||
)
|
||||
|
||||
@@ -282,10 +282,10 @@ class TestInputInvoiceMessageContentWithoutRequest(InputInvoiceMessageContentTes
|
||||
self.title,
|
||||
self.description,
|
||||
self.payload,
|
||||
self.provider_token,
|
||||
self.currency,
|
||||
# the first prices amount & the second lebal changed
|
||||
[LabeledPrice("label1", 24), LabeledPrice("label22", 314)],
|
||||
self.provider_token,
|
||||
)
|
||||
d = InputInvoiceMessageContent(
|
||||
self.title,
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
import pytest
|
||||
|
||||
from telegram import PassportElementErrorFiles, PassportElementErrorSelfie
|
||||
from telegram.warnings import PTBDeprecationWarning
|
||||
from tests.auxil.slots import mro_slots
|
||||
|
||||
|
||||
@@ -48,8 +49,8 @@ class TestPassportElementErrorFilesWithoutRequest(PassportElementErrorFilesTestB
|
||||
def test_expected_values(self, passport_element_error_files):
|
||||
assert passport_element_error_files.source == self.source
|
||||
assert passport_element_error_files.type == self.type_
|
||||
assert isinstance(passport_element_error_files.file_hashes, tuple)
|
||||
assert passport_element_error_files.file_hashes == tuple(self.file_hashes)
|
||||
assert isinstance(passport_element_error_files.file_hashes, list)
|
||||
assert passport_element_error_files.file_hashes == self.file_hashes
|
||||
assert passport_element_error_files.message == self.message
|
||||
|
||||
def test_to_dict(self, passport_element_error_files):
|
||||
@@ -59,8 +60,9 @@ class TestPassportElementErrorFilesWithoutRequest(PassportElementErrorFilesTestB
|
||||
assert passport_element_error_files_dict["source"] == passport_element_error_files.source
|
||||
assert passport_element_error_files_dict["type"] == passport_element_error_files.type
|
||||
assert passport_element_error_files_dict["message"] == passport_element_error_files.message
|
||||
assert passport_element_error_files_dict["file_hashes"] == list(
|
||||
passport_element_error_files.file_hashes
|
||||
assert (
|
||||
passport_element_error_files_dict["file_hashes"]
|
||||
== passport_element_error_files.file_hashes
|
||||
)
|
||||
|
||||
def test_equality(self):
|
||||
@@ -86,3 +88,13 @@ class TestPassportElementErrorFilesWithoutRequest(PassportElementErrorFilesTestB
|
||||
|
||||
assert a != f
|
||||
assert hash(a) != hash(f)
|
||||
|
||||
def test_file_hashes_deprecated(self, passport_element_error_files, recwarn):
|
||||
passport_element_error_files.file_hashes
|
||||
assert len(recwarn) == 1
|
||||
assert (
|
||||
"The attribute `file_hashes` will return a tuple instead of a list in future major"
|
||||
" versions." in str(recwarn[0].message)
|
||||
)
|
||||
assert recwarn[0].category is PTBDeprecationWarning
|
||||
assert recwarn[0].filename == __file__
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
import pytest
|
||||
|
||||
from telegram import PassportElementErrorSelfie, PassportElementErrorTranslationFiles
|
||||
from telegram.warnings import PTBDeprecationWarning
|
||||
from tests.auxil.slots import mro_slots
|
||||
|
||||
|
||||
@@ -50,8 +51,8 @@ class TestPassportElementErrorTranslationFilesWithoutRequest(
|
||||
def test_expected_values(self, passport_element_error_translation_files):
|
||||
assert passport_element_error_translation_files.source == self.source
|
||||
assert passport_element_error_translation_files.type == self.type_
|
||||
assert isinstance(passport_element_error_translation_files.file_hashes, tuple)
|
||||
assert passport_element_error_translation_files.file_hashes == tuple(self.file_hashes)
|
||||
assert isinstance(passport_element_error_translation_files.file_hashes, list)
|
||||
assert passport_element_error_translation_files.file_hashes == self.file_hashes
|
||||
assert passport_element_error_translation_files.message == self.message
|
||||
|
||||
def test_to_dict(self, passport_element_error_translation_files):
|
||||
@@ -72,8 +73,9 @@ class TestPassportElementErrorTranslationFilesWithoutRequest(
|
||||
passport_element_error_translation_files_dict["message"]
|
||||
== passport_element_error_translation_files.message
|
||||
)
|
||||
assert passport_element_error_translation_files_dict["file_hashes"] == list(
|
||||
passport_element_error_translation_files.file_hashes
|
||||
assert (
|
||||
passport_element_error_translation_files_dict["file_hashes"]
|
||||
== passport_element_error_translation_files.file_hashes
|
||||
)
|
||||
|
||||
def test_equality(self):
|
||||
@@ -99,3 +101,13 @@ class TestPassportElementErrorTranslationFilesWithoutRequest(
|
||||
|
||||
assert a != f
|
||||
assert hash(a) != hash(f)
|
||||
|
||||
def test_file_hashes_deprecated(self, passport_element_error_translation_files, recwarn):
|
||||
passport_element_error_translation_files.file_hashes
|
||||
assert len(recwarn) == 1
|
||||
assert (
|
||||
"The attribute `file_hashes` will return a tuple instead of a list in future major"
|
||||
" versions." in str(recwarn[0].message)
|
||||
)
|
||||
assert recwarn[0].category is PTBDeprecationWarning
|
||||
assert recwarn[0].filename == __file__
|
||||
|
||||
@@ -16,12 +16,10 @@
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser Public License
|
||||
# along with this program. If not, see [http://www.gnu.org/licenses/].
|
||||
import datetime as dtm
|
||||
|
||||
import pytest
|
||||
|
||||
from telegram import Bot, File, PassportElementError, PassportFile
|
||||
from telegram._utils.datetime import UTC, to_timestamp
|
||||
from telegram.warnings import PTBDeprecationWarning
|
||||
from tests.auxil.bot_method_checks import (
|
||||
check_defaults_handling,
|
||||
check_shortcut_call,
|
||||
@@ -46,7 +44,7 @@ class PassportFileTestBase:
|
||||
file_id = "data"
|
||||
file_unique_id = "adc3145fd2e84d95b64d68eaa22aa33e"
|
||||
file_size = 50
|
||||
file_date = dtm.datetime.now(tz=UTC).replace(microsecond=0)
|
||||
file_date = 1532879128
|
||||
|
||||
|
||||
class TestPassportFileWithoutRequest(PassportFileTestBase):
|
||||
@@ -69,27 +67,7 @@ class TestPassportFileWithoutRequest(PassportFileTestBase):
|
||||
assert passport_file_dict["file_id"] == passport_file.file_id
|
||||
assert passport_file_dict["file_unique_id"] == passport_file.file_unique_id
|
||||
assert passport_file_dict["file_size"] == passport_file.file_size
|
||||
assert passport_file_dict["file_date"] == to_timestamp(passport_file.file_date)
|
||||
|
||||
def test_de_json_localization(self, passport_file, tz_bot, offline_bot, raw_bot):
|
||||
json_dict = {
|
||||
"file_id": self.file_id,
|
||||
"file_unique_id": self.file_unique_id,
|
||||
"file_size": self.file_size,
|
||||
"file_date": to_timestamp(self.file_date),
|
||||
}
|
||||
|
||||
pf = PassportFile.de_json(json_dict, offline_bot)
|
||||
pf_raw = PassportFile.de_json(json_dict, raw_bot)
|
||||
pf_tz = PassportFile.de_json(json_dict, tz_bot)
|
||||
|
||||
# comparing utcoffsets because comparing timezones is unpredicatable
|
||||
date_offset = pf_tz.file_date.utcoffset()
|
||||
tz_bot_offset = tz_bot.defaults.tzinfo.utcoffset(pf_tz.file_date.replace(tzinfo=None))
|
||||
|
||||
assert pf_raw.file_date.tzinfo == UTC
|
||||
assert pf.file_date.tzinfo == UTC
|
||||
assert date_offset == tz_bot_offset
|
||||
assert passport_file_dict["file_date"] == passport_file.file_date
|
||||
|
||||
def test_equality(self):
|
||||
a = PassportFile(self.file_id, self.file_unique_id, self.file_size, self.file_date)
|
||||
@@ -111,6 +89,16 @@ class TestPassportFileWithoutRequest(PassportFileTestBase):
|
||||
assert a != e
|
||||
assert hash(a) != hash(e)
|
||||
|
||||
def test_file_date_deprecated(self, passport_file, recwarn):
|
||||
passport_file.file_date
|
||||
assert len(recwarn) == 1
|
||||
assert (
|
||||
"The attribute `file_date` will return a datetime instead of an integer in future"
|
||||
" major versions." in str(recwarn[0].message)
|
||||
)
|
||||
assert recwarn[0].category is PTBDeprecationWarning
|
||||
assert recwarn[0].filename == __file__
|
||||
|
||||
async def test_get_file_instance_method(self, monkeypatch, passport_file):
|
||||
async def make_assertion(*_, **kwargs):
|
||||
result = kwargs["file_id"] == passport_file.file_id
|
||||
|
||||
@@ -22,14 +22,12 @@ import pytest
|
||||
|
||||
from telegram import (
|
||||
AffiliateInfo,
|
||||
Chat,
|
||||
Gift,
|
||||
PaidMediaVideo,
|
||||
RevenueWithdrawalStatePending,
|
||||
Sticker,
|
||||
TransactionPartner,
|
||||
TransactionPartnerAffiliateProgram,
|
||||
TransactionPartnerChat,
|
||||
TransactionPartnerFragment,
|
||||
TransactionPartnerOther,
|
||||
TransactionPartnerTelegramAds,
|
||||
@@ -97,10 +95,6 @@ class TransactionPartnerTestBase:
|
||||
amount=42,
|
||||
)
|
||||
request_count = 42
|
||||
chat = Chat(
|
||||
id=3,
|
||||
type=Chat.CHANNEL,
|
||||
)
|
||||
|
||||
|
||||
class TestTransactionPartnerWithoutRequest(TransactionPartnerTestBase):
|
||||
@@ -129,7 +123,6 @@ class TestTransactionPartnerWithoutRequest(TransactionPartnerTestBase):
|
||||
("telegram_ads", TransactionPartnerTelegramAds),
|
||||
("telegram_api", TransactionPartnerTelegramApi),
|
||||
("other", TransactionPartnerOther),
|
||||
("chat", TransactionPartnerChat),
|
||||
],
|
||||
)
|
||||
def test_subclass(self, offline_bot, tp_type, subclass):
|
||||
@@ -457,58 +450,3 @@ class TestTransactionPartnerTelegramApiWithoutRequest(TransactionPartnerTestBase
|
||||
|
||||
assert a != d
|
||||
assert hash(a) != hash(d)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def transaction_partner_chat():
|
||||
return TransactionPartnerChat(
|
||||
chat=TransactionPartnerTestBase.chat,
|
||||
gift=TransactionPartnerTestBase.gift,
|
||||
)
|
||||
|
||||
|
||||
class TestTransactionPartnerChatWithoutRequest(TransactionPartnerTestBase):
|
||||
type = TransactionPartnerType.CHAT
|
||||
|
||||
def test_slot_behaviour(self, transaction_partner_chat):
|
||||
inst = transaction_partner_chat
|
||||
for attr in inst.__slots__:
|
||||
assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'"
|
||||
assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot"
|
||||
|
||||
def test_de_json(self, offline_bot):
|
||||
json_dict = {
|
||||
"chat": self.chat.to_dict(),
|
||||
"gift": self.gift.to_dict(),
|
||||
}
|
||||
tp = TransactionPartnerChat.de_json(json_dict, offline_bot)
|
||||
assert tp.api_kwargs == {}
|
||||
assert tp.type == "chat"
|
||||
assert tp.chat == self.chat
|
||||
assert tp.gift == self.gift
|
||||
|
||||
def test_to_dict(self, transaction_partner_chat):
|
||||
json_dict = transaction_partner_chat.to_dict()
|
||||
assert json_dict["type"] == self.type
|
||||
assert json_dict["chat"] == self.chat.to_dict()
|
||||
assert json_dict["gift"] == self.gift.to_dict()
|
||||
|
||||
def test_equality(self, transaction_partner_chat):
|
||||
a = transaction_partner_chat
|
||||
b = TransactionPartnerChat(
|
||||
chat=self.chat,
|
||||
gift=self.gift,
|
||||
)
|
||||
c = TransactionPartnerChat(
|
||||
chat=Chat(id=1, type=Chat.CHANNEL),
|
||||
)
|
||||
d = Chat(id=1, type=Chat.CHANNEL)
|
||||
|
||||
assert a == b
|
||||
assert hash(a) == hash(b)
|
||||
|
||||
assert a != c
|
||||
assert hash(a) != hash(c)
|
||||
|
||||
assert a != d
|
||||
assert hash(a) != hash(d)
|
||||
|
||||
@@ -269,9 +269,9 @@ class TestInvoiceWithRequest(InvoiceTestBase):
|
||||
self.title,
|
||||
self.description,
|
||||
self.payload,
|
||||
provider_token,
|
||||
self.currency,
|
||||
self.prices,
|
||||
provider_token,
|
||||
**kwargs,
|
||||
)
|
||||
for kwargs in ({}, {"protect_content": False})
|
||||
@@ -301,6 +301,7 @@ class TestInvoiceWithRequest(InvoiceTestBase):
|
||||
self.title,
|
||||
self.description,
|
||||
self.payload,
|
||||
"", # using tg stars
|
||||
"XTR",
|
||||
[self.prices[0]],
|
||||
allow_sending_without_reply=custom,
|
||||
@@ -314,9 +315,9 @@ class TestInvoiceWithRequest(InvoiceTestBase):
|
||||
self.title,
|
||||
self.description,
|
||||
self.payload,
|
||||
provider_token,
|
||||
self.currency,
|
||||
self.prices,
|
||||
provider_token,
|
||||
reply_to_message_id=reply_to_message.message_id,
|
||||
)
|
||||
assert message.reply_to_message is None
|
||||
@@ -327,9 +328,9 @@ class TestInvoiceWithRequest(InvoiceTestBase):
|
||||
self.title,
|
||||
self.description,
|
||||
self.payload,
|
||||
provider_token,
|
||||
self.currency,
|
||||
self.prices,
|
||||
provider_token,
|
||||
reply_to_message_id=reply_to_message.message_id,
|
||||
)
|
||||
|
||||
@@ -339,9 +340,9 @@ class TestInvoiceWithRequest(InvoiceTestBase):
|
||||
self.title,
|
||||
self.description,
|
||||
self.payload,
|
||||
provider_token,
|
||||
self.currency,
|
||||
self.prices,
|
||||
provider_token=provider_token,
|
||||
max_tip_amount=self.max_tip_amount,
|
||||
suggested_tip_amounts=self.suggested_tip_amounts,
|
||||
start_parameter=self.start_parameter,
|
||||
|
||||
@@ -351,9 +351,6 @@ def build_kwargs(
|
||||
allow_sending_without_reply=manually_passed_value,
|
||||
quote_parse_mode=manually_passed_value,
|
||||
)
|
||||
# TODO remove when gift_id isnt marked as optional anymore, tags: deprecated 21.11
|
||||
elif name == "gift_id":
|
||||
kws[name] = "GIFT-ID"
|
||||
|
||||
return kws
|
||||
|
||||
@@ -619,6 +616,8 @@ async def check_defaults_handling(
|
||||
defaults_no_custom_defaults = Defaults()
|
||||
kwargs = {kwarg: "custom_default" for kwarg in inspect.signature(Defaults).parameters}
|
||||
kwargs["tzinfo"] = zoneinfo.ZoneInfo("America/New_York")
|
||||
kwargs.pop("disable_web_page_preview") # mutually exclusive with link_preview_options
|
||||
kwargs.pop("quote") # mutually exclusive with do_quote
|
||||
kwargs["link_preview_options"] = LinkPreviewOptions(
|
||||
url="custom_default", show_above_text="custom_default"
|
||||
)
|
||||
|
||||
@@ -71,10 +71,6 @@ class OfflineRequest(BaseRequest):
|
||||
async def shutdown(self) -> None:
|
||||
pass
|
||||
|
||||
@property
|
||||
def read_timeout(self):
|
||||
return 1
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ from typing import Optional
|
||||
import pytest
|
||||
|
||||
from telegram import Bot, Chat, Message, MessageEntity, User
|
||||
from telegram.error import InvalidToken, TelegramError
|
||||
from telegram.error import TelegramError
|
||||
from telegram.ext import (
|
||||
Application,
|
||||
ApplicationBuilder,
|
||||
@@ -1516,6 +1516,49 @@ class TestApplication:
|
||||
found_log = True
|
||||
assert found_log
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"timeout_name",
|
||||
["read_timeout", "connect_timeout", "write_timeout", "pool_timeout", "poll_interval"],
|
||||
)
|
||||
@pytest.mark.skipif(
|
||||
platform.system() == "Windows",
|
||||
reason="Can't send signals without stopping whole process on windows",
|
||||
)
|
||||
def test_run_polling_timeout_deprecation_warnings(
|
||||
self, timeout_name, monkeypatch, recwarn, app
|
||||
):
|
||||
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")
|
||||
|
||||
time.sleep(0.05)
|
||||
|
||||
os.kill(os.getpid(), signal.SIGINT)
|
||||
|
||||
monkeypatch.setattr(app.bot, "get_updates", empty_get_updates)
|
||||
|
||||
thread = Thread(target=thread_target)
|
||||
thread.start()
|
||||
|
||||
kwargs = {timeout_name: 42}
|
||||
app.run_polling(drop_pending_updates=True, close_loop=False, **kwargs)
|
||||
thread.join()
|
||||
|
||||
if timeout_name == "poll_interval":
|
||||
assert len(recwarn) == 0
|
||||
return
|
||||
|
||||
assert len(recwarn) == 1
|
||||
assert "Setting timeouts via `Application.run_polling` is deprecated." in str(
|
||||
recwarn[0].message
|
||||
)
|
||||
assert recwarn[0].category is PTBDeprecationWarning
|
||||
assert recwarn[0].filename == __file__, "wrong stacklevel"
|
||||
|
||||
@pytest.mark.skipif(
|
||||
platform.system() == "Windows",
|
||||
reason="Can't send signals without stopping whole process on windows",
|
||||
@@ -2316,47 +2359,6 @@ class TestApplication:
|
||||
for record in recwarn:
|
||||
assert not str(record.message).startswith("Could not add signal handlers for the stop")
|
||||
|
||||
@pytest.mark.parametrize("exception_class", [InvalidToken, TelegramError])
|
||||
@pytest.mark.parametrize("retries", [3, 0])
|
||||
@pytest.mark.parametrize("method_name", ["run_polling", "run_webhook"])
|
||||
async def test_run_polling_webhook_bootstrap_retries(
|
||||
self, monkeypatch, exception_class, retries, offline_bot, method_name
|
||||
):
|
||||
"""This doesn't test all of the internals of the network retry loop. We do that quite
|
||||
intensively for the `Updater` and here we just want to make sure that the `Application`
|
||||
does do the retries.
|
||||
"""
|
||||
|
||||
def thread_target():
|
||||
asyncio.set_event_loop(asyncio.new_event_loop())
|
||||
app = (
|
||||
ApplicationBuilder().bot(offline_bot).application_class(PytestApplication).build()
|
||||
)
|
||||
|
||||
async def initialize(*args, **kwargs):
|
||||
self.count += 1
|
||||
raise exception_class(str(self.count))
|
||||
|
||||
monkeypatch.setattr(app, "initialize", initialize)
|
||||
method = functools.partial(
|
||||
getattr(app, method_name),
|
||||
bootstrap_retries=retries,
|
||||
close_loop=False,
|
||||
stop_signals=None,
|
||||
)
|
||||
|
||||
if exception_class == InvalidToken:
|
||||
with pytest.raises(InvalidToken, match="1"):
|
||||
method()
|
||||
else:
|
||||
with pytest.raises(TelegramError, match=str(retries + 1)):
|
||||
method()
|
||||
|
||||
thread = Thread(target=thread_target)
|
||||
thread.start()
|
||||
thread.join(timeout=10)
|
||||
assert not thread.is_alive(), "Test took to long to run. Aborting"
|
||||
|
||||
@pytest.mark.flaky(3, 1) # loop.call_later will error the test when a flood error is received
|
||||
def test_signal_handlers(self, app, monkeypatch):
|
||||
# this test should make sure that signal handlers are set by default on Linux + Mac,
|
||||
|
||||
@@ -41,6 +41,7 @@ from telegram.ext import (
|
||||
from telegram.ext._applicationbuilder import _BOT_CHECKS
|
||||
from telegram.ext._baseupdateprocessor import SimpleUpdateProcessor
|
||||
from telegram.request import HTTPXRequest
|
||||
from telegram.warnings import PTBDeprecationWarning
|
||||
from tests.auxil.constants import PRIVATE_KEY
|
||||
from tests.auxil.envvars import TEST_WITH_OPT_DEPS
|
||||
from tests.auxil.files import data_file
|
||||
@@ -206,6 +207,7 @@ class TestApplicationBuilder:
|
||||
"write_timeout",
|
||||
"media_write_timeout",
|
||||
"proxy",
|
||||
"proxy_url",
|
||||
"socket_options",
|
||||
"bot",
|
||||
"updater",
|
||||
@@ -215,8 +217,9 @@ class TestApplicationBuilder:
|
||||
def test_mutually_exclusive_for_request(self, builder, method):
|
||||
builder.request(1)
|
||||
|
||||
method_name = method.replace("proxy_url", "proxy")
|
||||
with pytest.raises(
|
||||
RuntimeError, match=f"`{method}` may only be set, if no request instance"
|
||||
RuntimeError, match=f"`{method_name}` may only be set, if no request instance"
|
||||
):
|
||||
getattr(builder, method)(data_file("private.key"))
|
||||
|
||||
@@ -234,6 +237,7 @@ class TestApplicationBuilder:
|
||||
"get_updates_read_timeout",
|
||||
"get_updates_write_timeout",
|
||||
"get_updates_proxy",
|
||||
"get_updates_proxy_url",
|
||||
"get_updates_socket_options",
|
||||
"get_updates_http_version",
|
||||
"bot",
|
||||
@@ -243,9 +247,10 @@ class TestApplicationBuilder:
|
||||
def test_mutually_exclusive_for_get_updates_request(self, builder, method):
|
||||
builder.get_updates_request(1)
|
||||
|
||||
method_name = method.replace("proxy_url", "proxy")
|
||||
with pytest.raises(
|
||||
RuntimeError,
|
||||
match=f"`{method}` may only be set, if no get_updates_request instance",
|
||||
match=f"`{method_name}` may only be set, if no get_updates_request instance",
|
||||
):
|
||||
getattr(builder, method)(data_file("private.key"))
|
||||
|
||||
@@ -262,6 +267,7 @@ class TestApplicationBuilder:
|
||||
"get_updates_pool_timeout",
|
||||
"get_updates_read_timeout",
|
||||
"get_updates_write_timeout",
|
||||
"get_updates_proxy_url",
|
||||
"get_updates_proxy",
|
||||
"get_updates_socket_options",
|
||||
"get_updates_http_version",
|
||||
@@ -272,6 +278,7 @@ class TestApplicationBuilder:
|
||||
"write_timeout",
|
||||
"media_write_timeout",
|
||||
"proxy",
|
||||
"proxy_url",
|
||||
"socket_options",
|
||||
"http_version",
|
||||
"bot",
|
||||
@@ -283,15 +290,17 @@ class TestApplicationBuilder:
|
||||
def test_mutually_exclusive_for_updater(self, builder, method):
|
||||
builder.updater(1)
|
||||
|
||||
method_name = method.replace("proxy_url", "proxy")
|
||||
with pytest.raises(
|
||||
RuntimeError,
|
||||
match=f"`{method}` may only be set, if no updater",
|
||||
match=f"`{method_name}` may only be set, if no updater",
|
||||
):
|
||||
getattr(builder, method)(data_file("private.key"))
|
||||
|
||||
builder = ApplicationBuilder()
|
||||
getattr(builder, method)(data_file("private.key"))
|
||||
|
||||
method = method.replace("proxy_url", "proxy")
|
||||
with pytest.raises(RuntimeError, match=f"`updater` may only be set, if no {method}"):
|
||||
builder.updater(1)
|
||||
|
||||
@@ -304,6 +313,7 @@ class TestApplicationBuilder:
|
||||
"get_updates_read_timeout",
|
||||
"get_updates_write_timeout",
|
||||
"get_updates_proxy",
|
||||
"get_updates_proxy_url",
|
||||
"get_updates_socket_options",
|
||||
"get_updates_http_version",
|
||||
"connection_pool_size",
|
||||
@@ -313,6 +323,7 @@ class TestApplicationBuilder:
|
||||
"write_timeout",
|
||||
"media_write_timeout",
|
||||
"proxy",
|
||||
"proxy_url",
|
||||
"socket_options",
|
||||
"bot",
|
||||
"http_version",
|
||||
@@ -330,11 +341,14 @@ class TestApplicationBuilder:
|
||||
getattr(builder, method)(data_file("private.key"))
|
||||
builder.updater(None)
|
||||
|
||||
# We test with bot the new & legacy version to ensure that the legacy version still works
|
||||
@pytest.mark.parametrize(
|
||||
("proxy_method", "get_updates_proxy_method"),
|
||||
[("proxy", "get_updates_proxy"), ("proxy_url", "get_updates_proxy_url")],
|
||||
ids=["new", "legacy"],
|
||||
)
|
||||
def test_all_bot_args_custom(
|
||||
self,
|
||||
builder,
|
||||
bot,
|
||||
monkeypatch,
|
||||
self, builder, bot, monkeypatch, proxy_method, get_updates_proxy_method
|
||||
):
|
||||
# Only socket_options is tested in a standalone test, since that's easier
|
||||
defaults = Defaults()
|
||||
@@ -389,7 +403,8 @@ class TestApplicationBuilder:
|
||||
builder = ApplicationBuilder().token(bot.token)
|
||||
builder.connection_pool_size(1).connect_timeout(2).pool_timeout(3).read_timeout(
|
||||
4
|
||||
).write_timeout(5).media_write_timeout(6).http_version("1.1").proxy("proxy")
|
||||
).write_timeout(5).media_write_timeout(6).http_version("1.1")
|
||||
getattr(builder, proxy_method)("proxy")
|
||||
app = builder.build()
|
||||
client = app.bot.request._client
|
||||
|
||||
@@ -408,9 +423,8 @@ class TestApplicationBuilder:
|
||||
5
|
||||
).get_updates_http_version(
|
||||
"1.1"
|
||||
).get_updates_proxy(
|
||||
"get_updates_proxy"
|
||||
)
|
||||
getattr(builder, get_updates_proxy_method)("get_updates_proxy")
|
||||
app = builder.build()
|
||||
client = app.bot._request[0]._client
|
||||
|
||||
@@ -571,6 +585,22 @@ class TestApplicationBuilder:
|
||||
assert isinstance(app.update_queue, asyncio.Queue)
|
||||
assert isinstance(app.updater, Updater)
|
||||
|
||||
def test_proxy_url_deprecation_warning(self, bot, builder, recwarn):
|
||||
builder.token(bot.token).proxy_url("proxy_url")
|
||||
assert len(recwarn) == 1
|
||||
assert "`ApplicationBuilder.proxy_url` is deprecated" in str(recwarn[0].message)
|
||||
assert recwarn[0].category is PTBDeprecationWarning
|
||||
assert recwarn[0].filename == __file__, "wrong stacklevel"
|
||||
|
||||
def test_get_updates_proxy_url_deprecation_warning(self, bot, builder, recwarn):
|
||||
builder.token(bot.token).get_updates_proxy_url("get_updates_proxy_url")
|
||||
assert len(recwarn) == 1
|
||||
assert "`ApplicationBuilder.get_updates_proxy_url` is deprecated" in str(
|
||||
recwarn[0].message
|
||||
)
|
||||
assert recwarn[0].category is PTBDeprecationWarning
|
||||
assert recwarn[0].filename == __file__, "wrong stacklevel"
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("read_timeout", "timeout", "expected"),
|
||||
[
|
||||
|
||||
@@ -22,7 +22,7 @@ import inspect
|
||||
|
||||
import pytest
|
||||
|
||||
from telegram import User
|
||||
from telegram import LinkPreviewOptions, User
|
||||
from telegram.ext import Defaults
|
||||
from telegram.warnings import PTBDeprecationWarning
|
||||
from tests.auxil.envvars import TEST_WITH_OPT_DEPS
|
||||
@@ -31,7 +31,7 @@ from tests.auxil.slots import mro_slots
|
||||
|
||||
class TestDefaults:
|
||||
def test_slot_behaviour(self):
|
||||
a = Defaults(parse_mode="HTML", do_quote=True)
|
||||
a = Defaults(parse_mode="HTML", quote=True)
|
||||
for attr in a.__slots__:
|
||||
assert getattr(a, attr, "err") != "err", f"got extra slot '{attr}'"
|
||||
assert len(mro_slots(a)) == len(set(mro_slots(a))), "duplicate slot"
|
||||
@@ -63,8 +63,8 @@ class TestDefaults:
|
||||
c = Defaults(parse_mode="HTML", do_quote=True, protect_content=True)
|
||||
d = Defaults(parse_mode="HTML", protect_content=True)
|
||||
e = User(123, "test_user", False)
|
||||
f = Defaults(parse_mode="HTML", block=True)
|
||||
g = Defaults(parse_mode="HTML", block=True)
|
||||
f = Defaults(parse_mode="HTML", disable_web_page_preview=True)
|
||||
g = Defaults(parse_mode="HTML", disable_web_page_preview=True)
|
||||
|
||||
assert a == b
|
||||
assert hash(a) == hash(b)
|
||||
@@ -81,3 +81,29 @@ class TestDefaults:
|
||||
|
||||
assert f == g
|
||||
assert hash(f) == hash(g)
|
||||
|
||||
def test_mutually_exclusive(self):
|
||||
with pytest.raises(ValueError, match="mutually exclusive"):
|
||||
Defaults(disable_web_page_preview=True, link_preview_options=LinkPreviewOptions(False))
|
||||
with pytest.raises(ValueError, match="mutually exclusive"):
|
||||
Defaults(quote=True, do_quote=False)
|
||||
|
||||
def test_deprecation_warning_for_disable_web_page_preview(self):
|
||||
with pytest.warns(
|
||||
PTBDeprecationWarning, match="`Defaults.disable_web_page_preview` is "
|
||||
) as record:
|
||||
Defaults(disable_web_page_preview=True)
|
||||
|
||||
assert record[0].filename == __file__, "wrong stacklevel!"
|
||||
|
||||
assert Defaults(disable_web_page_preview=True).link_preview_options.is_disabled is True
|
||||
assert Defaults(disable_web_page_preview=False).disable_web_page_preview is False
|
||||
|
||||
def test_deprecation_warning_for_quote(self):
|
||||
with pytest.warns(PTBDeprecationWarning, match="`Defaults.quote` is ") as record:
|
||||
Defaults(quote=True)
|
||||
|
||||
assert record[0].filename == __file__, "wrong stacklevel!"
|
||||
|
||||
assert Defaults(quote=True).do_quote is True
|
||||
assert Defaults(quote=False).quote is False
|
||||
|
||||
@@ -1068,6 +1068,11 @@ class TestFilters:
|
||||
assert filters.StatusUpdate.WRITE_ACCESS_ALLOWED.check_update(update)
|
||||
update.message.write_access_allowed = None
|
||||
|
||||
update.message.api_kwargs = {"user_shared": "user_shared"}
|
||||
assert filters.StatusUpdate.ALL.check_update(update)
|
||||
assert filters.StatusUpdate.USER_SHARED.check_update(update)
|
||||
update.message.api_kwargs = {}
|
||||
|
||||
update.message.users_shared = "users_shared"
|
||||
assert filters.StatusUpdate.ALL.check_update(update)
|
||||
assert filters.StatusUpdate.USERS_SHARED.check_update(update)
|
||||
@@ -1329,12 +1334,15 @@ class TestFilters:
|
||||
|
||||
def test_filters_chat_id(self, update):
|
||||
assert not filters.Chat(chat_id=1).check_update(update)
|
||||
assert filters.CHAT.check_update(update)
|
||||
update.message.chat.id = 1
|
||||
assert filters.Chat(chat_id=1).check_update(update)
|
||||
assert filters.CHAT.check_update(update)
|
||||
update.message.chat.id = 2
|
||||
assert filters.Chat(chat_id=[1, 2]).check_update(update)
|
||||
assert not filters.Chat(chat_id=[3, 4]).check_update(update)
|
||||
update.message.chat = None
|
||||
assert not filters.CHAT.check_update(update)
|
||||
assert not filters.Chat(chat_id=[3, 4]).check_update(update)
|
||||
|
||||
def test_filters_chat_username(self, update):
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user