diff --git a/.github/CONTRIBUTING.rst b/.github/CONTRIBUTING.rst
index 63906fabd6f..b64a368ac43 100644
--- a/.github/CONTRIBUTING.rst
+++ b/.github/CONTRIBUTING.rst
@@ -210,7 +210,7 @@ doc strings don't have a separate documentation site they generate, instead, the
User facing documentation
-------------------------
-We use `sphinx`_ to generate static HTML docs. To build them, first make sure you're running Python 3.9 or above and have the required dependencies installed as explained above.
+We use `sphinx`_ to generate static HTML docs. To build them, first make sure you're running Python 3.10 or above and have the required dependencies installed as explained above.
Then, run the following from the PTB root directory:
.. code-block:: bash
diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml
index 509f689df40..53c89496b21 100644
--- a/.github/ISSUE_TEMPLATE/bug-report.yml
+++ b/.github/ISSUE_TEMPLATE/bug-report.yml
@@ -1,7 +1,7 @@
name: Bug Report
description: Create a report to help us improve
-title: "[BUG]"
-labels: ["bug :bug:"]
+labels: ["📋 triage"]
+type: '🐛 bug'
body:
- type: markdown
diff --git a/.github/ISSUE_TEMPLATE/feature-request.yml b/.github/ISSUE_TEMPLATE/feature-request.yml
index 6c7ff80390e..e6cc817a1bd 100644
--- a/.github/ISSUE_TEMPLATE/feature-request.yml
+++ b/.github/ISSUE_TEMPLATE/feature-request.yml
@@ -1,7 +1,7 @@
name: Feature Request
description: Suggest an idea for this project
-title: "[FEATURE]"
-labels: ["enhancement"]
+labels: ["📋 triage"]
+type: '💡 feature'
body:
- type: textarea
diff --git a/.github/ISSUE_TEMPLATE/question.yml b/.github/ISSUE_TEMPLATE/question.yml
index 1836230ff21..220da04007e 100644
--- a/.github/ISSUE_TEMPLATE/question.yml
+++ b/.github/ISSUE_TEMPLATE/question.yml
@@ -1,7 +1,7 @@
name: Question
description: Get help with errors or general questions
-title: "[QUESTION]"
labels: ["question"]
+type: '❔ question'
body:
- type: markdown
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index 9d79fbdf366..fb5b1d14166 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -5,6 +5,9 @@ updates:
schedule:
interval: "weekly"
day: "friday"
+ labels:
+ - "⚙️ dependencies"
+ - "🔗 python"
# Updates the dependencies of the GitHub Actions workflows
- package-ecosystem: "github-actions"
@@ -12,3 +15,6 @@ updates:
schedule:
interval: "monthly"
day: "friday"
+ labels:
+ - "⚙️ dependencies"
+ - "🔗 github-actions"
diff --git a/.github/labeler.yml b/.github/labeler.yml
index 120c88f4b36..3d2eb437df9 100644
--- a/.github/labeler.yml
+++ b/.github/labeler.yml
@@ -3,7 +3,7 @@
version: 1
labels:
-- label: "dependencies"
+- label: "⚙️ dependencies"
authors: ["dependabot[bot]", "pre-commit-ci[bot]"]
-- label: "code quality ✨"
+- label: "🛠 code-quality"
authors: ["pre-commit-ci[bot]"]
diff --git a/.github/workflows/assets/release_template.html b/.github/workflows/assets/release_template.html
new file mode 100644
index 00000000000..2e672ca8482
--- /dev/null
+++ b/.github/workflows/assets/release_template.html
@@ -0,0 +1,5 @@
+We've just released {tag}.
+Thank you to everyone who contributed to this release.
+As usual, upgrade using pip install -U python-telegram-bot
.
+
+The release notes can be found here.
\ No newline at end of file
diff --git a/.github/workflows/chango.yml b/.github/workflows/chango.yml
new file mode 100644
index 00000000000..eb1db8ef440
--- /dev/null
+++ b/.github/workflows/chango.yml
@@ -0,0 +1,66 @@
+name: Chango
+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
+ outputs:
+ IS_RELEASE_PR: ${{ steps.check_title.outputs.IS_RELEASE_PR }}
+
+ steps:
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ with:
+ # needed for commit and push step at the end
+ persist-credentials: true
+ - name: Check PR Title
+ id: check_title
+ run: | # zizmor: ignore[template-injection]
+ if [[ "$(echo "${{ github.event.pull_request.title }}" | tr '[:upper:]' '[:lower:]')" =~ ^bump\ version\ to\ .* ]]; then
+ echo "COMMIT_AND_PUSH=false" >> $GITHUB_OUTPUT
+ echo "IS_RELEASE_PR=true" >> $GITHUB_OUTPUT
+ else
+ echo "COMMIT_AND_PUSH=true" >> $GITHUB_OUTPUT
+ echo "IS_RELEASE_PR=false" >> $GITHUB_OUTPUT
+ fi
+
+ # Create the new fragment
+ - uses: Bibo-Joshi/chango@9d6bd9d7612eca5fab2c5161687011be59baaf19 # v0.4.0
+ with:
+ github-token: ${{ secrets.CHANGO_PAT }}
+ query-issue-types: true
+ commit-and-push: ${{ steps.check_title.outputs.COMMIT_AND_PUSH }}
+
+ # Run `chango release` if applicable - needs some additional setup.
+ - name: Set up Python
+ if: steps.check_title.outputs.IS_RELEASE_PR == 'true'
+ uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5.5.0
+ with:
+ python-version: "3.x"
+
+ - name: Do Release
+ if: steps.check_title.outputs.IS_RELEASE_PR == 'true'
+ run: |
+ cd ./target-repo
+ git add changes/unreleased/*
+ pip install . -r docs/requirements-docs.txt
+ VERSION_TAG=$(python -c "from telegram import __version__; print(f'{__version__}')")
+ chango release --uid $VERSION_TAG
+
+ - name: Commit & Push
+ if: steps.check_title.outputs.IS_RELEASE_PR == 'true'
+ uses: stefanzweifel/git-auto-commit-action@e348103e9026cc0eee72ae06630dbe30c8bf7a79 # v5.1.0
+ with:
+ commit_message: "Do chango Release"
+ repository: ./target-repo
diff --git a/.github/workflows/dependabot-prs.yml b/.github/workflows/dependabot-prs.yml
index 80bace2d95d..9bb7a5299c3 100644
--- a/.github/workflows/dependabot-prs.yml
+++ b/.github/workflows/dependabot-prs.yml
@@ -4,6 +4,8 @@ on:
pull_request:
types: [opened, reopened]
+permissions: {}
+
jobs:
process-dependabot-prs:
permissions:
@@ -16,14 +18,15 @@ jobs:
- name: Fetch Dependabot metadata
id: dependabot-metadata
- uses: dependabot/fetch-metadata@v2.1.0
+ uses: dependabot/fetch-metadata@d7267f607e9d3fb96fc2fbe83e0af444713e90b7 # v2.3.0
- - uses: actions/checkout@v4
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ github.event.pull_request.head.ref }}
+ persist-credentials: false
- name: Update Version Number in Other Files
- uses: jacobtomlinson/gha-find-replace@v3
+ uses: jacobtomlinson/gha-find-replace@f1069b438f125e5395d84d1c6fd3b559a7880cb5 # v3
with:
find: ${{ steps.dependabot-metadata.outputs.previous-version }}
replace: ${{ steps.dependabot-metadata.outputs.new-version }}
@@ -31,7 +34,7 @@ jobs:
exclude: CHANGES.rst
- name: Commit & Push Changes to PR
- uses: EndBug/add-and-commit@v9.1.4
+ uses: EndBug/add-and-commit@a94899bca583c204427a224a7af87c02f9b325d5 # v9.1.4
with:
message: 'Update version number in other files'
committer_name: GitHub Actions
diff --git a/.github/workflows/docs.yml b/.github/workflows/docs-admonitions.yml
similarity index 52%
rename from .github/workflows/docs.yml
rename to .github/workflows/docs-admonitions.yml
index 73e123e17ea..00b03ae4cca 100644
--- a/.github/workflows/docs.yml
+++ b/.github/workflows/docs-admonitions.yml
@@ -1,26 +1,34 @@
-name: Test Documentation Build
+name: Test Admonitions Generation
on:
pull_request:
paths:
- telegram/**
- docs/**
+ - .github/workflows/docs-admonitions.yml
push:
branches:
- master
+permissions: {}
+
jobs:
- test-sphinx-build:
- name: test-sphinx-build
+ test-admonitions:
+ name: Test Admonitions Generation
runs-on: ${{matrix.os}}
+ permissions:
+ # for uploading artifacts
+ actions: write
strategy:
matrix:
- python-version: [3.9]
+ python-version: ['3.12']
os: [ubuntu-latest]
fail-fast: False
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ with:
+ persist-credentials: false
- name: Set up Python ${{ matrix.python-version }}
- uses: actions/setup-python@v5
+ uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5.5.0
with:
python-version: ${{ matrix.python-version }}
cache: 'pip'
@@ -30,17 +38,4 @@ jobs:
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
- - name: Build docs
- run: sphinx-build docs/source docs/build/html -W --keep-going -j auto
- - name: Upload docs
- uses: actions/upload-artifact@v4
- with:
- name: HTML Docs
- retention-days: 7
- path: |
- # Exclude the .doctrees folder and .buildinfo file from the artifact
- # since they are not needed and add to the size
- docs/build/html/*
- !docs/build/html/.doctrees
- !docs/build/html/.buildinfo
+ run: pytest -v --tb=short tests/docs/admonition_inserter.py
\ No newline at end of file
diff --git a/.github/workflows/docs-linkcheck.yml b/.github/workflows/docs-linkcheck.yml
index 82fafe3e53f..b25405b75cc 100644
--- a/.github/workflows/docs-linkcheck.yml
+++ b/.github/workflows/docs-linkcheck.yml
@@ -3,6 +3,11 @@ 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:
@@ -10,13 +15,15 @@ jobs:
runs-on: ${{matrix.os}}
strategy:
matrix:
- python-version: [3.9]
+ python-version: ['3.12']
os: [ubuntu-latest]
fail-fast: False
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ with:
+ persist-credentials: false
- name: Set up Python ${{ matrix.python-version }}
- uses: actions/setup-python@v5
+ uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5.5.0
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
@@ -24,4 +31,11 @@ jobs:
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
+ run: sphinx-build docs/source docs/build/html --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.*
diff --git a/.github/workflows/gha_security.yml b/.github/workflows/gha_security.yml
new file mode 100644
index 00000000000..c69f88c9c57
--- /dev/null
+++ b/.github/workflows/gha_security.yml
@@ -0,0 +1,33 @@
+name: GitHub Actions Security Analysis
+
+on:
+ push:
+ branches:
+ - master
+ pull_request:
+
+permissions: {}
+
+jobs:
+ zizmor:
+ name: Security Analysis with zizmor
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ security-events: write
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ with:
+ persist-credentials: false
+ - name: Install the latest version of uv
+ uses: astral-sh/setup-uv@0c5e2b8115b80b4c7c5ddf6ffdd634974642d182 # v5.4.1
+ - 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@1b549b9259bda1cb5ddde3b41741a82a2d15a841 # v3.28.13
+ with:
+ sarif_file: results.sarif
+ category: zizmor
\ No newline at end of file
diff --git a/.github/workflows/labelling.yml b/.github/workflows/labelling.yml
index 12cd1b4bea7..21a4d6733ba 100644
--- a/.github/workflows/labelling.yml
+++ b/.github/workflows/labelling.yml
@@ -4,6 +4,8 @@ on:
pull_request:
types: [opened]
+permissions: {}
+
jobs:
pre-commit-ci:
permissions:
@@ -11,7 +13,7 @@ jobs:
pull-requests: write # for srvaroa/labeler to add labels in PR
runs-on: ubuntu-latest
steps:
- - uses: srvaroa/labeler@v1.10.1
+ - uses: srvaroa/labeler@0a20eccb8c94a1ee0bed5f16859aece1c45c3e55 # v1.13.0
# Config file at .github/labeler.yml
env:
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
diff --git a/.github/workflows/lock.yml b/.github/workflows/lock.yml
index f2ab9d029b0..e32ece0ff4e 100644
--- a/.github/workflows/lock.yml
+++ b/.github/workflows/lock.yml
@@ -4,11 +4,17 @@ on:
schedule:
- cron: '8 4 * * *'
+permissions: {}
+
jobs:
lock:
runs-on: ubuntu-latest
+ permissions:
+ # For locking the threads
+ issues: write
+ pull-requests: write
steps:
- - uses: dessant/lock-threads@v5.0.1
+ - uses: dessant/lock-threads@1bf7ec25051fe7c00bdd17e6a7cf3d7bfb7dc771 # v5.0.1
with:
github-token: ${{ github.token }}
issue-inactive-days: '7'
diff --git a/.github/workflows/release_pypi.yml b/.github/workflows/release_pypi.yml
index bcd1794c468..dcb22f3c6e4 100644
--- a/.github/workflows/release_pypi.yml
+++ b/.github/workflows/release_pypi.yml
@@ -1,22 +1,27 @@
name: Publish to PyPI
on:
- # Run on any tag
- push:
- tags:
- - '**'
- # manually trigger the workflow - for testing only
+ # manually trigger the workflow
workflow_dispatch:
+permissions: {}
+
jobs:
build:
name: Build Distribution
runs-on: ubuntu-latest
+ outputs:
+ TAG: ${{ steps.get_tag.outputs.TAG }}
+ permissions:
+ # for uploading artifacts
+ actions: write
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ with:
+ persist-credentials: false
- name: Set up Python
- uses: actions/setup-python@v5
+ uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5.5.0
with:
python-version: "3.x"
- name: Install pypa/build
@@ -25,15 +30,19 @@ jobs:
- name: Build a binary wheel and a source tarball
run: python3 -m build
- name: Store the distribution packages
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: python-package-distributions
path: dist/
+ - name: Get Tag Name
+ id: get_tag
+ run: |
+ pip install .
+ TAG=$(python -c "from telegram import __version__; print(f'v{__version__}')")
+ echo "TAG=$TAG" >> $GITHUB_OUTPUT
publish-to-pypi:
name: Publish to PyPI
- # only publish to PyPI on tag pushes
- if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags')
needs:
- build
runs-on: ubuntu-latest
@@ -42,59 +51,30 @@ jobs:
url: https://pypi.org/p/python-telegram-bot
permissions:
id-token: write # IMPORTANT: mandatory for trusted publishing
+ actions: read # for downloading artifacts
steps:
- name: Download all the dists
- uses: actions/download-artifact@v4
+ uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1
with:
name: python-package-distributions
path: dist/
- name: Publish to PyPI
- uses: pypa/gh-action-pypi-publish@release/v1
-
- publish-to-test-pypi:
- name: Publish to Test PyPI
- needs:
- - build
- runs-on: ubuntu-latest
- environment:
- name: release_test_pypi
- url: https://test.pypi.org/p/python-telegram-bot
- permissions:
- id-token: write # IMPORTANT: mandatory for trusted publishing
-
- steps:
- - name: Download all the dists
- uses: actions/download-artifact@v4
- with:
- name: python-package-distributions
- path: dist/
- - name: Publish to Test PyPI
- uses: pypa/gh-action-pypi-publish@release/v1
- with:
- repository-url: https://test.pypi.org/legacy/
+ uses: pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc # v1.12.4
compute-signatures:
name: Compute SHA1 Sums and Sign with Sigstore
runs-on: ubuntu-latest
needs:
- publish-to-pypi
- - publish-to-test-pypi
- # run if either of the publishing jobs ran successfully
- # see also:
- # https://github.com/actions/runner/issues/491#issuecomment-850884422
- if: |
- always() && (
- (needs.publish-to-pypi.result == 'success') ||
- (needs.publish-to-test-pypi.result == 'success')
- )
permissions:
id-token: write # IMPORTANT: mandatory for sigstore
+ actions: write # for up/downloading artifacts
steps:
- name: Download all the dists
- uses: actions/download-artifact@v4
+ uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1
with:
name: python-package-distributions
path: dist/
@@ -106,13 +86,13 @@ jobs:
sha1sum $file > $file.sha1
done
- name: Sign the dists with Sigstore
- uses: sigstore/gh-action-sigstore-python@v2.1.1
+ uses: sigstore/gh-action-sigstore-python@f514d46b907ebcd5bedc05145c03b69c1edd8b46 # v3.0.0
with:
inputs: >-
./dist/*.tar.gz
./dist/*.whl
- name: Store the distribution packages and signatures
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: python-package-distributions-and-signatures
path: dist/
@@ -120,85 +100,71 @@ jobs:
github-release:
name: Upload to GitHub Release
needs:
- - publish-to-pypi
+ - build
- compute-signatures
- if: |
- always() && (
- (needs.publish-to-pypi.result == 'success') &&
- (needs.compute-signatures.result == 'success')
- )
runs-on: ubuntu-latest
permissions:
contents: write # IMPORTANT: mandatory for making GitHub Releases
+ actions: read # for downloading artifacts
steps:
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ with:
+ persist-credentials: false
- name: Download all the dists
- uses: actions/download-artifact@v4
+ uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1
with:
name: python-package-distributions-and-signatures
path: dist/
- name: Create GitHub Release
env:
GITHUB_TOKEN: ${{ github.token }}
- # Create a GitHub Release for this tag. The description can be changed later, as for now
- # we don't define it through this workflow.
+ 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.
run: >-
+ sed "s/{tag}/$TAG/g" .github/workflows/assets/release_template.html |
gh release create
- '${{ github.ref_name }}'
+ "$TAG"
--repo '${{ github.repository }}'
- --generate-notes
+ --notes-file -
- name: Upload artifact signatures to GitHub Release
env:
GITHUB_TOKEN: ${{ github.token }}
+ TAG: ${{ needs.build.outputs.TAG }}
# Upload to GitHub Release using the `gh` CLI.
# `dist/` contains the built packages, and the
# sigstore-produced signatures and certificates.
run: >-
gh release upload
- '${{ github.ref_name }}' dist/**
+ "$TAG" dist/**
--repo '${{ github.repository }}'
- github-test-release:
- name: Upload to GitHub Release Draft
+ telegram-channel:
+ name: Publish to Telegram Channel
needs:
- - publish-to-test-pypi
- - compute-signatures
- if: |
- always() && (
- (needs.publish-to-test-pypi.result == 'success') &&
- (needs.compute-signatures.result == 'success')
- )
- runs-on: ubuntu-latest
+ - github-release
- permissions:
- contents: write # IMPORTANT: mandatory for making GitHub Releases
+ runs-on: ubuntu-latest
+ environment:
+ name: release_pypi
+ permissions: {}
steps:
- - name: Download all the dists
- uses: actions/download-artifact@v4
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
- name: python-package-distributions-and-signatures
- path: dist/
- - name: Create GitHub Release
- env:
- GITHUB_TOKEN: ${{ github.token }}
- # Create a GitHub Release *draft*. The description can be changed later, as for now
- # we don't define it through this workflow.
- run: >-
- gh release create
- '${{ github.ref_name }}'
- --repo '${{ github.repository }}'
- --generate-notes
- --draft
- - name: Upload artifact signatures to GitHub Release
+ persist-credentials: false
+ - name: Publish to Telegram Channel
env:
- GITHUB_TOKEN: ${{ github.token }}
- # Upload to GitHub Release using the `gh` CLI.
- # `dist/` contains the built packages, and the
- # sigstore-produced signatures and certificates.
+ TAG: ${{ needs.build.outputs.TAG }}
+ # This secret is configured only for the `pypi-release` branch
+ BOT_TOKEN: ${{ secrets.CHANNEL_BOT_TOKEN }}
run: >-
- gh release upload
- '${{ github.ref_name }}' dist/**
- --repo '${{ github.repository }}'
+ 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@-"
diff --git a/.github/workflows/release_test_pypi.yml b/.github/workflows/release_test_pypi.yml
new file mode 100644
index 00000000000..b4b82be06c2
--- /dev/null
+++ b/.github/workflows/release_test_pypi.yml
@@ -0,0 +1,146 @@
+name: Publish to Test PyPI
+
+on:
+ # manually trigger the workflow
+ workflow_dispatch:
+
+permissions: {}
+
+jobs:
+ build:
+ name: Build Distribution
+ runs-on: ubuntu-latest
+ outputs:
+ TAG: ${{ steps.get_tag.outputs.TAG }}
+ permissions:
+ # for uploading artifacts
+ actions: write
+
+ steps:
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ with:
+ persist-credentials: false
+ - name: Set up Python
+ uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5.5.0
+ with:
+ python-version: "3.x"
+ - name: Install pypa/build
+ run: >-
+ python3 -m pip install build --user
+ - name: Build a binary wheel and a source tarball
+ run: python3 -m build
+ - name: Store the distribution packages
+ uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
+ with:
+ name: python-package-distributions
+ path: dist/
+ - name: Get Tag Name
+ id: get_tag
+ run: |
+ pip install .
+ TAG=$(python -c "from telegram import __version__; print(f'v{__version__}')")
+ echo "TAG=$TAG" >> $GITHUB_OUTPUT
+
+ publish-to-test-pypi:
+ name: Publish to Test PyPI
+ needs:
+ - build
+ runs-on: ubuntu-latest
+ environment:
+ name: release_test_pypi
+ url: https://test.pypi.org/p/python-telegram-bot
+ permissions:
+ id-token: write # IMPORTANT: mandatory for trusted publishing
+ actions: read # for downloading artifacts
+
+ steps:
+ - name: Download all the dists
+ uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1
+ with:
+ name: python-package-distributions
+ path: dist/
+ - name: Publish to Test PyPI
+ uses: pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc # v1.12.4
+ with:
+ repository-url: https://test.pypi.org/legacy/
+
+ compute-signatures:
+ name: Compute SHA1 Sums and Sign with Sigstore
+ runs-on: ubuntu-latest
+ needs:
+ - publish-to-test-pypi
+
+ permissions:
+ id-token: write # IMPORTANT: mandatory for sigstore
+ actions: write # for up/downloading artifacts
+
+ steps:
+ - name: Download all the dists
+ uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1
+ with:
+ name: python-package-distributions
+ path: dist/
+ - name: Compute SHA1 Sums
+ run: |
+ # Compute SHA1 sum of the distribution packages and save it to a file with the same name,
+ # but with .sha1 extension
+ for file in dist/*; do
+ sha1sum $file > $file.sha1
+ done
+ - name: Sign the dists with Sigstore
+ uses: sigstore/gh-action-sigstore-python@f514d46b907ebcd5bedc05145c03b69c1edd8b46 # v3.0.0
+ with:
+ inputs: >-
+ ./dist/*.tar.gz
+ ./dist/*.whl
+ - name: Store the distribution packages and signatures
+ uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
+ with:
+ name: python-package-distributions-and-signatures
+ path: dist/
+
+ github-test-release:
+ name: Upload to GitHub Release Draft
+ needs:
+ - build
+ - compute-signatures
+
+ runs-on: ubuntu-latest
+
+ permissions:
+ contents: write # IMPORTANT: mandatory for making GitHub Releases
+ actions: read # for downloading artifacts
+
+ steps:
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ with:
+ persist-credentials: false
+ - name: Download all the dists
+ uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1
+ with:
+ name: python-package-distributions-and-signatures
+ path: dist/
+ - name: Create GitHub Release
+ 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.
+ run: >-
+ sed "s/{tag}/$TAG/g" .github/workflows/assets/release_template.html |
+ gh release create
+ "$TAG"
+ --repo '${{ github.repository }}'
+ --draft
+ --notes-file -
+ - name: Upload artifact signatures to GitHub Release
+ env:
+ GITHUB_TOKEN: ${{ github.token }}
+ TAG: ${{ needs.build.outputs.TAG }}
+ # Upload to GitHub Release using the `gh` CLI.
+ # `dist/` contains the built packages, and the
+ # sigstore-produced signatures and certificates.
+ run: >-
+ gh release upload
+ "$TAG" dist/**
+ --repo '${{ github.repository }}'
diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml
index 56ba7410946..fdbf96cc4c4 100644
--- a/.github/workflows/stale.yml
+++ b/.github/workflows/stale.yml
@@ -3,17 +3,22 @@ on:
schedule:
- cron: '42 2 * * *'
+permissions: {}
+
jobs:
stale:
runs-on: ubuntu-latest
+ permissions:
+ # For adding labels and closing
+ issues: write
steps:
- - uses: actions/stale@v9
+ - uses: actions/stale@5bef64f19d7facfb25b37b414482c7164d639639 # v9.1.0
with:
# PRs never get stale
days-before-stale: 3
days-before-close: 2
days-before-pr-stale: -1
- stale-issue-label: 'stale'
+ stale-issue-label: '📋 stale'
only-labels: 'question'
stale-issue-message: ''
close-issue-message: 'This issue has been automatically closed due to inactivity. Feel free to comment in order to reopen or ask again in our Telegram support group at https://t.me/pythontelegrambotgroup.'
diff --git a/.github/workflows/test_official.yml b/.github/workflows/test_official.yml
index 5de11471eaa..14224d0901a 100644
--- a/.github/workflows/test_official.yml
+++ b/.github/workflows/test_official.yml
@@ -11,6 +11,8 @@ on:
# 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
@@ -21,9 +23,11 @@ jobs:
os: [ubuntu-latest]
fail-fast: False
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ with:
+ persist-credentials: false
- name: Set up Python ${{ matrix.python-version }}
- uses: actions/setup-python@v5
+ uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5.5.0
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
@@ -41,7 +45,7 @@ jobs:
- name: Test Summary
id: test_summary
- uses: test-summary/action@v2.3
+ uses: test-summary/action@31493c76ec9e7aa675f1585d3ed6f1da69269a86 # v2.4
if: always() # always run, even if tests fail
with:
paths: .test_report_official.xml
diff --git a/.github/workflows/type_completeness.yml b/.github/workflows/type_completeness.yml
index 17dc249c81f..3b3f30e4873 100644
--- a/.github/workflows/type_completeness.yml
+++ b/.github/workflows/type_completeness.yml
@@ -9,12 +9,14 @@ on:
branches:
- master
+permissions: {}
+
jobs:
test-type-completeness:
name: test-type-completeness
runs-on: ubuntu-latest
steps:
- - uses: Bibo-Joshi/pyright-type-completeness@1.0.0
+ - uses: Bibo-Joshi/pyright-type-completeness@c85a67ff3c66f51dcbb2d06bfcf4fe83a57d69cc # v1.0.1
with:
package-name: telegram
python-version: 3.12
diff --git a/.github/workflows/type_completeness_monthly.yml b/.github/workflows/type_completeness_monthly.yml
new file mode 100644
index 00000000000..af7b6da7848
--- /dev/null
+++ b/.github/workflows/type_completeness_monthly.yml
@@ -0,0 +1,35 @@
+name: Check Type Completeness Monthly Run
+on:
+ schedule:
+ # Run first friday of the month at 03:17 - odd time to spread load on GitHub Actions
+ - cron: '17 3 1-7 * 5'
+
+permissions: {}
+
+jobs:
+ test-type-completeness:
+ name: test-type-completeness
+ runs-on: ubuntu-latest
+ steps:
+ - uses: Bibo-Joshi/pyright-type-completeness@c85a67ff3c66f51dcbb2d06bfcf4fe83a57d69cc # v1.0.1
+ id: pyright-type-completeness
+ with:
+ package-name: telegram
+ python-version: 3.12
+ pyright-version: ~=1.1.367
+ - name: Check Output
+ uses: jannekem/run-python-script-action@bbfca66c612a28f3eeca0ae40e1f810265e2ea68 # v1.7
+ env:
+ TYPE_COMPLETENESS: ${{ steps.pyright-type-completeness.outputs.base-completeness-score }}
+ with:
+ script: |
+ import os
+ completeness = float(os.getenv("TYPE_COMPLETENESS"))
+
+ if completeness >= 1:
+ exit(0)
+
+ text = f"Type Completeness Decreased to {completeness}. ❌"
+ error(text)
+ set_summary(text)
+ exit(1)
diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml
index 6eac67758da..a24b39a9941 100644
--- a/.github/workflows/unit_tests.yml
+++ b/.github/workflows/unit_tests.yml
@@ -14,19 +14,23 @@ on:
# Run monday and friday morning at 03:07 - odd time to spread load on GitHub Actions
- cron: '7 3 * * 1,5'
+permissions: {}
+
jobs:
pytest:
name: pytest
runs-on: ${{matrix.os}}
strategy:
matrix:
- python-version: ['3.8', '3.9', '3.10', '3.11', '3.12', '3.13.0-beta.3']
+ python-version: ['3.9', '3.10', '3.11', '3.12', '3.13']
os: [ubuntu-latest, windows-latest, macos-latest]
fail-fast: False
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ with:
+ persist-credentials: false
- name: Set up Python ${{ matrix.python-version }}
- uses: actions/setup-python@v5
+ uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5.5.0
with:
python-version: ${{ matrix.python-version }}
cache: 'pip'
@@ -37,7 +41,7 @@ jobs:
python -W ignore -m pip install -U pytest-cov
python -W ignore -m pip install .
python -W ignore -m pip install -r requirements-unit-tests.txt
- python -W ignore -m pip install pytest-xdist[psutil]
+ python -W ignore -m pip install pytest-xdist
- name: Test with pytest
# We run 4 different suites here
@@ -57,21 +61,18 @@ jobs:
# - without socks support
# - without http2 support
TO_TEST="test_no_passport.py or test_datetime.py or test_defaults.py or test_jobqueue.py or test_applicationbuilder.py or test_ratelimiter.py or test_updater.py or test_callbackdatacache.py or test_request.py"
- pytest -v --cov -k "${TO_TEST}"
- # Rerun only failed tests (--lf), and don't run any tests if none failed (--lfnf=none)
- pytest -v --cov --cov-append -k "${TO_TEST}" --lf --lfnf=none --junit-xml=.test_report_no_optionals.xml
- # No tests were selected, convert returned status code to 0
- opt_dep_status=$(( $? == 5 ? 0 : $? ))
+ pytest -v --cov -k "${TO_TEST}" --junit-xml=.test_report_no_optionals_junit.xml
+ opt_dep_status=$?
# Test the rest
export TEST_WITH_OPT_DEPS='true'
- pip install .[all]
- # `-n auto --dist loadfile` uses pytest-xdist to run each test file on a different CPU
- # worker. Increasing number of workers has little effect on test duration, but it seems
- # to increase flakyness, specially on python 3.7 with --dist=loadgroup.
- pytest -v --cov --cov-append -n auto --dist loadfile
- pytest -v --cov --cov-append -n auto --dist loadfile --lf --lfnf=none --junit-xml=.test_report_optionals.xml
- main_status=$(( $? == 5 ? 0 : $? ))
+ # need to manually install pytz here, because it's no longer in the optional reqs
+ pip install .[all] pytz
+ # `-n auto --dist worksteal` uses pytest-xdist to run tests on multiple CPU
+ # workers. Increasing number of workers has little effect on test duration, but it seems
+ # to increase flakyness.
+ pytest -v --cov --cov-append -n auto --dist worksteal --junit-xml=.test_report_optionals_junit.xml
+ main_status=$?
# exit with non-zero status if any of the two pytest runs failed
exit $(( ${opt_dep_status} || ${main_status} ))
env:
@@ -83,17 +84,23 @@ jobs:
- name: Test Summary
id: test_summary
- uses: test-summary/action@v2.3
+ uses: test-summary/action@31493c76ec9e7aa675f1585d3ed6f1da69269a86 # v2.4
if: always() # always run, even if tests fail
with:
paths: |
- .test_report_no_optionals.xml
- .test_report_optionals.xml
+ .test_report_no_optionals_junit.xml
+ .test_report_optionals_junit.xml
- name: Submit coverage
- uses: codecov/codecov-action@v4
+ uses: codecov/codecov-action@1e68e06f1dbfde0e4cefc87efeba9e4643565303 # v5.1.2
with:
env_vars: OS,PYTHON
name: ${{ matrix.os }}-${{ matrix.python-version }}
fail_ci_if_error: true
token: ${{ secrets.CODECOV_TOKEN }}
+ - name: Upload test results to Codecov
+ uses: codecov/test-results-action@f2dba722c67b86c6caa034178c6e4d35335f6706 # v1.1.0
+ if: ${{ !cancelled() }}
+ with:
+ files: .test_report_no_optionals_junit.xml,.test_report_optionals_junit.xml
+ token: ${{ secrets.CODECOV_TOKEN }}
diff --git a/.gitignore b/.gitignore
index 470d2a2aac1..9e944f66958 100644
--- a/.gitignore
+++ b/.gitignore
@@ -67,6 +67,7 @@ docs/_build/
# PyBuilder
target/
.idea/
+.run/
# Sublime Text 2
*.sublime*
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index e0d933ea11c..6b56f457ed3 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -7,7 +7,7 @@ ci:
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
- rev: 'v0.5.0'
+ rev: 'v0.8.6'
hooks:
- id: ruff
name: ruff
@@ -15,21 +15,21 @@ repos:
- httpx~=0.27
- tornado~=6.4
- APScheduler~=3.10.4
- - cachetools~=5.3.3
- - aiolimiter~=1.1.0
+ - cachetools>=5.3.3,<5.5.0
+ - aiolimiter~=1.1,<1.3
- repo: https://github.com/psf/black-pre-commit-mirror
- rev: 24.4.2
+ rev: 24.10.0
hooks:
- id: black
args:
- --diff
- --check
- repo: https://github.com/PyCQA/flake8
- rev: 7.1.0
+ rev: 7.1.1
hooks:
- id: flake8
- repo: https://github.com/PyCQA/pylint
- rev: v3.2.4
+ rev: v3.3.3
hooks:
- id: pylint
files: ^(?!(tests|docs)).*\.py$
@@ -37,11 +37,11 @@ repos:
- httpx~=0.27
- tornado~=6.4
- APScheduler~=3.10.4
- - cachetools~=5.3.3
- - aiolimiter~=1.1.0
+ - cachetools>=5.3.3,<5.5.0
+ - aiolimiter~=1.1,<1.3
- . # this basically does `pip install -e .`
- repo: https://github.com/pre-commit/mirrors-mypy
- rev: v1.10.1
+ rev: v1.14.1
hooks:
- id: mypy
name: mypy-ptb
@@ -53,8 +53,8 @@ repos:
- httpx~=0.27
- tornado~=6.4
- APScheduler~=3.10.4
- - cachetools~=5.3.3
- - aiolimiter~=1.1.0
+ - cachetools>=5.3.3,<5.5.0
+ - aiolimiter~=1.1,<1.3
- . # this basically does `pip install -e .`
- id: mypy
name: mypy-examples
@@ -65,14 +65,14 @@ repos:
additional_dependencies:
- tornado~=6.4
- APScheduler~=3.10.4
- - cachetools~=5.3.3
+ - cachetools>=5.3.3,<5.5.0
- . # this basically does `pip install -e .`
- repo: https://github.com/asottile/pyupgrade
- rev: v3.16.0
+ rev: v3.19.1
hooks:
- id: pyupgrade
args:
- - --py38-plus
+ - --py39-plus
- repo: https://github.com/pycqa/isort
rev: 5.13.2
hooks:
diff --git a/.readthedocs.yml b/.readthedocs.yml
index a23c582637d..11075b0fe2b 100644
--- a/.readthedocs.yml
+++ b/.readthedocs.yml
@@ -18,7 +18,7 @@ python:
install:
- method: pip
path: .
- - requirements: docs/requirements-docs.txt
+ - requirements: requirements-dev-all.txt
build:
os: ubuntu-22.04
diff --git a/AUTHORS.rst b/AUTHORS.rst
index 403362aaab0..61535397919 100644
--- a/AUTHORS.rst
+++ b/AUTHORS.rst
@@ -7,10 +7,8 @@ The current development team includes
- `Hinrich Mahler `_ (maintainer)
- `Poolitzer `_ (community liaison)
-- `Shivam `_
- `Harshil `_
-- `Dmitry Kolomatskiy `_
-- `Aditya `_
+- `Abdelrahman `_
Emeritus maintainers include
`Jannes Höke `_ (`@jh0ker `_ on Telegram),
@@ -21,8 +19,9 @@ Contributors
The following wonderful people contributed directly or indirectly to this project:
-- `Abdelrahman `_
+- `Aditya `_
- `Abshar `_
+- `Abubakar Alaya `_
- `Alateas `_
- `Ales Dokshanin `_
- `Alexandre `_
@@ -30,6 +29,7 @@ The following wonderful people contributed directly or indirectly to this projec
- `Ambro17 `_
- `Andrej Zhilenkov `_
- `Anton Tagunov `_
+- `Anya Marcano `
- `Avanatiker `_
- `Balduro `_
- `Bibo-Joshi `_
@@ -40,6 +40,7 @@ The following wonderful people contributed directly or indirectly to this projec
- `daimajia `_
- `Daniel Reed `_
- `D David Livingston `_
+- `Dmitry Kolomatskiy `_
- `DonalDuck004 `_
- `Eana Hufwe `_
- `Ehsan Online `_
@@ -57,12 +58,14 @@ The following wonderful people contributed directly or indirectly to this projec
- `gamgi `_
- `Gauthamram Ravichandran `_
- `Harshil `_
+- `Henry Galue `
- `Hugo Damer `_
- `ihoru `_
- `Iulian Onofrei `_
- `Jainam Oswal `_
- `Jasmin Bom `_
- `JASON0916 `_
+- `Jeamhowards Montiel `
- `jeffffc `_
- `Jelle Besseling `_
- `jh0ker `_
@@ -71,6 +74,7 @@ The following wonderful people contributed directly or indirectly to this projec
- `Joscha Götzer `_
- `jossalgon `_
- `JRoot3D `_
+- `Juan Cuevas `
- `kenjitagawa `_
- `kennethcheo `_
- `Kirill Vasin `_
@@ -80,13 +84,16 @@ The following wonderful people contributed directly or indirectly to this projec
- `LRezende `_
- `Luca Bellanti `_
- `Lucas Molinari `_
+- `Luis Pérez `_
- `macrojames `_
- `Matheus Lemos `_
- `Michael Dix `_
- `Michael Elovskikh `_
- `Miguel C. R. `_
+- `Miguel Salomon `
- `miles `_
- `Mischa Krüger `_
+- `Mohd Yusuf `_
- `naveenvhegde `_
- `neurrone `_
- `NikitaPirate `_
@@ -97,6 +104,7 @@ The following wonderful people contributed directly or indirectly to this projec
- `Oleg Sushchenko `_
- `Or Bin `_
- `overquota `_
+- `Pablo Martinez `_
- `Paradox `_
- `Patrick Hofmann `_
- `Paul Larsen `_
@@ -108,11 +116,14 @@ The following wonderful people contributed directly or indirectly to this projec
- `Rahiel Kasim `_
- `Riko Naka `_
- `Rizlas `_
+- Snehashish Biswas
- `Sahil Sharma `_
- `Sam Mosleh `_
- `Sascha `_
- `Shelomentsev D `_
+- `Shivam `_
- `Shivam Saini `_
+- `Siloé Garcez `_
- `Simon Schürrle `_
- `sooyhwang `_
- `syntx `_
diff --git a/README.rst b/README.rst
index 09b01d923a8..d19a93d4d3f 100644
--- a/README.rst
+++ b/README.rst
@@ -11,7 +11,7 @@
:target: https://pypi.org/project/python-telegram-bot/
:alt: Supported Python versions
-.. image:: https://img.shields.io/badge/Bot%20API-7.7-blue?logo=telegram
+.. image:: https://img.shields.io/badge/Bot%20API-8.3-blue?logo=telegram
:target: https://core.telegram.org/bots/api-changelog
:alt: Supported Bot API version
@@ -70,7 +70,7 @@ Introduction
This library provides a pure Python, asynchronous interface for the
`Telegram Bot API `_.
-It's compatible with Python versions **3.8+**.
+It's compatible with Python versions **3.9+**.
In addition to the pure API implementation, this library features several convenience methods and shortcuts as well as a number of high-level classes to
make the development of bots easy and straightforward. These classes are contained in the
@@ -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 **7.7** are natively supported by this library.
+All types and methods of the Telegram Bot API **8.3** are natively supported by this library.
In addition, Bot API functionality not yet natively included can still be used as described `in our wiki `_.
Notable Features
@@ -119,9 +119,9 @@ Verifying Releases
To enable you to verify that a release file that you downloaded was indeed provided by the ``python-telegram-bot`` team, we have taken the following measures.
-Starting with v21.4, all releases are signed via `sigstore `_.
+Starting with v21.4, all releases are signed via `sigstore `_.
The corresponding signature files are uploaded to the `GitHub releases page`_.
-To verify the signature, please install the `sigstore Python client `_ and follow the instructions for `verifying signatures from GitHub Actions `_. As input for the ``--repository`` parameter, please use the value ``python-telegram-bot/python-telegram-bot``.
+To verify the signature, please install the `sigstore Python client `_ and follow the instructions for `verifying signatures from GitHub Actions `_. As input for the ``--repository`` parameter, please use the value ``python-telegram-bot/python-telegram-bot``.
Earlier releases are signed with a GPG key.
The signatures are uploaded to both the `GitHub releases page`_ and the `PyPI project `_ and end with a suffix ``.asc``.
@@ -155,10 +155,10 @@ PTB can be installed with optional dependencies:
* ``pip install "python-telegram-bot[passport]"`` installs the `cryptography>=39.0.1 `_ library. Use this, if you want to use Telegram Passport related functionality.
* ``pip install "python-telegram-bot[socks]"`` installs `httpx[socks] `_. Use this, if you want to work behind a Socks5 server.
* ``pip install "python-telegram-bot[http2]"`` installs `httpx[http2] `_. Use this, if you want to use HTTP/2.
-* ``pip install "python-telegram-bot[rate-limiter]"`` installs `aiolimiter~=1.1.0 `_. Use this, if you want to use ``telegram.ext.AIORateLimiter``.
+* ``pip install "python-telegram-bot[rate-limiter]"`` installs `aiolimiter~=1.1,<1.3 `_. Use this, if you want to use ``telegram.ext.AIORateLimiter``.
* ``pip install "python-telegram-bot[webhooks]"`` installs the `tornado~=6.4 `_ library. Use this, if you want to use ``telegram.ext.Updater.start_webhook``/``telegram.ext.Application.run_webhook``.
-* ``pip install "python-telegram-bot[callback-data]"`` installs the `cachetools~=5.3.3 `_ library. Use this, if you want to use `arbitrary callback_data `_.
-* ``pip install "python-telegram-bot[job-queue]"`` installs the `APScheduler~=3.10.4 `_ library and enforces `pytz>=2018.6 `_, where ``pytz`` is a dependency of ``APScheduler``. Use this, if you want to use the ``telegram.ext.JobQueue``.
+* ``pip install "python-telegram-bot[callback-data]"`` installs the `cachetools>=5.3.3,<5.6.0 `_ library. Use this, if you want to use `arbitrary callback_data `_.
+* ``pip install "python-telegram-bot[job-queue]"`` installs the `APScheduler>=3.10.4,<3.12.0 `_ library. Use this, if you want to use the ``telegram.ext.JobQueue``.
To install multiple optional dependencies, separate them by commas, e.g. ``pip install "python-telegram-bot[socks,webhooks]"``.
@@ -230,6 +230,6 @@ License
-------
You may copy, distribute and modify the software provided that modifications are described and licensed for free under `LGPL-3 `_.
-Derivatives works (including modifications or anything statically linked to the library) can only be redistributed under LGPL-3, but applications that use the library don't have to be.
+Derivative works (including modifications or anything statically linked to the library) can only be redistributed under LGPL-3, but applications that use the library don't have to be.
-.. _`GitHub releases page`: https://github.com/python-telegram-bot/python-telegram-bot/releases>
\ No newline at end of file
+.. _`GitHub releases page`: https://github.com/python-telegram-bot/python-telegram-bot/releases
diff --git a/changes/22.0_2025-03-15/4671.7B3boYRvHdGzsrZgkpKp7B.toml b/changes/22.0_2025-03-15/4671.7B3boYRvHdGzsrZgkpKp7B.toml
new file mode 100644
index 00000000000..002b70d4ad5
--- /dev/null
+++ b/changes/22.0_2025-03-15/4671.7B3boYRvHdGzsrZgkpKp7B.toml
@@ -0,0 +1,19 @@
+breaking = """This release removes all functionality that was deprecated in v20.x. This is in line with our :ref:`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"]
diff --git a/changes/22.0_2025-03-15/4672.G9szuJ7pRafycByfem2DrT.toml b/changes/22.0_2025-03-15/4672.G9szuJ7pRafycByfem2DrT.toml
new file mode 100644
index 00000000000..c13a2d145df
--- /dev/null
+++ b/changes/22.0_2025-03-15/4672.G9szuJ7pRafycByfem2DrT.toml
@@ -0,0 +1,5 @@
+documentation = "Add `chango `_ As Changelog Management Tool"
+[[pull_requests]]
+uid = "4672"
+author_uid = "Bibo-Joshi"
+closes_threads = ["4321"]
diff --git a/changes/22.0_2025-03-15/4697.GzyGCEgj74G6bTEDbuFjUU.toml b/changes/22.0_2025-03-15/4697.GzyGCEgj74G6bTEDbuFjUU.toml
new file mode 100644
index 00000000000..abdb8f95575
--- /dev/null
+++ b/changes/22.0_2025-03-15/4697.GzyGCEgj74G6bTEDbuFjUU.toml
@@ -0,0 +1,5 @@
+internal = "Bump github/codeql-action from 3.28.8 to 3.28.10"
+[[pull_requests]]
+uid = "4697"
+author_uid = "dependabot"
+closes_threads = []
diff --git a/changes/22.0_2025-03-15/4698.Fkzr3oU2qcFmFX28xfoja5.toml b/changes/22.0_2025-03-15/4698.Fkzr3oU2qcFmFX28xfoja5.toml
new file mode 100644
index 00000000000..93eb25aa5b5
--- /dev/null
+++ b/changes/22.0_2025-03-15/4698.Fkzr3oU2qcFmFX28xfoja5.toml
@@ -0,0 +1,5 @@
+internal = "Bump srvaroa/labeler from 1.12.0 to 1.13.0"
+[[pull_requests]]
+uid = "4698"
+author_uid = "dependabot"
+closes_threads = []
diff --git a/changes/22.0_2025-03-15/4699.LYAYfKXX7C7wqT54kRPnVy.toml b/changes/22.0_2025-03-15/4699.LYAYfKXX7C7wqT54kRPnVy.toml
new file mode 100644
index 00000000000..6d028a80509
--- /dev/null
+++ b/changes/22.0_2025-03-15/4699.LYAYfKXX7C7wqT54kRPnVy.toml
@@ -0,0 +1,5 @@
+internal = "Bump astral-sh/setup-uv from 5.2.2 to 5.3.1"
+[[pull_requests]]
+uid = "4699"
+author_uid = "dependabot"
+closes_threads = []
diff --git a/changes/22.0_2025-03-15/4700.nm6R6YTnkmCo5evbykz4kz.toml b/changes/22.0_2025-03-15/4700.nm6R6YTnkmCo5evbykz4kz.toml
new file mode 100644
index 00000000000..5f5355f6d2e
--- /dev/null
+++ b/changes/22.0_2025-03-15/4700.nm6R6YTnkmCo5evbykz4kz.toml
@@ -0,0 +1,5 @@
+internal = "Bump Bibo-Joshi/chango from 0.3.1 to 0.3.2"
+[[pull_requests]]
+uid = "4700"
+author_uid = "dependabot"
+closes_threads = []
diff --git a/changes/22.0_2025-03-15/4701.ah8Wi4SWc22EbgBc4KQeqH.toml b/changes/22.0_2025-03-15/4701.ah8Wi4SWc22EbgBc4KQeqH.toml
new file mode 100644
index 00000000000..ac941f29246
--- /dev/null
+++ b/changes/22.0_2025-03-15/4701.ah8Wi4SWc22EbgBc4KQeqH.toml
@@ -0,0 +1,5 @@
+internal = "Bump pypa/gh-action-pypi-publish from 1.12.3 to 1.12.4"
+[[pull_requests]]
+uid = "4701"
+author_uid = "dependabot"
+closes_threads = []
diff --git a/changes/22.0_2025-03-15/4709.dbwPVaU8vSacVkMLhiMjyJ.toml b/changes/22.0_2025-03-15/4709.dbwPVaU8vSacVkMLhiMjyJ.toml
new file mode 100644
index 00000000000..5cc432cd401
--- /dev/null
+++ b/changes/22.0_2025-03-15/4709.dbwPVaU8vSacVkMLhiMjyJ.toml
@@ -0,0 +1,5 @@
+internal = "Bump pytest from 8.3.4 to 8.3.5"
+[[pull_requests]]
+uid = "4709"
+author_uid = "dependabot"
+closes_threads = []
diff --git a/changes/22.0_2025-03-15/4710.CSNixpvxJdLFaM6xSQ39Zf.toml b/changes/22.0_2025-03-15/4710.CSNixpvxJdLFaM6xSQ39Zf.toml
new file mode 100644
index 00000000000..4219b05acba
--- /dev/null
+++ b/changes/22.0_2025-03-15/4710.CSNixpvxJdLFaM6xSQ39Zf.toml
@@ -0,0 +1,5 @@
+internal = "Bump sphinx from 8.1.3 to 8.2.3"
+[[pull_requests]]
+uid = "4710"
+author_uid = "dependabot"
+closes_threads = []
diff --git a/changes/22.0_2025-03-15/4712.8ckkAPAXGzedityWEyv363.toml b/changes/22.0_2025-03-15/4712.8ckkAPAXGzedityWEyv363.toml
new file mode 100644
index 00000000000..63f0ad0743e
--- /dev/null
+++ b/changes/22.0_2025-03-15/4712.8ckkAPAXGzedityWEyv363.toml
@@ -0,0 +1,5 @@
+internal = "Bump Bibo-Joshi/chango from 0.3.2 to 0.4.0"
+[[pull_requests]]
+uid = "4712"
+author_uid = "Bibo-Joshi"
+closes_threads = []
diff --git a/changes/22.0_2025-03-15/4719.d4xMztC8JjrbVgEscxvMWX.toml b/changes/22.0_2025-03-15/4719.d4xMztC8JjrbVgEscxvMWX.toml
new file mode 100644
index 00000000000..c220a3eb6f3
--- /dev/null
+++ b/changes/22.0_2025-03-15/4719.d4xMztC8JjrbVgEscxvMWX.toml
@@ -0,0 +1,5 @@
+internal = "Bump Version to v22.0"
+[[pull_requests]]
+uid = "4719"
+author_uid = "Bibo-Joshi"
+closes_threads = []
diff --git a/CHANGES.rst b/changes/LEGACY.rst
similarity index 89%
rename from CHANGES.rst
rename to changes/LEGACY.rst
index f56a61b9b0c..81c4205cc29 100644
--- a/CHANGES.rst
+++ b/changes/LEGACY.rst
@@ -1,8 +1,279 @@
-.. _ptb-changelog:
+Version 21.11.1
+===============
+
+*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 `_.
+
+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 `_.
+
+Major Changes and New Features
+------------------------------
+
+- Full Support for Bot API 8.3 (:pr:`4676` closes :issue:`4677`, :pr:`4682` by `aelkheir `_, :pr:`4690` by `aelkheir `_, :pr:`4691` by `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 `_)
+- 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`)
+
+Version 21.10
+=============
+
+*Released 2025-01-03*
+
+This is the technical changelog for version 21.10. More elaborate release notes can be found in the news channel `@pythontelegrambotchannel `_.
+
+Major Changes
+-------------
+
+- Full Support for Bot API 8.2 (:pr:`4633`)
+- Bump ``apscheduler`` & Deprecate ``pytz`` Support (:pr:`4582`)
+
+New Features
+------------
+- Add Parameter ``pattern`` to ``JobQueue.jobs()`` (:pr:`4613` closes :issue:`4544`)
+- Allow Input of Type ``Sticker`` for Several Methods (:pr:`4616` closes :issue:`4580`)
+
+Bug Fixes
+---------
+- Ensure Forward Compatibility of ``Gift`` and ``Gifts`` (:pr:`4634` closes :issue:`4637`)
+
+
+Documentation Improvements & Internal Changes
+---------------------------------------------
+
+- Use Custom Labels for ``dependabot`` PRs (:pr:`4621`)
+- Remove Redundant ``pylint`` Suppressions (:pr:`4628`)
+- Update Copyright to 2025 (:pr:`4631`)
+- Refactor Module Structure and Tests for Star Payments Classes (:pr:`4615` closes :issue:`4593`)
+- Unify ``datetime`` Imports (:pr:`4605` by `cuevasrja `_ closes :issue:`4577`)
+- Add Static Security Analysis of GitHub Actions Workflows (:pr:`4606`)
+
+Dependency Updates
+------------------
+
+- Bump ``astral-sh/setup-uv`` from 4.2.0 to 5.1.0 (:pr:`4625`)
+- Bump ``codecov/codecov-action`` from 5.1.1 to 5.1.2 (:pr:`4622`)
+- Bump ``actions/upload-artifact`` from 4.4.3 to 4.5.0 (:pr:`4623`)
+- Bump ``github/codeql-action`` from 3.27.9 to 3.28.0 (:pr:`4624`)
+
+Version 21.9
+============
+
+*Released 2024-12-07*
+
+This is the technical changelog for version 21.9. More elaborate release notes can be found in the news channel `@pythontelegrambotchannel `_.
+
+Major Changes
+-------------
+
+- Full Support for Bot API 8.1 (:pr:`4594` closes :issue:`4592`)
+
+Minor Changes
+-------------
+
+- Use ``MessageLimit.DEEP_LINK_LENGTH`` in ``helpers.create_deep_linked_url`` (:pr:`4597` by `nemacysts `_)
+- Allow ``Sequence`` Input for ``allowed_updates`` in ``Application`` and ``Updater`` Methods (:pr:`4589` by `nemacysts `_)
+
+Dependency Updates
+------------------
+
+- Update ``aiolimiter`` requirement from ~=1.1.0 to >=1.1,<1.3 (:pr:`4595`)
+- Bump ``pytest`` from 8.3.3 to 8.3.4 (:pr:`4596`)
+- Bump ``codecov/codecov-action`` from 4 to 5 (:pr:`4585`)
+- Bump ``pylint`` to v3.3.2 to Improve Python 3.13 Support (:pr:`4590` by `nemacysts `_)
+- Bump ``srvaroa/labeler`` from 1.11.1 to 1.12.0 (:pr:`4586`)
+
+Version 21.8
+============
+*Released 2024-12-01*
+
+This is the technical changelog for version 21.8. More elaborate release notes can be found in the news channel `@pythontelegrambotchannel `_.
+
+Major Changes
+-------------
+
+- Full Support for Bot API 8.0 (:pr:`4568`, :pr:`4566` closes :issue:`4567`, :pr:`4572`, :pr:`4571`, :pr:`4570`, :pr:`4576`, :pr:`4574`)
+
+Documentation Improvements
+--------------------------
+
+- Documentation Improvements (:pr:`4565` by Snehashish06, :pr:`4573`)
+
+Version 21.7
+============
+*Released 2024-11-04*
+
+This is the technical changelog for version 21.7. More elaborate release notes can be found in the news channel `@pythontelegrambotchannel `_.
+
+Major Changes
+-------------
+
+- Full Support for Bot API 7.11 (:pr:`4546` closes :issue:`4543`)
+- Add ``Message.reply_paid_media`` (:pr:`4551`)
+- Drop Support for Python 3.8 (:pr:`4398` by `elpekenin `_)
+
+Minor Changes
+-------------
+
+- Allow ``Sequence`` in ``Application.add_handlers`` (:pr:`4531` by `roast-lord `_ closes :issue:`4530`)
+- Improve Exception Handling in ``File.download_*`` (:pr:`4542`)
+- Use Stable Python 3.13 Release in Test Suite (:pr:`4535`)
+
+Documentation Improvements
+--------------------------
+
+- Documentation Improvements (:pr:`4536` by `Ecode2 `_, :pr:`4556`)
+- Fix Linkcheck Workflow (:pr:`4545`)
+- Use ``sphinx-build-compatibility`` to Keep Sphinx Compatibility (:pr:`4492`)
+
+Internal Changes
+----------------
+
+- Improve Test Instability Caused by ``Message`` Fixtures (:pr:`4507`)
+- Stabilize Some Flaky Tests (:pr:`4500`)
+- Reduce Creation of HTTP Clients in Tests (:pr:`4493`)
+- Update ``pytest-xdist`` Usage (:pr:`4491`)
+- Fix Failing Tests by Making Them Independent (:pr:`4494`)
+- Introduce Codecov's Test Analysis (:pr:`4487`)
+- Maintenance Work on ``Bot`` Tests (:pr:`4489`)
+- Introduce ``conftest.py`` for File Related Tests (:pr:`4488`)
+- Update Issue Templates to Use Issue Types (:pr:`4553`)
+- Update Automation to Label Changes (:pr:`4552`)
+
+Dependency Updates
+------------------
+
+- Bump ``srvaroa/labeler`` from 1.11.0 to 1.11.1 (:pr:`4549`)
+- Bump ``sphinx`` from 8.0.2 to 8.1.3 (:pr:`4532`)
+- Bump ``sphinxcontrib-mermaid`` from 0.9.2 to 1.0.0 (:pr:`4529`)
+- Bump ``srvaroa/labeler`` from 1.10.1 to 1.11.0 (:pr:`4509`)
+- Bump ``Bibo-Joshi/pyright-type-completeness`` from 1.0.0 to 1.0.1 (:pr:`4510`)
+
+Version 21.6
+============
+
+*Released 2024-09-19*
+
+This is the technical changelog for version 21.6. More elaborate release notes can be found in the news channel `@pythontelegrambotchannel `_.
+
+New Features
+------------
+
+- Full Support for Bot API 7.10 (:pr:`4461` closes :issue:`4459`, :pr:`4460`, :pr:`4463` by `aelkheir `_, :pr:`4464`)
+- Add Parameter ``httpx_kwargs`` to ``HTTPXRequest`` (:pr:`4451` closes :issue:`4424`)
+
+Minor Changes
+-------------
+
+- Improve Type Completeness (:pr:`4466`)
+
+Internal Changes
+----------------
+
+- Update Python 3.13 Test Suite to RC2 (:pr:`4471`)
+- Enforce the ``offline_bot`` Fixture in ``Test*WithoutRequest`` (:pr:`4465`)
+- Make Tests for ``telegram.ext`` Independent of Networking (:pr:`4454`)
+- Rename Testing Base Classes (:pr:`4453`)
+
+Dependency Updates
+------------------
+
+- Bump ``pytest`` from 8.3.2 to 8.3.3 (:pr:`4475`)
+
+Version 21.5
+============
+
+*Released 2024-09-01*
+
+This is the technical changelog for version 21.5. More elaborate release notes can be found in the news channel `@pythontelegrambotchannel `_.
+
+Major Changes
+-------------
+
+- Full Support for Bot API 7.9 (:pr:`4429`)
+- Full Support for Bot API 7.8 (:pr:`4408`)
+
+New Features
+------------
+
+- Add ``MessageEntity.shift_entities`` and ``MessageEntity.concatenate`` (:pr:`4376` closes :issue:`4372`)
+- Add Parameter ``game_pattern`` to ``CallbackQueryHandler`` (:pr:`4353` by `jainamoswal `_ closes :issue:`4269`)
+- Add Parameter ``read_file_handle`` to ``InputFile`` (:pr:`4388` closes :issue:`4339`)
+
+Documentation Improvements
+--------------------------
+
+- Bugfix for "Available In" Admonitions (:pr:`4413`)
+- Documentation Improvements (:pr:`4400` closes :issue:`4446`, :pr:`4448` by `Palaptin `_)
+- Document Return Types of ``RequestData`` Members (:pr:`4396`)
+- Add Introductory Paragraphs to Telegram Types Subsections (:pr:`4389` by `mohdyusuf2312 `_ closes :issue:`4380`)
+- Start Adapting to RTD Addons (:pr:`4386`)
+
+Minor and Internal Changes
+---------------------------
+
+- Remove Surplus Logging from ``Updater`` Network Loop (:pr:`4432` by `MartinHjelmare `_)
+- Add Internal Constants for Encodings (:pr:`4378` by `elpekenin `_)
+- Improve PyPI Automation (:pr:`4375` closes :issue:`4373`)
+- Update Test Suite to New Test Channel Setup (:pr:`4435`)
+- Improve Fixture Usage in ``test_message.py`` (:pr:`4431` by `Palaptin `_)
+- Update Python 3.13 Test Suite to RC1 (:pr:`4415`)
+- Bump ``ruff`` and Add New Rules (:pr:`4416`)
+
+Dependency Updates
+------------------
-=========
-Changelog
-=========
+- Update ``cachetools`` requirement from <5.5.0,>=5.3.3 to >=5.3.3,<5.6.0 (:pr:`4437`)
+- Bump ``sphinx`` from 7.4.7 to 8.0.2 and ``furo`` from 2024.7.18 to 2024.8.6 (:pr:`4412`)
+- Bump ``test-summary/action`` from 2.3 to 2.4 (:pr:`4410`)
+- Bump ``pytest`` from 8.2.2 to 8.3.2 (:pr:`4403`)
+- Bump ``dependabot/fetch-metadata`` from 2.1.0 to 2.2.0 (:pr:`4411`)
+- Update ``cachetools`` requirement from ~=5.3.3 to >=5.3.3,<5.5.0 (:pr:`4390`)
+- Bump ``sphinx`` from 7.3.7 to 7.4.7 (:pr:`4395`)
+- Bump ``furo`` from 2024.5.6 to 2024.7.18 (:pr:`4392`)
Version 21.4
============
diff --git a/changes/config.py b/changes/config.py
new file mode 100644
index 00000000000..1fd95fa9767
--- /dev/null
+++ b/changes/config.py
@@ -0,0 +1,105 @@
+# noqa: INP001
+# pylint: disable=import-error
+"""Configuration for the chango changelog tool"""
+
+import re
+from collections.abc import Collection
+from pathlib import Path
+from typing import Optional
+
+from chango import Version
+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"}
+
+
+class CustomChango(DirectoryChanGo):
+ """Custom ChanGo class for overriding release"""
+
+ def release(self, version: Version) -> bool:
+ """replace "14.5" with version.uid except in the contrib guide
+ then call super
+ """
+ root = Path(__file__).parent.parent / "telegram"
+ python_files = root.rglob("*.py")
+ pattern = re.compile(r"NEXT\.VERSION")
+ excluded_paths = {root / "docs/source/contribute.rst"}
+ for file_path in python_files:
+ if str(file_path) in excluded_paths:
+ continue
+
+ content = file_path.read_text(encoding="utf-8")
+ modified = pattern.sub(version.uid, content)
+
+ if content != modified:
+ file_path.write_text(modified, encoding="utf-8")
+
+ return super().release(version)
+
+
+chango_instance = CustomChango(
+ change_note_type=ChangoSectionChangeNote,
+ version_note_type=SectionVersionNote,
+ version_history_type=HeaderVersionHistory,
+ scanner=version_scanner,
+)
diff --git a/changes/unreleased/.gitkeep b/changes/unreleased/.gitkeep
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/changes/unreleased/4692.dVZs28GuwTFnNJdWkvPbNv.toml b/changes/unreleased/4692.dVZs28GuwTFnNJdWkvPbNv.toml
new file mode 100644
index 00000000000..aebbd7e67c1
--- /dev/null
+++ b/changes/unreleased/4692.dVZs28GuwTFnNJdWkvPbNv.toml
@@ -0,0 +1,5 @@
+breaking = "Drop backward compatibility for `user_id` in `send_gift` by updating the order of parameters. Please adapt your code accordingly or use keyword arguments."
+[[pull_requests]]
+uid = "4692"
+author_uid = "Bibo-Joshi"
+closes_threads = []
diff --git a/changes/unreleased/4733.BRLwsEuh76974FPJRuiBjf.toml b/changes/unreleased/4733.BRLwsEuh76974FPJRuiBjf.toml
new file mode 100644
index 00000000000..579b6c3b37d
--- /dev/null
+++ b/changes/unreleased/4733.BRLwsEuh76974FPJRuiBjf.toml
@@ -0,0 +1,5 @@
+bugfixes = "Ensure execution of ``Bot.shutdown`` even if ``Bot.get_me()`` fails in ``Bot.initialize()``"
+[[pull_requests]]
+uid = "4733"
+author_uid = "Poolitzer"
+closes_threads = []
diff --git a/changes/unreleased/4741.nVLBrFX4p8jTCBjMRqaYoQ.toml b/changes/unreleased/4741.nVLBrFX4p8jTCBjMRqaYoQ.toml
new file mode 100644
index 00000000000..aacb5f2d501
--- /dev/null
+++ b/changes/unreleased/4741.nVLBrFX4p8jTCBjMRqaYoQ.toml
@@ -0,0 +1,5 @@
+internal = "Bump codecov/test-results-action from 1.0.2 to 1.1.0"
+[[pull_requests]]
+uid = "4741"
+author_uid = "dependabot"
+closes_threads = []
diff --git a/changes/unreleased/4742.oEA6MjYXMafdbu2akWT5tC.toml b/changes/unreleased/4742.oEA6MjYXMafdbu2akWT5tC.toml
new file mode 100644
index 00000000000..97463ed483f
--- /dev/null
+++ b/changes/unreleased/4742.oEA6MjYXMafdbu2akWT5tC.toml
@@ -0,0 +1,5 @@
+internal = "Bump actions/setup-python from 5.4.0 to 5.5.0"
+[[pull_requests]]
+uid = "4742"
+author_uid = "dependabot"
+closes_threads = []
diff --git a/changes/unreleased/4743.SpMm4vvAMjEreykTcGwzcF.toml b/changes/unreleased/4743.SpMm4vvAMjEreykTcGwzcF.toml
new file mode 100644
index 00000000000..b6724ab2917
--- /dev/null
+++ b/changes/unreleased/4743.SpMm4vvAMjEreykTcGwzcF.toml
@@ -0,0 +1,5 @@
+internal = "Bump github/codeql-action from 3.28.10 to 3.28.13"
+[[pull_requests]]
+uid = "4743"
+author_uid = "dependabot"
+closes_threads = []
diff --git a/changes/unreleased/4744.a4tsF64kZPA2noP7HtTzTX.toml b/changes/unreleased/4744.a4tsF64kZPA2noP7HtTzTX.toml
new file mode 100644
index 00000000000..cb5f24ea554
--- /dev/null
+++ b/changes/unreleased/4744.a4tsF64kZPA2noP7HtTzTX.toml
@@ -0,0 +1,5 @@
+internal = "Bump astral-sh/setup-uv from 5.3.1 to 5.4.1"
+[[pull_requests]]
+uid = "4744"
+author_uid = "dependabot"
+closes_threads = []
diff --git a/changes/unreleased/4745.emNmhxtvtTP9uLNQxpcVSj.toml b/changes/unreleased/4745.emNmhxtvtTP9uLNQxpcVSj.toml
new file mode 100644
index 00000000000..cae16287a79
--- /dev/null
+++ b/changes/unreleased/4745.emNmhxtvtTP9uLNQxpcVSj.toml
@@ -0,0 +1,5 @@
+internal = "Bump actions/download-artifact from 4.1.8 to 4.2.1"
+[[pull_requests]]
+uid = "4745"
+author_uid = "dependabot"
+closes_threads = []
diff --git a/changes/unreleased/4746.gWnX3BCxbvujQ8B2QejtTK.toml b/changes/unreleased/4746.gWnX3BCxbvujQ8B2QejtTK.toml
new file mode 100644
index 00000000000..c24204d45c4
--- /dev/null
+++ b/changes/unreleased/4746.gWnX3BCxbvujQ8B2QejtTK.toml
@@ -0,0 +1,5 @@
+internal = "Reenable ``test_official`` Blocked by Debug Remnant"
+[[pull_requests]]
+uid = "4746"
+author_uid = "aelkheir"
+closes_threads = []
diff --git a/changes/unreleased/4747.MLmApvpGdwN7J24j7fXsDU.toml b/changes/unreleased/4747.MLmApvpGdwN7J24j7fXsDU.toml
new file mode 100644
index 00000000000..e6bb47332f9
--- /dev/null
+++ b/changes/unreleased/4747.MLmApvpGdwN7J24j7fXsDU.toml
@@ -0,0 +1,5 @@
+documentation = "Update ``AUTHORS.rst``, Adding `@aelkheir `_ to Active Development Team"
+[[pull_requests]]
+uid = "4747"
+author_uid = "Bibo-Joshi"
+closes_threads = []
diff --git a/changes/unreleased/4758.dSyCdBJWEJroH2GynR2VaJ.toml b/changes/unreleased/4758.dSyCdBJWEJroH2GynR2VaJ.toml
new file mode 100644
index 00000000000..23ffc153339
--- /dev/null
+++ b/changes/unreleased/4758.dSyCdBJWEJroH2GynR2VaJ.toml
@@ -0,0 +1,5 @@
+internal = "Fine Tune ``chango`` and Release Workflows"
+[[pull_requests]]
+uid = "4758"
+author_uid = "Bibo-Joshi"
+closes_threads = ["4720"]
diff --git a/changes/unreleased/4761.mmsngFA6b4ccdEzEpFTZS3.toml b/changes/unreleased/4761.mmsngFA6b4ccdEzEpFTZS3.toml
new file mode 100644
index 00000000000..a47dcdcc3b1
--- /dev/null
+++ b/changes/unreleased/4761.mmsngFA6b4ccdEzEpFTZS3.toml
@@ -0,0 +1,6 @@
+bugfixes = "Fix Handling of ``Defaults`` for ``InputPaidMedia``"
+
+[[pull_requests]]
+uid = "4761"
+author_uid = "ngrogolev"
+closes_threads = ["4753"]
diff --git a/changes/unreleased/4762.PbcJGM8KPBMbKri3fdHKjh.toml b/changes/unreleased/4762.PbcJGM8KPBMbKri3fdHKjh.toml
new file mode 100644
index 00000000000..0aeebe750b6
--- /dev/null
+++ b/changes/unreleased/4762.PbcJGM8KPBMbKri3fdHKjh.toml
@@ -0,0 +1,5 @@
+documentation = "Clarify Documentation and Type Hints of ``InputMedia`` and ``InputPaidMedia``. Note that the ``media`` parameter accepts only objects of type ``str`` and ``InputFile``. The respective subclasses of ``Input(Paid)Media`` each accept a broader range of input type for the ``media`` parameter."
+[[pull_requests]]
+uid = "4762"
+author_uid = "Bibo-Joshi"
+closes_threads = []
diff --git a/docs/auxil/admonition_inserter.py b/docs/auxil/admonition_inserter.py
index 4227a845382..56d63d08cb2 100644
--- a/docs/auxil/admonition_inserter.py
+++ b/docs/auxil/admonition_inserter.py
@@ -1,6 +1,6 @@
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2024
+# Copyright (C) 2015-2025
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -16,17 +16,55 @@
# 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 collections.abc
+import contextlib
import inspect
import re
import typing
from collections import defaultdict
-from typing import Any, Iterator, Union
+from collections.abc import Iterator
+from socket import socket
+from types import FunctionType
+from typing import Union
+
+from apscheduler.job import Job as APSJob
import telegram
+import telegram._utils.defaultvalue
+import telegram._utils.types
import telegram.ext
+import telegram.ext._utils.types
+from tests.auxil.slots import mro_slots
+
+# Define the namespace for type resolution. This helps dealing with the internal imports that
+# we do in many places
+# The .copy() is important to avoid modifying the original namespace
+TG_NAMESPACE = vars(telegram).copy()
+TG_NAMESPACE.update(vars(telegram._utils.types))
+TG_NAMESPACE.update(vars(telegram._utils.defaultvalue))
+TG_NAMESPACE.update(vars(telegram.ext))
+TG_NAMESPACE.update(vars(telegram.ext._utils.types))
+TG_NAMESPACE.update(vars(telegram.ext._applicationbuilder))
+TG_NAMESPACE.update({"socket": socket, "APSJob": APSJob})
+
+
+class PublicMethod(typing.NamedTuple):
+ name: str
+ method: FunctionType
+
+
+def _is_inherited_method(cls: type, method_name: str) -> bool:
+ """Checks if a method is inherited from a parent class.
+ Inheritance is not considered if the parent class is private.
+ Recurses through all direcot or indirect parent classes.
+ """
+ # The [1:] slice is used to exclude the class itself from the MRO.
+ for base in cls.__mro__[1:]:
+ if method_name in base.__dict__ and not base.__name__.startswith("_"):
+ return True
+ return False
-def _iter_own_public_methods(cls: type) -> Iterator[tuple[str, type]]:
+def _iter_own_public_methods(cls: type) -> Iterator[PublicMethod]:
"""Iterates over methods of a class that are not protected/private,
not camelCase and not inherited from the parent class.
@@ -34,13 +72,15 @@ def _iter_own_public_methods(cls: type) -> Iterator[tuple[str, type]]:
This function is defined outside the class because it is used to create class constants.
"""
- return (
- m
- for m in inspect.getmembers(cls, predicate=inspect.isfunction) # not .ismethod
- if not m[0].startswith("_")
- and m[0].islower() # to avoid camelCase methods
- and m[0] in cls.__dict__ # method is not inherited from parent class
- )
+
+ # Use .isfunction() instead of .ismethod() because we want to include static methods.
+ for m in inspect.getmembers(cls, predicate=inspect.isfunction):
+ if (
+ not m[0].startswith("_")
+ and m[0].islower() # to avoid camelCase methods
+ and not _is_inherited_method(cls, m[0])
+ ):
+ yield PublicMethod(m[0], m[1])
class AdmonitionInserter:
@@ -57,18 +97,12 @@ class AdmonitionInserter:
start and end markers.
"""
- FORWARD_REF_SKIP_PATTERN = re.compile(r"^ForwardRef\('DefaultValue\[\w+]'\)$")
- """A pattern that will be used to skip known ForwardRef's that need not be resolved
- to a Telegram class, e.g.:
- ForwardRef('DefaultValue[None]')
- ForwardRef('DefaultValue[DVValueType]')
- """
-
- METHOD_NAMES_FOR_BOT_AND_APPBUILDER: typing.ClassVar[dict[type, str]] = {
- cls: tuple(m[0] for m in _iter_own_public_methods(cls)) # m[0] means we take only names
- for cls in (telegram.Bot, telegram.ext.ApplicationBuilder)
+ METHOD_NAMES_FOR_BOT_APP_APPBUILDER: typing.ClassVar[dict[type, str]] = {
+ cls: tuple(m.name for m in _iter_own_public_methods(cls))
+ for cls in (telegram.Bot, telegram.ext.ApplicationBuilder, telegram.ext.Application)
}
- """A dictionary mapping Bot and ApplicationBuilder classes to their relevant methods that will
+ """A dictionary mapping Bot, Application & ApplicationBuilder classes to their relevant methods
+ that will
be mentioned in 'Returned in' and 'Use in' admonitions in other classes' docstrings.
Methods must be public, not aliases, not inherited from TelegramObject.
"""
@@ -82,13 +116,20 @@ def __init__(self):
"""Dictionary with admonitions. Contains sub-dictionaries, one per admonition type.
Each sub-dictionary matches bot methods (for "Shortcuts") or telegram classes (for other
admonition types) to texts of admonitions, e.g.:
+
```
{
- "use_in": {:
- <"Use in" admonition for ChatInviteLink>, ...},
- "available_in": {:
- <"Available in" admonition">, ...},
- "returned_in": {...}
+ "use_in": {
+ :
+ <"Use in" admonition for ChatInviteLink>,
+ ...
+ },
+ "available_in": {
+ :
+ <"Available in" admonition">,
+ ...
+ },
+ "returned_in": {...}
}
```
"""
@@ -127,34 +168,6 @@ def _create_available_in(self) -> dict[type, str]:
# i.e. {telegram._files.sticker.Sticker: {":attr:`telegram.Message.sticker`", ...}}
attrs_for_class = defaultdict(set)
- # The following regex is supposed to capture a class name in a line like this:
- # media (:obj:`str` | :class:`telegram.InputFile`): Audio file to send.
- #
- # Note that even if such typing description spans over multiple lines but each line ends
- # with a backslash (otherwise Sphinx will throw an error)
- # (e.g. EncryptedPassportElement.data), then Sphinx will combine these lines into a single
- # line automatically, and it will contain no backslash (only some extra many whitespaces
- # from the indentation).
-
- attr_docstr_pattern = re.compile(
- r"^\s*(?P[a-z_]+)" # Any number of spaces, named group for attribute
- r"\s?\(" # Optional whitespace, opening parenthesis
- r".*" # Any number of characters (that could denote a built-in type)
- r":class:`.+`" # Marker of a classref, class name in backticks
- r".*\):" # Any number of characters, closing parenthesis, colon.
- # The ^ colon above along with parenthesis is important because it makes sure that
- # the class is mentioned in the attribute description, not in free text.
- r".*$", # Any number of characters, end of string (end of line)
- re.VERBOSE,
- )
-
- # for properties: there is no attr name in docstring. Just check if there's a class name.
- prop_docstring_pattern = re.compile(r":class:`.+`.*:")
-
- # pattern for iterating over potentially many class names in docstring for one attribute.
- # Tilde is optional (sometimes it is in the docstring, sometimes not).
- single_class_name_pattern = re.compile(r":class:`~?(?P[\w.]*)`")
-
classes_to_inspect = inspect.getmembers(telegram, inspect.isclass) + inspect.getmembers(
telegram.ext, inspect.isclass
)
@@ -165,40 +178,31 @@ def _create_available_in(self) -> dict[type, str]:
# docstrings.
name_of_inspected_class_in_docstr = self._generate_class_name_for_link(inspected_class)
- # Parsing part of the docstring with attributes (parsing of properties follows later)
- docstring_lines = inspect.getdoc(inspected_class).splitlines()
- lines_with_attrs = []
- for idx, line in enumerate(docstring_lines):
- if line.strip() == "Attributes:":
- lines_with_attrs = docstring_lines[idx + 1 :]
- break
-
- for line in lines_with_attrs:
- if not (line_match := attr_docstr_pattern.match(line)):
- continue
-
- target_attr = line_match.group("attr_name")
- # a typing description of one attribute can contain multiple classes
- for match in single_class_name_pattern.finditer(line):
- name_of_class_in_attr = match.group("class_name")
-
- # Writing to dictionary: matching the class found in the docstring
- # and its subclasses to the attribute of the class being inspected.
- # The class in the attribute docstring (or its subclass) is the key,
- # ReST link to attribute of the class currently being inspected is the value.
- try:
- self._resolve_arg_and_add_link(
- arg=name_of_class_in_attr,
- dict_of_methods_for_class=attrs_for_class,
- link=f":attr:`{name_of_inspected_class_in_docstr}.{target_attr}`",
- )
- except NotImplementedError as e:
- raise NotImplementedError(
- "Error generating Sphinx 'Available in' admonition "
- f"(admonition_inserter.py). Class {name_of_class_in_attr} present in "
- f"attribute {target_attr} of class {name_of_inspected_class_in_docstr}"
- f" could not be resolved. {e!s}"
- ) from e
+ # Writing to dictionary: matching the class found in the type hint
+ # and its subclasses to the attribute of the class being inspected.
+ # The class in the attribute typehint (or its subclass) is the key,
+ # ReST link to attribute of the class currently being inspected is the value.
+
+ # best effort - args of __init__ means not all attributes are covered, but there is no
+ # other way to get type hints of all attributes, other than doing ast parsing maybe.
+ # (Docstring parsing was discontinued with the closing of #4414)
+ type_hints = typing.get_type_hints(inspected_class.__init__, localns=TG_NAMESPACE)
+ class_attrs = [slot for slot in mro_slots(inspected_class) if not slot.startswith("_")]
+ for target_attr in class_attrs:
+ try:
+ self._resolve_arg_and_add_link(
+ dict_of_methods_for_class=attrs_for_class,
+ link=f":attr:`{name_of_inspected_class_in_docstr}.{target_attr}`",
+ type_hints={target_attr: type_hints.get(target_attr)},
+ resolve_nested_type_vars=False,
+ )
+ except NotImplementedError as e:
+ raise NotImplementedError(
+ "Error generating Sphinx 'Available in' admonition "
+ f"(admonition_inserter.py). Class {inspected_class} present in "
+ f"attribute {target_attr} of class {name_of_inspected_class_in_docstr}"
+ f" could not be resolved. {e!s}"
+ ) from e
# Properties need to be parsed separately because they act like attributes but not
# listed as attributes.
@@ -209,39 +213,29 @@ def _create_available_in(self) -> dict[type, str]:
if prop_name not in inspected_class.__dict__:
continue
- # 1. Can't use typing.get_type_hints because double-quoted type hints
- # (like "Application") will throw a NameError
- # 2. Can't use inspect.signature because return annotations of properties can be
- # hard to parse (like "(self) -> BD").
- # 3. fget is used to access the actual function under the property wrapper
- docstring = inspect.getdoc(getattr(inspected_class, prop_name).fget)
- if docstring is None:
- continue
-
- first_line = docstring.splitlines()[0]
- if not prop_docstring_pattern.match(first_line):
- continue
+ # fget is used to access the actual function under the property wrapper
+ type_hints = typing.get_type_hints(
+ getattr(inspected_class, prop_name).fget, localns=TG_NAMESPACE
+ )
- for match in single_class_name_pattern.finditer(first_line):
- name_of_class_in_prop = match.group("class_name")
-
- # Writing to dictionary: matching the class found in the docstring and its
- # subclasses to the property of the class being inspected.
- # The class in the property docstring (or its subclass) is the key,
- # ReST link to property of the class currently being inspected is the value.
- try:
- self._resolve_arg_and_add_link(
- arg=name_of_class_in_prop,
- dict_of_methods_for_class=attrs_for_class,
- link=f":attr:`{name_of_inspected_class_in_docstr}.{prop_name}`",
- )
- except NotImplementedError as e:
- raise NotImplementedError(
- "Error generating Sphinx 'Available in' admonition "
- f"(admonition_inserter.py). Class {name_of_class_in_prop} present in "
- f"property {prop_name} of class {name_of_inspected_class_in_docstr}"
- f" could not be resolved. {e!s}"
- ) from e
+ # Writing to dictionary: matching the class found in the docstring and its
+ # subclasses to the property of the class being inspected.
+ # The class in the property docstring (or its subclass) is the key,
+ # ReST link to property of the class currently being inspected is the value.
+ try:
+ self._resolve_arg_and_add_link(
+ dict_of_methods_for_class=attrs_for_class,
+ link=f":attr:`{name_of_inspected_class_in_docstr}.{prop_name}`",
+ type_hints={prop_name: type_hints.get("return")},
+ resolve_nested_type_vars=False,
+ )
+ except NotImplementedError as e:
+ raise NotImplementedError(
+ "Error generating Sphinx 'Available in' admonition "
+ f"(admonition_inserter.py). Class {inspected_class} present in "
+ f"property {prop_name} of class {name_of_inspected_class_in_docstr}"
+ f" could not be resolved. {e!s}"
+ ) from e
return self._generate_admonitions(attrs_for_class, admonition_type="available_in")
@@ -249,29 +243,28 @@ def _create_returned_in(self) -> dict[type, str]:
"""Creates a dictionary with 'Returned in' admonitions for classes that are returned
in Bot's and ApplicationBuilder's methods.
"""
-
# Generate a mapping of classes to ReST links to Bot methods which return it,
# i.e. {: {:meth:`telegram.Bot.send_message`, ...}}
methods_for_class = defaultdict(set)
- for cls, method_names in self.METHOD_NAMES_FOR_BOT_AND_APPBUILDER.items():
+ for cls, method_names in self.METHOD_NAMES_FOR_BOT_APP_APPBUILDER.items():
for method_name in method_names:
- sig = inspect.signature(getattr(cls, method_name))
- ret_annot = sig.return_annotation
-
method_link = self._generate_link_to_method(method_name, cls)
+ arg = getattr(cls, method_name)
+ ret_type_hint = typing.get_type_hints(arg, localns=TG_NAMESPACE)
try:
self._resolve_arg_and_add_link(
- arg=ret_annot,
dict_of_methods_for_class=methods_for_class,
link=method_link,
+ type_hints={"return": ret_type_hint.get("return")},
+ resolve_nested_type_vars=False,
)
except NotImplementedError as e:
raise NotImplementedError(
"Error generating Sphinx 'Returned in' admonition "
f"(admonition_inserter.py). {cls}, method {method_name}. "
- f"Couldn't resolve type hint in return annotation {ret_annot}. {e!s}"
+ f"Couldn't resolve type hint in return annotation {ret_type_hint}. {e!s}"
) from e
return self._generate_admonitions(methods_for_class, admonition_type="returned_in")
@@ -298,8 +291,13 @@ def _create_shortcuts(self) -> dict[collections.abc.Callable, str]:
# inspect methods of all telegram classes for return statements that indicate
# that this given method is a shortcut for a Bot method
for _class_name, cls in inspect.getmembers(telegram, predicate=inspect.isclass):
- # no need to inspect Bot's own methods, as Bot can't have shortcuts in Bot
+ if not cls.__module__.startswith("telegram"):
+ # For some reason inspect.getmembers() also yields some classes that are
+ # imported in the namespace but not part of the telegram module.
+ continue
+
if cls is telegram.Bot:
+ # no need to inspect Bot's own methods, as Bot can't have shortcuts in Bot
continue
for method_name, method in _iter_own_public_methods(cls):
@@ -309,9 +307,7 @@ def _create_shortcuts(self) -> dict[collections.abc.Callable, str]:
continue
bot_method = getattr(telegram.Bot, bot_method_match.group())
-
link_to_shortcut_method = self._generate_link_to_method(method_name, cls)
-
shortcuts_for_bot_method[bot_method].add(link_to_shortcut_method)
return self._generate_admonitions(shortcuts_for_bot_method, admonition_type="shortcuts")
@@ -326,26 +322,24 @@ def _create_use_in(self) -> dict[type, str]:
# {:meth:`telegram.Bot.answer_inline_query`, ...}}
methods_for_class = defaultdict(set)
- for cls, method_names in self.METHOD_NAMES_FOR_BOT_AND_APPBUILDER.items():
+ for cls, method_names in self.METHOD_NAMES_FOR_BOT_APP_APPBUILDER.items():
for method_name in method_names:
method_link = self._generate_link_to_method(method_name, cls)
- sig = inspect.signature(getattr(cls, method_name))
- parameters = sig.parameters
-
- for param in parameters.values():
- try:
- self._resolve_arg_and_add_link(
- arg=param.annotation,
- dict_of_methods_for_class=methods_for_class,
- link=method_link,
- )
- except NotImplementedError as e:
- raise NotImplementedError(
- "Error generating Sphinx 'Use in' admonition "
- f"(admonition_inserter.py). {cls}, method {method_name}, parameter "
- f"{param}: Couldn't resolve type hint {param.annotation}. {e!s}"
- ) from e
+ arg = getattr(cls, method_name)
+ param_type_hints = typing.get_type_hints(arg, localns=TG_NAMESPACE)
+ param_type_hints.pop("return", None)
+ try:
+ self._resolve_arg_and_add_link(
+ dict_of_methods_for_class=methods_for_class,
+ link=method_link,
+ type_hints=param_type_hints,
+ )
+ except NotImplementedError as e:
+ raise NotImplementedError(
+ "Error generating Sphinx 'Use in' admonition "
+ f"(admonition_inserter.py). {cls}, method {method_name}, parameter "
+ ) from e
return self._generate_admonitions(methods_for_class, admonition_type="use_in")
@@ -361,11 +355,12 @@ def _find_insert_pos_for_admonition(lines: list[str]) -> int:
for idx, value in list(enumerate(lines)):
if value.startswith(
(
- ".. seealso:",
+ # ".. seealso:",
# The docstring contains heading "Examples:", but Sphinx will have it converted
# to ".. admonition: Examples":
".. admonition:: Examples",
".. version",
+ "Args:",
# The space after ":param" is important because docstring can contain
# ":paramref:" in its plain text in the beginning of a line (e.g. ExtBot):
":param ",
@@ -433,12 +428,12 @@ def _generate_admonitions(
return admonition_for_class
@staticmethod
- def _generate_class_name_for_link(cls: type) -> str:
+ def _generate_class_name_for_link(cls_: type) -> str:
"""Generates class name that can be used in a ReST link."""
# Check for potential presence of ".ext.", we will need to keep it.
- ext = ".ext" if ".ext." in str(cls) else ""
- return f"telegram{ext}.{cls.__name__}"
+ ext = ".ext" if ".ext." in str(cls_) else ""
+ return f"telegram{ext}.{cls_.__name__}"
def _generate_link_to_method(self, method_name: str, cls: type) -> str:
"""Generates a ReST link to a method of a telegram class."""
@@ -446,19 +441,22 @@ def _generate_link_to_method(self, method_name: str, cls: type) -> str:
return f":meth:`{self._generate_class_name_for_link(cls)}.{method_name}`"
@staticmethod
- def _iter_subclasses(cls: type) -> Iterator:
+ def _iter_subclasses(cls_: type) -> Iterator:
+ if not hasattr(cls_, "__subclasses__") or cls_ is telegram.TelegramObject:
+ return iter([])
return (
# exclude private classes
c
- for c in cls.__subclasses__()
+ for c in cls_.__subclasses__()
if not str(c).split(".")[-1].startswith("_")
)
def _resolve_arg_and_add_link(
self,
- arg: Any,
dict_of_methods_for_class: defaultdict,
link: str,
+ type_hints: dict[str, type],
+ resolve_nested_type_vars: bool = True,
) -> None:
"""A helper method. Tries to resolve the arg into a valid class. In case of success,
adds the link (to a method, attribute, or property) for that class' and its subclasses'
@@ -466,7 +464,9 @@ def _resolve_arg_and_add_link(
**Modifies dictionary in place.**
"""
- for cls in self._resolve_arg(arg):
+ type_hints.pop("self", None)
+
+ for cls in self._resolve_arg(type_hints, resolve_nested_type_vars):
# When trying to resolve an argument from args or return annotation,
# the method _resolve_arg returns None if nothing could be resolved.
# Also, if class was resolved correctly, "telegram" will definitely be in its str().
@@ -478,88 +478,67 @@ def _resolve_arg_and_add_link(
for subclass in self._iter_subclasses(cls):
dict_of_methods_for_class[subclass].add(link)
- def _resolve_arg(self, arg: Any) -> Iterator[Union[type, None]]:
+ def _resolve_arg(
+ self,
+ type_hints: dict[str, type],
+ resolve_nested_type_vars: bool,
+ ) -> list[type]:
"""Analyzes an argument of a method and recursively yields classes that the argument
or its sub-arguments (in cases like Union[...]) belong to, if they can be resolved to
telegram or telegram.ext classes.
+ Args:
+ type_hints: A dictionary of argument names and their types.
+ resolve_nested_type_vars: If True, nested type variables (like Application[BT, …])
+ will be resolved to their actual classes. If False, only the outermost type
+ variable will be resolved. *Only* affects ptb classes, not built-in types.
+ Useful for checking the return type of methods, where nested type variables
+ are not really useful.
+
Raises `NotImplementedError`.
"""
- origin = typing.get_origin(arg)
+ def _is_ptb_class(cls: type) -> bool:
+ if not hasattr(cls, "__module__"):
+ return False
+ return cls.__module__.startswith("telegram")
- if (
- origin in (collections.abc.Callable, typing.IO)
- or arg is None
- # no other check available (by type or origin) for these:
- or str(type(arg)) in ("", "")
- ):
- pass
-
- # RECURSIVE CALLS
- # for cases like Union[Sequence....
- elif origin in (
- Union,
- collections.abc.Coroutine,
- collections.abc.Sequence,
- ):
- for sub_arg in typing.get_args(arg):
- yield from self._resolve_arg(sub_arg)
-
- elif isinstance(arg, typing.TypeVar):
- # gets access to the "bound=..." parameter
- yield from self._resolve_arg(arg.__bound__)
- # END RECURSIVE CALLS
-
- elif isinstance(arg, typing.ForwardRef):
- m = self.FORWARD_REF_PATTERN.match(str(arg))
- # We're sure it's a ForwardRef, so, unless it belongs to known exceptions,
- # the class must be resolved.
- # If it isn't resolved, we'll have the program throw an exception to be sure.
- try:
- cls = self._resolve_class(m.group("class_name"))
- except AttributeError as exc:
- # skip known ForwardRef's that need not be resolved to a Telegram class
- if self.FORWARD_REF_SKIP_PATTERN.match(str(arg)):
- pass
- else:
- raise NotImplementedError(f"Could not process ForwardRef: {arg}") from exc
- else:
- yield cls
-
- # For custom generics like telegram.ext._application.Application[~BT, ~CCT, ~UD...].
- # This must come before the check for isinstance(type) because GenericAlias can also be
- # recognized as type if it belongs to .
- elif str(type(arg)) in (
- "",
- "",
- "",
- ):
- if "telegram" in str(arg):
- # get_origin() of telegram.ext._application.Application[~BT, ~CCT, ~UD...]
- # will produce
- yield origin
-
- elif isinstance(arg, type):
- if "telegram" in str(arg):
- yield arg
-
- # For some reason "InlineQueryResult", "InputMedia" & some others are currently not
- # recognized as ForwardRefs and are identified as plain strings.
- elif isinstance(arg, str):
- # args like "ApplicationBuilder[BT, CCT, UD, CD, BD, JQ]" can be recognized as strings.
- # Remove whatever is in the square brackets because it doesn't need to be parsed.
- arg = re.sub(r"\[.+]", "", arg)
-
- cls = self._resolve_class(arg)
- # Here we don't want an exception to be thrown since we're not sure it's ForwardRef
- if cls is not None:
- yield cls
-
- else:
- raise NotImplementedError(
- f"Cannot process argument {arg} of type {type(arg)} (origin {origin})"
- )
+ # will be edited in place
+ telegram_classes = set()
+
+ def recurse_type(type_, is_recursed_from_ptb_class: bool):
+ next_is_recursed_from_ptb_class = is_recursed_from_ptb_class or _is_ptb_class(type_)
+
+ if hasattr(type_, "__origin__"): # For generic types like Union, List, etc.
+ # Make sure it's not a telegram.ext generic type (e.g. ContextTypes[...])
+ org = typing.get_origin(type_)
+ if "telegram.ext" in str(org):
+ telegram_classes.add(org)
+
+ args = typing.get_args(type_)
+ for arg in args:
+ recurse_type(arg, next_is_recursed_from_ptb_class)
+ elif isinstance(type_, typing.TypeVar) and (
+ resolve_nested_type_vars or not is_recursed_from_ptb_class
+ ):
+ # gets access to the "bound=..." parameter
+ recurse_type(type_.__bound__, next_is_recursed_from_ptb_class)
+ elif inspect.isclass(type_) and "telegram" in inspect.getmodule(type_).__name__:
+ telegram_classes.add(type_)
+ elif isinstance(type_, typing.ForwardRef):
+ # Resolving ForwardRef is not easy. https://peps.python.org/pep-0749/ will
+ # hopefully make it better by introducing typing.resolve_forward_ref() in py3.14
+ # but that's not there yet
+ # So for now we fall back to a best effort approach of guessing if the class is
+ # available in tg or tg.ext
+ with contextlib.suppress(AttributeError):
+ telegram_classes.add(self._resolve_class(type_.__forward_arg__))
+
+ for type_hint in type_hints.values():
+ if type_hint is not None:
+ recurse_type(type_hint, False)
+
+ return list(telegram_classes)
@staticmethod
def _resolve_class(name: str) -> Union[type, None]:
@@ -579,16 +558,14 @@ def _resolve_class(name: str) -> Union[type, None]:
f"telegram.ext.{name}",
f"telegram.ext.filters.{name}",
):
- try:
- return eval(option)
# NameError will be raised if trying to eval just name and it doesn't work, e.g.
# "Name 'ApplicationBuilder' is not defined".
# AttributeError will be raised if trying to e.g. eval f"telegram.{name}" when the
# class denoted by `name` actually belongs to `telegram.ext`:
# "module 'telegram' has no attribute 'ApplicationBuilder'".
# If neither option works, this is not a PTB class.
- except (NameError, AttributeError):
- continue
+ with contextlib.suppress(NameError, AttributeError):
+ return eval(option)
return None
diff --git a/docs/auxil/kwargs_insertion.py b/docs/auxil/kwargs_insertion.py
index ffb2ada137a..e24003f1d71 100644
--- a/docs/auxil/kwargs_insertion.py
+++ b/docs/auxil/kwargs_insertion.py
@@ -1,6 +1,6 @@
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2024
+# Copyright (C) 2015-2025
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -16,7 +16,6 @@
# 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 inspect
-from typing import List
keyword_args = [
"Keyword Arguments:",
@@ -48,29 +47,29 @@
"",
]
-media_write_timeout_deprecation_methods = [
- "send_photo",
+media_write_timeout_change_methods = [
+ "add_sticker_to_set",
+ "create_new_sticker_set",
+ "send_animation",
"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_deprecation = [
+media_write_timeout_change = [
" 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."
"",
"",
- " .. deprecated:: 20.7",
- " In future versions, the default value will be changed to "
+ " .. versionchanged:: 22.0",
+ " The default value changed to "
" :attr:`~telegram.request.BaseRequest.DEFAULT_NONE`.",
"",
"",
@@ -85,7 +84,7 @@
]
-def find_insert_pos_for_kwargs(lines: List[str]) -> int:
+def find_insert_pos_for_kwargs(lines: list[str]) -> int:
"""Finds the correct position to insert the keyword arguments and returns the index."""
for idx, value in reversed(list(enumerate(lines))): # reversed since :returns: is at the end
if value.startswith("Returns"):
diff --git a/docs/auxil/link_code.py b/docs/auxil/link_code.py
index 8c20f34b4af..63dd3fad86a 100644
--- a/docs/auxil/link_code.py
+++ b/docs/auxil/link_code.py
@@ -1,6 +1,6 @@
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2024
+# Copyright (C) 2015-2025
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -21,7 +21,6 @@
"""
import subprocess
from pathlib import Path
-from typing import Dict, Tuple
from sphinx.util import logging
@@ -32,7 +31,7 @@
# must be a module-level variable so that it can be written to by the `autodoc-process-docstring`
# event handler in `sphinx_hooks.py`
-LINE_NUMBERS: Dict[str, Tuple[Path, int, int]] = {}
+LINE_NUMBERS: dict[str, tuple[Path, int, int]] = {}
def _git_branch() -> str:
diff --git a/docs/auxil/sphinx_hooks.py b/docs/auxil/sphinx_hooks.py
index 2cfbfe14049..53f3c4edea0 100644
--- a/docs/auxil/sphinx_hooks.py
+++ b/docs/auxil/sphinx_hooks.py
@@ -1,6 +1,6 @@
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2024
+# Copyright (C) 2015-2025
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -16,6 +16,7 @@
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
import collections.abc
+import contextlib
import inspect
import re
import typing
@@ -31,8 +32,8 @@
find_insert_pos_for_kwargs,
get_updates_read_timeout_addition,
keyword_args,
- media_write_timeout_deprecation,
- media_write_timeout_deprecation_methods,
+ media_write_timeout_change,
+ media_write_timeout_change_methods,
)
from docs.auxil.link_code import LINE_NUMBERS
@@ -115,9 +116,9 @@ def autodoc_process_docstring(
if (
"post.write_timeout`. Defaults to" in to_insert
- and method_name in media_write_timeout_deprecation_methods
+ and method_name in media_write_timeout_change_methods
):
- effective_insert: list[str] = media_write_timeout_deprecation
+ effective_insert: list[str] = media_write_timeout_change
elif get_updates and to_insert.lstrip().startswith("read_timeout"):
effective_insert = [to_insert, *get_updates_read_timeout_addition]
else:
@@ -153,13 +154,11 @@ def autodoc_process_docstring(
if isinstance(obj, telegram.ext.filters.BaseFilter):
obj = obj.__class__
- try:
+ with contextlib.suppress(Exception):
source_lines, start_line = inspect.getsourcelines(obj)
end_line = start_line + len(source_lines)
file = Path(inspect.getsourcefile(obj)).relative_to(FILE_ROOT)
LINE_NUMBERS[name] = (file, start_line, end_line)
- except Exception:
- pass
# Since we don't document the `__init__`, we call this manually to have it available for
# attributes -- see the note above
@@ -188,6 +187,11 @@ def autodoc_process_bases(app, name, obj, option, bases: list) -> None:
bases[idx] = ":class:`enum.IntEnum`"
continue
+ if "FloatEnum" in base:
+ bases[idx] = ":class:`enum.Enum`"
+ bases.insert(0, ":class:`float`")
+ continue
+
# Drop generics (at least for now)
if base.endswith("]"):
base = base.split("[", maxsplit=1)[0]
diff --git a/docs/auxil/tg_const_role.py b/docs/auxil/tg_const_role.py
index d4d5961ad7f..a46ebcb48f0 100644
--- a/docs/auxil/tg_const_role.py
+++ b/docs/auxil/tg_const_role.py
@@ -1,6 +1,6 @@
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2024
+# Copyright (C) 2015-2025
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -15,7 +15,7 @@
#
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
-import datetime
+import datetime as dtm
from enum import Enum
from docutils.nodes import Element
@@ -75,7 +75,7 @@ def process_link(
):
return str(value), target
if (
- isinstance(value, datetime.datetime)
+ isinstance(value, dtm.datetime)
and value == telegram.constants.ZERO_DATE
and target in ("telegram.constants.ZERO_DATE",)
):
@@ -88,7 +88,6 @@ def process_link(
refnode.rawsource,
CONSTANTS_ROLE,
)
- return title, target
except Exception as exc:
sphinx_logger.exception(
"%s:%d: WARNING: Did not convert reference %s due to an exception.",
@@ -98,3 +97,5 @@ def process_link(
exc_info=exc,
)
return title, target
+ else:
+ return title, target
diff --git a/docs/requirements-docs.txt b/docs/requirements-docs.txt
index 08fba15d32f..e207cc48175 100644
--- a/docs/requirements-docs.txt
+++ b/docs/requirements-docs.txt
@@ -1,7 +1,10 @@
-sphinx==7.3.7
-furo==2024.5.6
+chango~=0.4.0
+sphinx==8.2.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
-sphinxcontrib-mermaid==0.9.2
+sphinxcontrib-mermaid==1.0.0
sphinx-copybutton==0.5.2
-sphinx-inline-tabs==2023.4.21
\ No newline at end of file
+sphinx-inline-tabs==2023.4.21
+# Temporary. See #4387
+sphinx-build-compatibility @ git+https://github.com/readthedocs/sphinx-build-compatibility.git@58aabc5f207c6c2421f23d3578adc0b14af57047
diff --git a/docs/source/_static/style_admonitions.css b/docs/source/_static/style_admonitions.css
index 89c0d4b9e5e..4d86486afe9 100644
--- a/docs/source/_static/style_admonitions.css
+++ b/docs/source/_static/style_admonitions.css
@@ -61,5 +61,5 @@
}
.admonition.returned-in > ul, .admonition.available-in > ul, .admonition.use-in > ul, .admonition.shortcuts > ul {
max-height: 200px;
- overflow-y: scroll;
+ overflow-y: auto;
}
diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst
index 1cb32f6be91..c5be34d2b04 100644
--- a/docs/source/changelog.rst
+++ b/docs/source/changelog.rst
@@ -1 +1,9 @@
-.. include:: ../../CHANGES.rst
\ No newline at end of file
+.. _ptb-changelog:
+
+=========
+Changelog
+=========
+
+.. chango::
+
+.. include:: ../../changes/LEGACY.rst
\ No newline at end of file
diff --git a/docs/source/conf.py b/docs/source/conf.py
index 5858a79e2da..fbb8b43168e 100644
--- a/docs/source/conf.py
+++ b/docs/source/conf.py
@@ -1,3 +1,4 @@
+import os
import re
import sys
from pathlib import Path
@@ -7,12 +8,17 @@
# 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 ------------------------------------------------
# General information about the project.
project = "python-telegram-bot"
-copyright = "2015-2024, Leandro Toledo"
+copyright = "2015-2025, Leandro Toledo"
author = "Leandro Toledo"
# The version info for the project you're documenting, acts as replacement for
@@ -29,12 +35,13 @@
release = telegram.__version__
# If your documentation needs a minimal Sphinx version, state it here.
-needs_sphinx = "6.1.3"
+needs_sphinx = "8.1.3"
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
+ "chango.sphinx_ext",
"sphinx.ext.autodoc",
"sphinx.ext.napoleon",
"sphinx.ext.intersphinx",
@@ -47,6 +54,13 @@
"sphinx_search.extension",
]
+# Temporary. See #4387
+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"),
@@ -106,6 +120,11 @@
# 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
@@ -251,7 +270,14 @@
# The base URL which points to the root of the HTML documentation. It is used to indicate the
# location of document using The Canonical Link Relation. Default: ''.
-html_baseurl = "https://docs.python-telegram-bot.org"
+# Set canonical URL from the Read the Docs Domain
+html_baseurl = os.environ.get("READTHEDOCS_CANONICAL_URL", "")
+
+# Tell Jinja2 templates the build is running on Read the Docs
+html_context = {}
+if os.environ.get("READTHEDOCS", "") == "True":
+ html_context["READTHEDOCS"] = True
+
# -- Options for LaTeX output ---------------------------------------------
diff --git a/docs/source/inclusions/bot_methods.rst b/docs/source/inclusions/bot_methods.rst
index 3189de1c1d3..240c258f68f 100644
--- a/docs/source/inclusions/bot_methods.rst
+++ b/docs/source/inclusions/bot_methods.rst
@@ -25,6 +25,8 @@
- Used for sending documents
* - :meth:`~telegram.Bot.send_game`
- Used for sending a game
+ * - :meth:`~telegram.Bot.send_gift`
+ - Used for sending a gift
* - :meth:`~telegram.Bot.send_invoice`
- Used for sending an invoice
* - :meth:`~telegram.Bot.send_location`
@@ -151,6 +153,8 @@
- Used for setting a chat title
* - :meth:`~telegram.Bot.set_chat_description`
- Used for setting the description of a chat
+ * - :meth:`~telegram.Bot.set_user_emoji_status`
+ - Used for setting the users status emoji
* - :meth:`~telegram.Bot.pin_chat_message`
- Used for pinning a message
* - :meth:`~telegram.Bot.unpin_chat_message`
@@ -179,6 +183,29 @@
+.. raw:: html
+
+
+ Verification on behalf of an organization
+
+.. list-table::
+ :align: left
+ :widths: 1 4
+
+ * - :meth:`~telegram.Bot.verify_chat`
+ - Used for verifying a chat
+ * - :meth:`~telegram.Bot.verify_user`
+ - Used for verifying a user
+ * - :meth:`~telegram.Bot.remove_chat_verification`
+ - Used for removing the verification from a chat
+ * - :meth:`~telegram.Bot.remove_user_verification`
+ - Used for removing the verification from a user
+
+.. raw:: html
+
+
+
+
.. raw:: html
@@ -355,7 +382,7 @@
.. raw:: html
- Miscellaneous
+ Payments and Stars
.. list-table::
:align: left
@@ -363,18 +390,39 @@
* - :meth:`~telegram.Bot.create_invoice_link`
- Used to generate an HTTP link for an invoice
+ * - :meth:`~telegram.Bot.edit_user_star_subscription`
+ - Used for editing a user's star subscription
+ * - :meth:`~telegram.Bot.get_star_transactions`
+ - Used for obtaining the bot's Telegram Stars transactions
+ * - :meth:`~telegram.Bot.refund_star_payment`
+ - Used for refunding a payment in Telegram Stars
+
+.. raw:: html
+
+
+
+
+.. raw:: html
+
+
+ Miscellaneous
+
+.. list-table::
+ :align: left
+ :widths: 1 4
+
* - :meth:`~telegram.Bot.close`
- Used for closing server instance when switching to another local server
* - :meth:`~telegram.Bot.log_out`
- Used for logging out from cloud Bot API server
* - :meth:`~telegram.Bot.get_file`
- Used for getting basic info about a file
+ * - :meth:`~telegram.Bot.get_available_gifts`
+ - Used for getting information about gifts available for sending
* - :meth:`~telegram.Bot.get_me`
- Used for getting basic information about the bot
- * - :meth:`~telegram.Bot.get_star_transactions`
- - Used for obtaining the bot's Telegram Stars transactions
- * - :meth:`~telegram.Bot.refund_star_payment`
- - Used for refunding a payment in Telegram Stars
+ * - :meth:`~telegram.Bot.save_prepared_inline_message`
+ - Used for storing a message to be sent by a user of a Mini App
.. raw:: html
diff --git a/docs/source/stability_policy.rst b/docs/source/stability_policy.rst
index 972892185fc..621b99b540e 100644
--- a/docs/source/stability_policy.rst
+++ b/docs/source/stability_policy.rst
@@ -1,3 +1,5 @@
+.. _stability-policy:
+
Stability Policy
================
diff --git a/docs/source/telegram.affiliateinfo.rst b/docs/source/telegram.affiliateinfo.rst
new file mode 100644
index 00000000000..0b2e51863af
--- /dev/null
+++ b/docs/source/telegram.affiliateinfo.rst
@@ -0,0 +1,6 @@
+AffiliateInfo
+=============
+
+.. autoclass:: telegram.AffiliateInfo
+ :members:
+ :show-inheritance:
\ No newline at end of file
diff --git a/docs/source/telegram.at-tree.rst b/docs/source/telegram.at-tree.rst
index 8d3238a27e4..22abbfb3867 100644
--- a/docs/source/telegram.at-tree.rst
+++ b/docs/source/telegram.at-tree.rst
@@ -29,6 +29,7 @@ Available Types
telegram.chat
telegram.chatadministratorrights
telegram.chatbackground
+ telegram.copytextbutton
telegram.backgroundtype
telegram.backgroundtypefill
telegram.backgroundtypewallpaper
@@ -92,7 +93,6 @@ Available Types
telegram.inputpaidmediaphoto
telegram.inputpaidmediavideo
telegram.inputpolloption
- telegram.inputsticker
telegram.keyboardbutton
telegram.keyboardbuttonpolltype
telegram.keyboardbuttonrequestchat
@@ -120,6 +120,7 @@ Available Types
telegram.paidmediainfo
telegram.paidmediaphoto
telegram.paidmediapreview
+ telegram.paidmediapurchased
telegram.paidmediavideo
telegram.photosize
telegram.poll
@@ -130,6 +131,7 @@ Available Types
telegram.reactiontype
telegram.reactiontypecustomemoji
telegram.reactiontypeemoji
+ telegram.reactiontypepaid
telegram.replykeyboardmarkup
telegram.replykeyboardremove
telegram.replyparameters
diff --git a/docs/source/telegram.constants.rst b/docs/source/telegram.constants.rst
index 4b5edf51094..ef1e6720107 100644
--- a/docs/source/telegram.constants.rst
+++ b/docs/source/telegram.constants.rst
@@ -5,5 +5,5 @@ telegram.constants Module
:members:
:show-inheritance:
:no-undoc-members:
- :inherited-members: Enum, EnumMeta, str, int
+ :inherited-members: Enum, EnumMeta, str, int, float
:exclude-members: __format__, __new__, __repr__, __str__
diff --git a/docs/source/telegram.copytextbutton.rst b/docs/source/telegram.copytextbutton.rst
new file mode 100644
index 00000000000..7110fbf8b6b
--- /dev/null
+++ b/docs/source/telegram.copytextbutton.rst
@@ -0,0 +1,6 @@
+CopyTextButton
+==============
+
+.. autoclass:: telegram.CopyTextButton
+ :members:
+ :show-inheritance:
\ No newline at end of file
diff --git a/docs/source/telegram.ext.handlers-tree.rst b/docs/source/telegram.ext.handlers-tree.rst
index 6749cacb9dd..72e0d824c53 100644
--- a/docs/source/telegram.ext.handlers-tree.rst
+++ b/docs/source/telegram.ext.handlers-tree.rst
@@ -18,6 +18,7 @@ Handlers
telegram.ext.inlinequeryhandler
telegram.ext.messagehandler
telegram.ext.messagereactionhandler
+ telegram.ext.paidmediapurchasedhandler
telegram.ext.pollanswerhandler
telegram.ext.pollhandler
telegram.ext.precheckoutqueryhandler
diff --git a/docs/source/telegram.ext.paidmediapurchasedhandler.rst b/docs/source/telegram.ext.paidmediapurchasedhandler.rst
new file mode 100644
index 00000000000..19bfbeea31e
--- /dev/null
+++ b/docs/source/telegram.ext.paidmediapurchasedhandler.rst
@@ -0,0 +1,6 @@
+PaidMediaPurchasedHandler
+=========================
+
+.. autoclass:: telegram.ext.PaidMediaPurchasedHandler
+ :members:
+ :show-inheritance:
diff --git a/docs/source/telegram.games-tree.rst b/docs/source/telegram.games-tree.rst
index 64f399d86a9..97b961a9e85 100644
--- a/docs/source/telegram.games-tree.rst
+++ b/docs/source/telegram.games-tree.rst
@@ -1,6 +1,21 @@
+.. _games-tree:
+
Games
-----
+Your bot can offer users **HTML5 games** to play solo or to compete against each other in groups and one-on-one chats. Create games via `@BotFather `_ using the ``/newgame`` command. Please note that this kind of power requires responsibility: you will need to accept the terms for each game that your bots will be offering.
+
+* Games are a new type of content on Telegram, represented by the :class:`telegram.Game` and :class:`telegram.InlineQueryResultGame` objects.
+* Once you've created a game via `BotFather `_, you can send games to chats as regular messages using the :meth:`~telegram.Bot.sendGame` method, or use :ref:`inline mode ` with :class:`telegram.InlineQueryResultGame`.
+* If you send the game message without any buttons, it will automatically have a 'Play ``GameName``' button. When this button is pressed, your bot gets a :class:`telegram.CallbackQuery` with the ``game_short_name`` of the requested game. You provide the correct URL for this particular user and the app opens the game in the in-app browser.
+* You can manually add multiple buttons to your game message. Please note that the first button in the first row **must always** launch the game, using the field ``callback_game`` in :class:`telegram.InlineKeyboardButton`. You can add extra buttons according to taste: e.g., for a description of the rules, or to open the game's official community.
+* To make your game more attractive, you can upload a GIF animation that demonstrates the game to the users via `BotFather `_ (see `Lumberjack `_ for example).
+* A game message will also display high scores for the current chat. Use :meth:`~telegram.Bot.setGameScore` to post high scores to the chat with the game, add the :paramref:`~telegram.Bot.set_game_score.disable_edit_message` parameter to disable automatic update of the message with the current scoreboard.
+* Use :meth:`~telegram.Bot.getGameHighScores` to get data for in-game high score tables.
+* You can also add an extra sharing button for users to share their best score to different chats.
+* For examples of what can be done using this new stuff, check the `@gamebot `_ and `@gamee `_ bots.
+
+
.. toctree::
:titlesonly:
diff --git a/docs/source/telegram.gift.rst b/docs/source/telegram.gift.rst
new file mode 100644
index 00000000000..e42cb720ac2
--- /dev/null
+++ b/docs/source/telegram.gift.rst
@@ -0,0 +1,6 @@
+Gift
+====
+
+.. autoclass:: telegram.Gift
+ :members:
+ :show-inheritance:
diff --git a/docs/source/telegram.gifts.rst b/docs/source/telegram.gifts.rst
new file mode 100644
index 00000000000..649522d0dce
--- /dev/null
+++ b/docs/source/telegram.gifts.rst
@@ -0,0 +1,6 @@
+Gifts
+=====
+
+.. autoclass:: telegram.Gifts
+ :members:
+ :show-inheritance:
diff --git a/docs/source/telegram.inline-tree.rst b/docs/source/telegram.inline-tree.rst
index 7fa52a94b58..c21b3c33828 100644
--- a/docs/source/telegram.inline-tree.rst
+++ b/docs/source/telegram.inline-tree.rst
@@ -1,6 +1,14 @@
+.. _inline-tree:
+
Inline Mode
-----------
+The following methods and objects allow your bot to work in `inline mode `_.
+Please see Telegrams `Introduction to Inline bots `_ for more details.
+
+To enable this option, send the ``/setinline`` command to `@BotFather `_ and provide the placeholder text that the user will see in the input field after typing your bot's name.
+
+
.. toctree::
:titlesonly:
@@ -34,3 +42,4 @@ Inline Mode
telegram.inputvenuemessagecontent
telegram.inputcontactmessagecontent
telegram.inputinvoicemessagecontent
+ telegram.preparedinlinemessage
diff --git a/docs/source/telegram.paidmediapurchased.rst b/docs/source/telegram.paidmediapurchased.rst
new file mode 100644
index 00000000000..80568ae405c
--- /dev/null
+++ b/docs/source/telegram.paidmediapurchased.rst
@@ -0,0 +1,6 @@
+PaidMediaPurchased
+==================
+
+.. autoclass:: telegram.PaidMediaPurchased
+ :members:
+ :show-inheritance:
diff --git a/docs/source/telegram.passport-tree.rst b/docs/source/telegram.passport-tree.rst
index fb4e3b4ffde..079ce948924 100644
--- a/docs/source/telegram.passport-tree.rst
+++ b/docs/source/telegram.passport-tree.rst
@@ -1,6 +1,9 @@
Passport
--------
+Passport is a unified authorization method for services that require personal identification. Users can upload their documents once, then instantly share their data with services that require real-world ID (finance, ICOs, etc.). Please see the `manual `_ for details.
+
+
.. toctree::
:titlesonly:
diff --git a/docs/source/telegram.payments-tree.rst b/docs/source/telegram.payments-tree.rst
index 0db0ba21959..3e6f42bdc97 100644
--- a/docs/source/telegram.payments-tree.rst
+++ b/docs/source/telegram.payments-tree.rst
@@ -1,9 +1,15 @@
+.. _payments-tree:
+
Payments
--------
+Your bot can accept payments from Telegram users. Please see the `introduction to payments `_ for more details on the process and how to set up payments for your bot.
+
+
.. toctree::
:titlesonly:
+ telegram.affiliateinfo
telegram.invoice
telegram.labeledprice
telegram.orderinfo
@@ -20,7 +26,10 @@ Payments
telegram.startransactions
telegram.successfulpayment
telegram.transactionpartner
+ telegram.transactionpartneraffiliateprogram
+ telegram.transactionpartnerchat
telegram.transactionpartnerfragment
telegram.transactionpartnerother
telegram.transactionpartnertelegramads
+ telegram.transactionpartnertelegramapi
telegram.transactionpartneruser
diff --git a/docs/source/telegram.preparedinlinemessage.rst b/docs/source/telegram.preparedinlinemessage.rst
new file mode 100644
index 00000000000..2522f8c58cf
--- /dev/null
+++ b/docs/source/telegram.preparedinlinemessage.rst
@@ -0,0 +1,6 @@
+PreparedInlineMessage
+=====================
+
+.. autoclass:: telegram.PreparedInlineMessage
+ :members:
+ :show-inheritance:
\ No newline at end of file
diff --git a/docs/source/telegram.reactiontypepaid.rst b/docs/source/telegram.reactiontypepaid.rst
new file mode 100644
index 00000000000..f5035a1ba5b
--- /dev/null
+++ b/docs/source/telegram.reactiontypepaid.rst
@@ -0,0 +1,6 @@
+ReactionTypePaid
+================
+
+.. autoclass:: telegram.ReactionTypePaid
+ :members:
+ :show-inheritance:
diff --git a/docs/source/telegram.stickers-tree.rst b/docs/source/telegram.stickers-tree.rst
index 783b90ec0c7..e45dcacb56b 100644
--- a/docs/source/telegram.stickers-tree.rst
+++ b/docs/source/telegram.stickers-tree.rst
@@ -1,9 +1,14 @@
Stickers
--------
+The following methods and objects allow your bot to handle stickers and sticker sets.
+
.. toctree::
:titlesonly:
+ telegram.gift
+ telegram.gifts
+ telegram.inputsticker
telegram.maskposition
telegram.sticker
telegram.stickerset
diff --git a/docs/source/telegram.transactionpartneraffiliateprogram.rst b/docs/source/telegram.transactionpartneraffiliateprogram.rst
new file mode 100644
index 00000000000..dfcab6ec22b
--- /dev/null
+++ b/docs/source/telegram.transactionpartneraffiliateprogram.rst
@@ -0,0 +1,6 @@
+TransactionPartnerAffiliateProgram
+===================================
+
+.. autoclass:: telegram.TransactionPartnerAffiliateProgram
+ :members:
+ :show-inheritance:
\ No newline at end of file
diff --git a/docs/source/telegram.transactionpartnerchat.rst b/docs/source/telegram.transactionpartnerchat.rst
new file mode 100644
index 00000000000..d57cf128378
--- /dev/null
+++ b/docs/source/telegram.transactionpartnerchat.rst
@@ -0,0 +1,7 @@
+TransactionPartnerChat
+======================
+
+.. autoclass:: telegram.TransactionPartnerChat
+ :members:
+ :show-inheritance:
+ :inherited-members: TransactionPartner
diff --git a/docs/source/telegram.transactionpartnerfragment.rst b/docs/source/telegram.transactionpartnerfragment.rst
index 0845b4a800b..c65f9262f81 100644
--- a/docs/source/telegram.transactionpartnerfragment.rst
+++ b/docs/source/telegram.transactionpartnerfragment.rst
@@ -4,4 +4,4 @@ TransactionPartnerFragment
.. autoclass:: telegram.TransactionPartnerFragment
:members:
:show-inheritance:
- :inherited-members: TelegramObject
+ :inherited-members: TransactionPartner
diff --git a/docs/source/telegram.transactionpartnerother.rst b/docs/source/telegram.transactionpartnerother.rst
index c3ffddc7de0..b0c14f0713c 100644
--- a/docs/source/telegram.transactionpartnerother.rst
+++ b/docs/source/telegram.transactionpartnerother.rst
@@ -4,4 +4,4 @@ TransactionPartnerOther
.. autoclass:: telegram.TransactionPartnerOther
:members:
:show-inheritance:
- :inherited-members: TelegramObject
+ :inherited-members: TransactionPartner
diff --git a/docs/source/telegram.transactionpartnertelegramads.rst b/docs/source/telegram.transactionpartnertelegramads.rst
index 926b25bdcd4..ce9a52a117f 100644
--- a/docs/source/telegram.transactionpartnertelegramads.rst
+++ b/docs/source/telegram.transactionpartnertelegramads.rst
@@ -4,4 +4,4 @@ TransactionPartnerTelegramAds
.. autoclass:: telegram.TransactionPartnerTelegramAds
:members:
:show-inheritance:
- :inherited-members: TelegramObject
+ :inherited-members: TransactionPartner
diff --git a/docs/source/telegram.transactionpartnertelegramapi.rst b/docs/source/telegram.transactionpartnertelegramapi.rst
new file mode 100644
index 00000000000..9aeba6b94b8
--- /dev/null
+++ b/docs/source/telegram.transactionpartnertelegramapi.rst
@@ -0,0 +1,7 @@
+TransactionPartnerTelegramApi
+=============================
+
+.. autoclass:: telegram.TransactionPartnerTelegramApi
+ :members:
+ :show-inheritance:
+ :inherited-members: TransactionPartner
diff --git a/docs/source/telegram.transactionpartneruser.rst b/docs/source/telegram.transactionpartneruser.rst
index d2e145e1866..def37495344 100644
--- a/docs/source/telegram.transactionpartneruser.rst
+++ b/docs/source/telegram.transactionpartneruser.rst
@@ -4,4 +4,4 @@ TransactionPartnerUser
.. autoclass:: telegram.TransactionPartnerUser
:members:
:show-inheritance:
- :inherited-members: TelegramObject
+ :inherited-members: TransactionPartner
diff --git a/docs/substitutions/global.rst b/docs/substitutions/global.rst
index c4e9e493bb3..8fb9e9360d7 100644
--- a/docs/substitutions/global.rst
+++ b/docs/substitutions/global.rst
@@ -60,10 +60,12 @@
.. |removed_thumb_note| replace:: Removed the deprecated argument and attribute ``thumb``.
-.. |removed_thumb_url_note| replace:: Removed the deprecated argument and attribute ``thumb_url``.
+.. |removed_thumb_url_note| replace:: Removed the deprecated argument and attribute ``thumb_url`` which made thumbnail_url mandatory.
.. |removed_thumb_wildcard_note| replace:: Removed the deprecated arguments and attributes ``thumb_*``.
+.. |thumbnail_url_mandatory| replace:: Removal of the deprecated argument ``thumb_url`` made ``thumbnail_url`` mandatory.
+
.. |async_context_manager| replace:: Asynchronous context manager which
.. |reply_parameters| replace:: Description of the message to reply to.
@@ -91,3 +93,11 @@
.. |show_cap_above_med| replace:: :obj:`True`, if the caption must be shown above the message media.
.. |tg_stars| replace:: `Telegram Stars `__
+
+.. |allow_paid_broadcast| replace:: Pass True to allow up to :tg-const:`telegram.constants.FloodLimit.PAID_MESSAGES_PER_SECOND` messages per second, ignoring `broadcasting limits `__ for a fee of 0.1 Telegram Stars per message. The relevant Stars will be withdrawn from the bot's balance.
+
+.. |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 `__
+
+.. |time-period-input| replace:: :class:`datetime.timedelta` objects are accepted in addition to plain :obj:`int` values.
diff --git a/examples/arbitrarycallbackdatabot.py b/examples/arbitrarycallbackdatabot.py
index cf3d46fa91b..e11620c1670 100644
--- a/examples/arbitrarycallbackdatabot.py
+++ b/examples/arbitrarycallbackdatabot.py
@@ -12,7 +12,7 @@
`pip install "python-telegram-bot[callback-data]"`
"""
import logging
-from typing import List, Tuple, cast
+from typing import cast
from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Update
from telegram.ext import (
@@ -36,7 +36,7 @@
async def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Sends a message with 5 inline buttons attached."""
- number_list: List[int] = []
+ number_list: list[int] = []
await update.message.reply_text("Please choose:", reply_markup=build_keyboard(number_list))
@@ -55,7 +55,7 @@ async def clear(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
await update.effective_message.reply_text("All clear!")
-def build_keyboard(current_list: List[int]) -> InlineKeyboardMarkup:
+def build_keyboard(current_list: list[int]) -> InlineKeyboardMarkup:
"""Helper function to build the next inline keyboard."""
return InlineKeyboardMarkup.from_column(
[InlineKeyboardButton(str(i), callback_data=(i, current_list)) for i in range(1, 6)]
@@ -69,7 +69,7 @@ async def list_button(update: Update, context: ContextTypes.DEFAULT_TYPE) -> Non
# Get the data from the callback_data.
# If you're using a type checker like MyPy, you'll have to use typing.cast
# to make the checker get the expected type of the callback_data
- number, number_list = cast(Tuple[int, List[int]], query.data)
+ number, number_list = cast(tuple[int, list[int]], query.data)
# append the number to the list
number_list.append(number)
diff --git a/examples/chatmemberbot.py b/examples/chatmemberbot.py
index dd299b73acf..34dad2a8385 100644
--- a/examples/chatmemberbot.py
+++ b/examples/chatmemberbot.py
@@ -12,7 +12,7 @@
"""
import logging
-from typing import Optional, Tuple
+from typing import Optional
from telegram import Chat, ChatMember, ChatMemberUpdated, Update
from telegram.constants import ParseMode
@@ -37,7 +37,7 @@
logger = logging.getLogger(__name__)
-def extract_status_change(chat_member_update: ChatMemberUpdated) -> Optional[Tuple[bool, bool]]:
+def extract_status_change(chat_member_update: ChatMemberUpdated) -> Optional[tuple[bool, bool]]:
"""Takes a ChatMemberUpdated instance and extracts whether the 'old_chat_member' was a member
of the chat and whether the 'new_chat_member' is a member of the chat. Returns None, if
the status didn't change.
diff --git a/examples/contexttypesbot.py b/examples/contexttypesbot.py
index 9c361772cdb..b89d8ffc7d7 100644
--- a/examples/contexttypesbot.py
+++ b/examples/contexttypesbot.py
@@ -12,7 +12,7 @@
import logging
from collections import defaultdict
-from typing import DefaultDict, Optional, Set
+from typing import Optional
from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Update
from telegram.constants import ParseMode
@@ -40,7 +40,7 @@ class ChatData:
"""Custom class for chat_data. Here we store data per message."""
def __init__(self) -> None:
- self.clicks_per_message: DefaultDict[int, int] = defaultdict(int)
+ self.clicks_per_message: defaultdict[int, int] = defaultdict(int)
# The [ExtBot, dict, ChatData, dict] is for type checkers like mypy
@@ -57,7 +57,7 @@ def __init__(
self._message_id: Optional[int] = None
@property
- def bot_user_ids(self) -> Set[int]:
+ def bot_user_ids(self) -> set[int]:
"""Custom shortcut to access a value stored in the bot_data dict"""
return self.bot_data.setdefault("user_ids", set())
diff --git a/examples/conversationbot2.py b/examples/conversationbot2.py
index 6a5e54a8e5b..af29e0198e9 100644
--- a/examples/conversationbot2.py
+++ b/examples/conversationbot2.py
@@ -15,7 +15,6 @@
"""
import logging
-from typing import Dict
from telegram import ReplyKeyboardMarkup, ReplyKeyboardRemove, Update
from telegram.ext import (
@@ -46,7 +45,7 @@
markup = ReplyKeyboardMarkup(reply_keyboard, one_time_keyboard=True)
-def facts_to_str(user_data: Dict[str, str]) -> str:
+def facts_to_str(user_data: dict[str, str]) -> str:
"""Helper function for formatting the gathered user info."""
facts = [f"{key} - {value}" for key, value in user_data.items()]
return "\n".join(facts).join(["\n", "\n"])
diff --git a/examples/nestedconversationbot.py b/examples/nestedconversationbot.py
index 918f0becf24..bc940f4cd45 100644
--- a/examples/nestedconversationbot.py
+++ b/examples/nestedconversationbot.py
@@ -15,7 +15,7 @@
"""
import logging
-from typing import Any, Dict, Tuple
+from typing import Any
from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Update
from telegram.ext import (
@@ -66,7 +66,7 @@
# Helper
-def _name_switcher(level: str) -> Tuple[str, str]:
+def _name_switcher(level: str) -> tuple[str, str]:
if level == PARENTS:
return "Father", "Mother"
return "Brother", "Sister"
@@ -122,7 +122,7 @@ async def adding_self(update: Update, context: ContextTypes.DEFAULT_TYPE) -> str
async def show_data(update: Update, context: ContextTypes.DEFAULT_TYPE) -> str:
"""Pretty print gathered data."""
- def pretty_print(data: Dict[str, Any], level: str) -> str:
+ def pretty_print(data: dict[str, Any], level: str) -> str:
people = data.get(level)
if not people:
return "\nNo information yet."
@@ -371,8 +371,8 @@ def main() -> None:
entry_points=[CommandHandler("start", start)],
states={
SHOWING: [CallbackQueryHandler(start, pattern="^" + str(END) + "$")],
- SELECTING_ACTION: selection_handlers,
- SELECTING_LEVEL: selection_handlers,
+ SELECTING_ACTION: selection_handlers, # type: ignore[dict-item]
+ SELECTING_LEVEL: selection_handlers, # type: ignore[dict-item]
DESCRIBING_SELF: [description_conv],
STOPPING: [CommandHandler("start", start)],
},
diff --git a/examples/paymentbot.py b/examples/paymentbot.py
index a18ee6c2827..dfa641cccc8 100644
--- a/examples/paymentbot.py
+++ b/examples/paymentbot.py
@@ -2,7 +2,7 @@
# pylint: disable=unused-argument
# This program is dedicated to the public domain under the CC0 license.
-"""Basic example for a bot that can receive payment from user."""
+"""Basic example for a bot that can receive payments from users."""
import logging
@@ -26,44 +26,44 @@
logger = logging.getLogger(__name__)
+# Insert the token from your payment provider.
+# In order to get a provider_token see https://core.telegram.org/bots/payments#getting-a-token
PAYMENT_PROVIDER_TOKEN = "PAYMENT_PROVIDER_TOKEN"
async def start_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Displays info on how to use the bot."""
+ """Provides instructions on how to use the bot."""
msg = (
- "Use /shipping to get an invoice for shipping-payment, or /noshipping for an "
+ "Use /shipping to receive an invoice with shipping included, or /noshipping for an "
"invoice without shipping."
)
-
await update.message.reply_text(msg)
async def start_with_shipping_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Sends an invoice with shipping-payment."""
+ """Sends an invoice which triggers a shipping query."""
chat_id = update.message.chat_id
title = "Payment Example"
- description = "Payment Example using python-telegram-bot"
- # select a payload just for you to recognize its the donation from your bot
+ description = "Example of a payment process using the python-telegram-bot library."
+ # Unique payload to identify this payment request as being from your bot
payload = "Custom-Payload"
- # In order to get a provider_token see https://core.telegram.org/bots/payments#getting-a-token
+ # Set up the currency.
+ # List of supported currencies: https://core.telegram.org/bots/payments#supported-currencies
currency = "USD"
- # price in dollars
+ # Price in dollars
price = 1
- # price * 100 so as to include 2 decimal points
- # check https://core.telegram.org/bots/payments#supported-currencies for more details
+ # Convert price to cents from dollars.
prices = [LabeledPrice("Test", price * 100)]
-
- # optionally pass need_name=True, need_phone_number=True,
- # need_email=True, need_shipping_address=True, is_flexible=True
+ # Optional parameters like need_shipping_address and is_flexible trigger extra user prompts
+ # https://docs.python-telegram-bot.org/en/stable/telegram.bot.html#telegram.Bot.send_invoice
await context.bot.send_invoice(
chat_id,
title,
description,
payload,
- PAYMENT_PROVIDER_TOKEN,
currency,
prices,
+ provider_token=PAYMENT_PROVIDER_TOKEN,
need_name=True,
need_phone_number=True,
need_email=True,
@@ -75,86 +75,91 @@ async def start_with_shipping_callback(update: Update, context: ContextTypes.DEF
async def start_without_shipping_callback(
update: Update, context: ContextTypes.DEFAULT_TYPE
) -> None:
- """Sends an invoice without shipping-payment."""
+ """Sends an invoice without requiring shipping details."""
chat_id = update.message.chat_id
title = "Payment Example"
- description = "Payment Example using python-telegram-bot"
- # select a payload just for you to recognize its the donation from your bot
+ description = "Example of a payment process using the python-telegram-bot library."
+ # Unique payload to identify this payment request as being from your bot
payload = "Custom-Payload"
- # In order to get a provider_token see https://core.telegram.org/bots/payments#getting-a-token
currency = "USD"
- # price in dollars
+ # Price in dollars
price = 1
- # price * 100 so as to include 2 decimal points
+ # Convert price to cents from dollars.
prices = [LabeledPrice("Test", price * 100)]
# 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, PAYMENT_PROVIDER_TOKEN, currency, prices
+ chat_id,
+ title,
+ description,
+ payload,
+ currency,
+ prices,
+ provider_token=PAYMENT_PROVIDER_TOKEN,
)
async def shipping_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Answers the ShippingQuery with ShippingOptions"""
+ """Handles the ShippingQuery with available shipping options."""
query = update.shipping_query
- # check the payload, is this from your bot?
+ # Verify if the payload matches, ensure it's from your bot
if query.invoice_payload != "Custom-Payload":
- # answer False pre_checkout_query
+ # If not, respond with an error
await query.answer(ok=False, error_message="Something went wrong...")
return
- # First option has a single LabeledPrice
+ # Define available shipping options
+ # First option with a single price entry
options = [ShippingOption("1", "Shipping Option A", [LabeledPrice("A", 100)])]
- # second option has an array of LabeledPrice objects
+ # Second option with multiple price entries
price_list = [LabeledPrice("B1", 150), LabeledPrice("B2", 200)]
options.append(ShippingOption("2", "Shipping Option B", price_list))
await query.answer(ok=True, shipping_options=options)
-# after (optional) shipping, it's the pre-checkout
+# After (optional) shipping, process the pre-checkout step
async def precheckout_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Answers the PreQecheckoutQuery"""
+ """Responds to the PreCheckoutQuery as the final confirmation for checkout."""
query = update.pre_checkout_query
- # check the payload, is this from your bot?
+ # Verify if the payload matches, ensure it's from your bot
if query.invoice_payload != "Custom-Payload":
- # answer False pre_checkout_query
+ # If not, respond with an error
await query.answer(ok=False, error_message="Something went wrong...")
else:
await query.answer(ok=True)
-# finally, after contacting the payment provider...
+# Final callback after successful payment
async def successful_payment_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Confirms the successful payment."""
- # do something after successfully receiving payment?
- await update.message.reply_text("Thank you for your payment!")
+ """Acknowledges successful payment and thanks the user."""
+ await update.message.reply_text("Thank you for your payment.")
def main() -> None:
- """Run the bot."""
+ """Starts the bot and sets up handlers."""
# Create the Application and pass it your bot's token.
application = Application.builder().token("TOKEN").build()
- # simple start function
+ # Start command to display usage instructions
application.add_handler(CommandHandler("start", start_callback))
- # Add command handler to start the payment invoice
+ # Command handlers for starting the payment process
application.add_handler(CommandHandler("shipping", start_with_shipping_callback))
application.add_handler(CommandHandler("noshipping", start_without_shipping_callback))
- # Optional handler if your product requires shipping
+ # Handler for shipping query (if product requires shipping)
application.add_handler(ShippingQueryHandler(shipping_callback))
- # Pre-checkout handler to final check
+ # Pre-checkout handler for verifying payment details.
application.add_handler(PreCheckoutQueryHandler(precheckout_callback))
- # Success! Notify your user!
+ # Handler for successful payment. Notify the user that the payment was successful.
application.add_handler(
MessageHandler(filters.SUCCESSFUL_PAYMENT, successful_payment_callback)
)
- # Run the bot until the user presses Ctrl-C
+ # Start polling for updates until interrupted (CTRL+C)
application.run_polling(allowed_updates=Update.ALL_TYPES)
diff --git a/examples/persistentconversationbot.py b/examples/persistentconversationbot.py
index 19be96f562f..4c5322456bb 100644
--- a/examples/persistentconversationbot.py
+++ b/examples/persistentconversationbot.py
@@ -15,7 +15,6 @@
"""
import logging
-from typing import Dict
from telegram import ReplyKeyboardMarkup, ReplyKeyboardRemove, Update
from telegram.ext import (
@@ -47,7 +46,7 @@
markup = ReplyKeyboardMarkup(reply_keyboard, one_time_keyboard=True)
-def facts_to_str(user_data: Dict[str, str]) -> str:
+def facts_to_str(user_data: dict[str, str]) -> str:
"""Helper function for formatting the gathered user info."""
facts = [f"{key} - {value}" for key, value in user_data.items()]
return "\n".join(facts).join(["\n", "\n"])
diff --git a/pyproject.toml b/pyproject.toml
index 2484250c3d5..f036d57a533 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -8,9 +8,9 @@ dynamic = ["version"]
name = "python-telegram-bot"
description = "We have made you a wrapper you can't refuse"
readme = "README.rst"
-requires-python = ">=3.8"
+requires-python = ">=3.9"
license = "LGPL-3.0-only"
-license-files = { paths = ["LICENSE", "LICENSE.dual", "LICENSE.lesser"] }
+license-files = ["LICENSE", "LICENSE.dual", "LICENSE.lesser"]
authors = [
{ name = "Leandro Toledo", email = "devs@python-telegram-bot.org" }
]
@@ -31,7 +31,6 @@ classifiers = [
"Topic :: Internet",
"Programming Language :: Python",
"Programming Language :: Python :: 3",
- "Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
@@ -67,7 +66,7 @@ all = [
]
callback-data = [
# Cachetools doesn't have a strict stability policy. Let's be cautious for now.
- "cachetools~=5.3.3",
+ "cachetools>=5.3.3,<5.6.0",
]
ext = [
"python-telegram-bot[callback-data,job-queue,rate-limiter,webhooks]",
@@ -77,9 +76,7 @@ http2 = [
]
job-queue = [
# APS doesn't have a strict stability policy. Let's be cautious for now.
- "APScheduler~=3.10.4",
- # pytz is required by APS and just needs the lower bound due to #2120
- "pytz>=2018.6",
+ "APScheduler>=3.10.4,<3.12.0",
]
passport = [
"cryptography!=3.4,!=3.4.1,!=3.4.2,!=3.4.3,>=39.0.1",
@@ -87,7 +84,7 @@ passport = [
"cffi >= 1.17.0rc1; python_version > '3.12'"
]
rate-limiter = [
- "aiolimiter~=1.1.0",
+ "aiolimiter>=1.1,<1.3",
]
socks = [
"httpx[socks]",
@@ -108,6 +105,11 @@ 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
@@ -128,14 +130,21 @@ explicit-preview-rules = true # TODO: Drop this when RUF022 and RUF023 are out
ignore = ["PLR2004", "PLR0911", "PLR0912", "PLR0913", "PLR0915", "PERF203"]
select = ["E", "F", "I", "PL", "UP", "RUF", "PTH", "C4", "B", "PIE", "SIM", "RET", "RSE",
"G", "ISC", "PT", "ASYNC", "TCH", "SLOT", "PERF", "PYI", "FLY", "AIR", "RUF022",
- "RUF023", "Q", "INP", "W", "YTT", "DTZ", "ARG", "T20", "FURB"]
+ "RUF023", "Q", "INP", "W", "YTT", "DTZ", "ARG", "T20", "FURB", "DOC", "TRY",
+ "D100", "D101", "D102", "D103", "D300", "D418", "D419", "S"]
# Add "A (flake8-builtins)" after we drop pylint
[tool.ruff.lint.per-file-ignores]
"tests/*.py" = ["B018"]
-"tests/**.py" = ["RUF012", "ASYNC230", "DTZ", "ARG", "T201"]
-"docs/**.py" = ["INP001", "ARG"]
-"examples/**.py" = ["ARG"]
+"tests/**.py" = ["RUF012", "ASYNC230", "DTZ", "ARG", "T201", "ASYNC109", "D", "S", "TRY"]
+"telegram/**.py" = ["TRY003"]
+"telegram/ext/_applicationbuilder.py" = ["TRY004"]
+"telegram/ext/filters.py" = ["D102"]
+"docs/**.py" = ["INP001", "ARG", "D", "TRY003", "S"]
+"examples/**.py" = ["ARG", "D", "S105", "TRY003"]
+
+[tool.ruff.lint.pydocstyle]
+convention = "google"
# PYLINT:
[tool.pylint."messages control"]
@@ -143,7 +152,8 @@ enable = ["useless-suppression"]
disable = ["duplicate-code", "too-many-arguments", "too-many-public-methods",
"too-few-public-methods", "broad-exception-caught", "too-many-instance-attributes",
"fixme", "missing-function-docstring", "missing-class-docstring", "too-many-locals",
- "too-many-lines", "too-many-branches", "too-many-statements", "cyclic-import"
+ "too-many-lines", "too-many-branches", "too-many-statements", "cyclic-import",
+ "too-many-positional-arguments",
]
[tool.pylint.main]
@@ -174,8 +184,8 @@ markers = [
"req",
]
asyncio_mode = "auto"
-log_format = "%(funcName)s - Line %(lineno)d - %(message)s"
-# log_level = "DEBUG" # uncomment to see DEBUG logs
+log_cli_format = "%(funcName)s - Line %(lineno)d - %(message)s"
+# log_cli_level = "DEBUG" # uncomment to see DEBUG logs
# MYPY:
[tool.mypy]
@@ -185,7 +195,7 @@ disallow_untyped_defs = true
disallow_incomplete_defs = true
disallow_untyped_decorators = true
show_error_codes = true
-python_version = "3.8"
+python_version = "3.9"
# For some files, it's easier to just disable strict-optional all together instead of
# cluttering the code with `# type: ignore`s or stuff like
diff --git a/requirements-unit-tests.txt b/requirements-unit-tests.txt
index eb69f9d9283..f90d2950f60 100644
--- a/requirements-unit-tests.txt
+++ b/requirements-unit-tests.txt
@@ -4,7 +4,7 @@
build
# For the test suite
-pytest==8.2.2
+pytest==8.3.5
# needed because pytest doesn't come with native support for coroutines as tests
pytest-asyncio==0.21.2
@@ -16,4 +16,8 @@ pytest-xdist==3.6.1
flaky>=3.8.1
# used in test_official for parsing tg docs
-beautifulsoup4
\ No newline at end of file
+beautifulsoup4
+
+# For testing with timezones. Might not be needed on all systems, but to ensure that unit tests
+# run correctly on all systems, we include it here.
+tzdata
\ No newline at end of file
diff --git a/telegram/__init__.py b/telegram/__init__.py
index 5b52bf85c40..fe2fce247ea 100644
--- a/telegram/__init__.py
+++ b/telegram/__init__.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2024
+# Copyright (C) 2015-2025
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -20,6 +20,7 @@
__author__ = "devs@python-telegram-bot.org"
__all__ = (
+ "AffiliateInfo",
"Animation",
"Audio",
"BackgroundFill",
@@ -81,6 +82,7 @@
"ChatShared",
"ChosenInlineResult",
"Contact",
+ "CopyTextButton",
"Credentials",
"DataCredentials",
"Dice",
@@ -100,6 +102,8 @@
"GameHighScore",
"GeneralForumTopicHidden",
"GeneralForumTopicUnhidden",
+ "Gift",
+ "Gifts",
"Giveaway",
"GiveawayCompleted",
"GiveawayCreated",
@@ -180,6 +184,7 @@
"PaidMediaInfo",
"PaidMediaPhoto",
"PaidMediaPreview",
+ "PaidMediaPurchased",
"PaidMediaVideo",
"PassportData",
"PassportElementError",
@@ -199,11 +204,13 @@
"PollAnswer",
"PollOption",
"PreCheckoutQuery",
+ "PreparedInlineMessage",
"ProximityAlertTriggered",
"ReactionCount",
"ReactionType",
"ReactionTypeCustomEmoji",
"ReactionTypeEmoji",
+ "ReactionTypePaid",
"RefundedPayment",
"ReplyKeyboardMarkup",
"ReplyKeyboardRemove",
@@ -230,9 +237,12 @@
"TelegramObject",
"TextQuote",
"TransactionPartner",
+ "TransactionPartnerAffiliateProgram",
+ "TransactionPartnerChat",
"TransactionPartnerFragment",
"TransactionPartnerOther",
"TransactionPartnerTelegramAds",
+ "TransactionPartnerTelegramApi",
"TransactionPartnerUser",
"Update",
"User",
@@ -262,6 +272,18 @@
"warnings",
)
+from telegram._payment.stars.startransactions import StarTransaction, StarTransactions
+from telegram._payment.stars.transactionpartner import (
+ TransactionPartner,
+ TransactionPartnerAffiliateProgram,
+ TransactionPartnerChat,
+ TransactionPartnerFragment,
+ TransactionPartnerOther,
+ TransactionPartnerTelegramAds,
+ TransactionPartnerTelegramApi,
+ TransactionPartnerUser,
+)
+
from . import _version, constants, error, helpers, request, warnings
from ._birthdate import Birthdate
from ._bot import Bot
@@ -328,6 +350,7 @@
from ._chatmemberupdated import ChatMemberUpdated
from ._chatpermissions import ChatPermissions
from ._choseninlineresult import ChosenInlineResult
+from ._copytextbutton import CopyTextButton
from ._dice import Dice
from ._files.animation import Animation
from ._files.audio import Audio
@@ -368,6 +391,7 @@
from ._games.callbackgame import CallbackGame
from ._games.game import Game
from ._games.gamehighscore import GameHighScore
+from ._gifts import Gift, Gifts
from ._giveaway import Giveaway, GiveawayCompleted, GiveawayCreated, GiveawayWinners
from ._inline.inlinekeyboardbutton import InlineKeyboardButton
from ._inline.inlinekeyboardmarkup import InlineKeyboardMarkup
@@ -400,6 +424,7 @@
from ._inline.inputmessagecontent import InputMessageContent
from ._inline.inputtextmessagecontent import InputTextMessageContent
from ._inline.inputvenuemessagecontent import InputVenueMessageContent
+from ._inline.preparedinlinemessage import PreparedInlineMessage
from ._keyboardbutton import KeyboardButton
from ._keyboardbuttonpolltype import KeyboardButtonPollType
from ._keyboardbuttonrequest import KeyboardButtonRequestChat, KeyboardButtonRequestUsers
@@ -418,7 +443,14 @@
MessageOriginUser,
)
from ._messagereactionupdated import MessageReactionCountUpdated, MessageReactionUpdated
-from ._paidmedia import PaidMedia, PaidMediaInfo, PaidMediaPhoto, PaidMediaPreview, PaidMediaVideo
+from ._paidmedia import (
+ PaidMedia,
+ PaidMediaInfo,
+ PaidMediaPhoto,
+ PaidMediaPreview,
+ PaidMediaPurchased,
+ PaidMediaVideo,
+)
from ._passport.credentials import (
Credentials,
DataCredentials,
@@ -451,23 +483,23 @@
from ._payment.shippingaddress import ShippingAddress
from ._payment.shippingoption import ShippingOption
from ._payment.shippingquery import ShippingQuery
-from ._payment.stars import (
+from ._payment.stars.affiliateinfo import AffiliateInfo
+from ._payment.stars.revenuewithdrawalstate import (
RevenueWithdrawalState,
RevenueWithdrawalStateFailed,
RevenueWithdrawalStatePending,
RevenueWithdrawalStateSucceeded,
- StarTransaction,
- StarTransactions,
- TransactionPartner,
- TransactionPartnerFragment,
- TransactionPartnerOther,
- TransactionPartnerTelegramAds,
- TransactionPartnerUser,
)
from ._payment.successfulpayment import SuccessfulPayment
from ._poll import InputPollOption, Poll, PollAnswer, PollOption
from ._proximityalerttriggered import ProximityAlertTriggered
-from ._reaction import ReactionCount, ReactionType, ReactionTypeCustomEmoji, ReactionTypeEmoji
+from ._reaction import (
+ ReactionCount,
+ ReactionType,
+ ReactionTypeCustomEmoji,
+ ReactionTypeEmoji,
+ ReactionTypePaid,
+)
from ._reply import ExternalReplyInfo, ReplyParameters, TextQuote
from ._replykeyboardmarkup import ReplyKeyboardMarkup
from ._replykeyboardremove import ReplyKeyboardRemove
diff --git a/telegram/__main__.py b/telegram/__main__.py
index 2491a330ac6..7d291b2ae1e 100644
--- a/telegram/__main__.py
+++ b/telegram/__main__.py
@@ -1,7 +1,7 @@
# !/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2024
+# Copyright (C) 2015-2025
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -17,7 +17,7 @@
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
# pylint: disable=missing-module-docstring
-# ruff: noqa: T201
+# ruff: noqa: T201, D100, S603, S607
import subprocess
import sys
from typing import Optional
diff --git a/telegram/_birthdate.py b/telegram/_birthdate.py
index 06caf67d5ec..643af05fc7d 100644
--- a/telegram/_birthdate.py
+++ b/telegram/_birthdate.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2024
+# Copyright (C) 2015-2025
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -17,7 +17,7 @@
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains an object that represents a Telegram Birthday."""
-from datetime import date
+import datetime as dtm
from typing import Optional
from telegram._telegramobject import TelegramObject
@@ -70,7 +70,7 @@ def __init__(
self._freeze()
- def to_date(self, year: Optional[int] = None) -> date:
+ def to_date(self, year: Optional[int] = None) -> dtm.date:
"""Return the birthdate as a date object.
.. versionchanged:: 21.2
@@ -89,4 +89,4 @@ def to_date(self, year: Optional[int] = None) -> date:
"The `year` argument is required if the `year` attribute was not present."
)
- return date(year or self.year, self.month, self.day) # type: ignore[arg-type]
+ return dtm.date(year or self.year, self.month, self.day) # type: ignore[arg-type]
diff --git a/telegram/_bot.py b/telegram/_bot.py
index 4d8a778cc63..56a0d08a538 100644
--- a/telegram/_bot.py
+++ b/telegram/_bot.py
@@ -2,7 +2,7 @@
# pylint: disable=too-many-arguments
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2024
+# Copyright (C) 2015-2025
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -18,25 +18,20 @@
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains an object that represents a Telegram Bot."""
+
import asyncio
import contextlib
import copy
+import datetime as dtm
import pickle
-from datetime import datetime
+from collections.abc import Sequence
from types import TracebackType
from typing import (
TYPE_CHECKING,
Any,
- AsyncContextManager,
Callable,
- Dict,
- List,
NoReturn,
Optional,
- Sequence,
- Set,
- Tuple,
- Type,
TypeVar,
Union,
cast,
@@ -80,11 +75,13 @@
from telegram._files.voice import Voice
from telegram._forumtopic import ForumTopic
from telegram._games.gamehighscore import GameHighScore
+from telegram._gifts import Gift, Gifts
from telegram._inline.inlinequeryresultsbutton import InlineQueryResultsButton
+from telegram._inline.preparedinlinemessage import PreparedInlineMessage
from telegram._menubutton import MenuButton
from telegram._message import Message
from telegram._messageid import MessageId
-from telegram._payment.stars import StarTransactions
+from telegram._payment.stars.startransactions import StarTransactions
from telegram._poll import InputPollOption, Poll
from telegram._reaction import ReactionType, ReactionTypeCustomEmoji, ReactionTypeEmoji
from telegram._reply import ReplyParameters
@@ -99,7 +96,15 @@
from telegram._utils.logging import get_logger
from telegram._utils.repr import build_repr_with_selected_attrs
from telegram._utils.strings import to_camel_case
-from telegram._utils.types import CorrectOptionID, FileInput, JSONDict, ODVInput, ReplyMarkup
+from telegram._utils.types import (
+ BaseUrl,
+ CorrectOptionID,
+ FileInput,
+ JSONDict,
+ ODVInput,
+ ReplyMarkup,
+ TimePeriod,
+)
from telegram._utils.warnings import warn
from telegram._webhookinfo import WebhookInfo
from telegram.constants import InlineQueryLimit, ReactionEmoji
@@ -107,7 +112,7 @@
from telegram.request import BaseRequest, RequestData
from telegram.request._httpxrequest import HTTPXRequest
from telegram.request._requestparameter import RequestParameter
-from telegram.warnings import PTBDeprecationWarning, PTBUserWarning
+from telegram.warnings import PTBUserWarning
if TYPE_CHECKING:
from telegram import (
@@ -129,7 +134,36 @@
BT = TypeVar("BT", bound="Bot")
-class Bot(TelegramObject, AsyncContextManager["Bot"]):
+# Even though we document only {token} as supported insertion, we are a bit more flexible
+# internally and support additional variants. At the very least, we don't want the insertion
+# to be case sensitive.
+_SUPPORTED_INSERTIONS = {"token", "TOKEN", "bot_token", "BOT_TOKEN", "bot-token", "BOT-TOKEN"}
+_INSERTION_STRINGS = {f"{{{insertion}}}" for insertion in _SUPPORTED_INSERTIONS}
+
+
+class _TokenDict(dict):
+ __slots__ = ("token",)
+
+ # small helper to make .format_map work without knowing which exact insertion name is used
+ def __init__(self, token: str):
+ self.token = token
+ super().__init__()
+
+ def __missing__(self, key: str) -> str:
+ if key in _SUPPORTED_INSERTIONS:
+ return self.token
+ raise KeyError(f"Base URL string contains unsupported insertion: {key}")
+
+
+def _parse_base_url(value: BaseUrl, token: str) -> str:
+ if callable(value):
+ return value(token)
+ if any(insertion in value for insertion in _INSERTION_STRINGS):
+ return value.format_map(_TokenDict(token))
+ return value + token
+
+
+class Bot(TelegramObject, contextlib.AbstractAsyncContextManager["Bot"]):
"""This object represents a Telegram Bot.
Instances of this class can be used as asyncio context managers, where
@@ -196,8 +230,40 @@ class Bot(TelegramObject, AsyncContextManager["Bot"]):
Args:
token (:obj:`str`): Bot's unique authentication token.
- base_url (:obj:`str`, optional): Telegram Bot API service URL.
+ base_url (:obj:`str` | Callable[[:obj:`str`], :obj:`str`], optional): Telegram Bot API
+ service URL. If the string contains ``{token}``, it will be replaced with the bot's
+ token. If a callable is passed, it will be called with the bot's token as the only
+ argument and must return the base URL. Otherwise, the token will be appended to the
+ string. Defaults to ``"https://api.telegram.org/bot"``.
+
+ Tip:
+ Customizing the base URL can be used to run a bot against
+ :wiki:`Local Bot API Server ` or using Telegrams
+ `test environment \
+ `_.
+
+ Example:
+ ``"https://api.telegram.org/bot{token}/test"``
+
+ .. versionchanged:: 21.11
+ 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
+ token. If a callable is passed, it will be called with the bot's token as the only
+ argument and must return the base URL. Otherwise, the token will be appended to the
+ string. Defaults to ``"https://api.telegram.org/bot"``.
+
+ Tip:
+ Customizing the base URL can be used to run a bot against
+ :wiki:`Local Bot API Server ` or using Telegrams
+ `test environment \
+ `_.
+
+ Example:
+ ``"https://api.telegram.org/file/bot{token}/test"``
+
+ .. versionchanged:: 21.11
+ 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
*except* for :meth:`get_updates`. If not passed, an instance of
@@ -242,8 +308,8 @@ class Bot(TelegramObject, AsyncContextManager["Bot"]):
def __init__(
self,
token: str,
- base_url: str = "https://api.telegram.org/bot",
- base_file_url: str = "https://api.telegram.org/file/bot",
+ base_url: BaseUrl = "https://api.telegram.org/bot",
+ base_file_url: BaseUrl = "https://api.telegram.org/file/bot",
request: Optional[BaseRequest] = None,
get_updates_request: Optional[BaseRequest] = None,
private_key: Optional[bytes] = None,
@@ -255,19 +321,22 @@ def __init__(
raise InvalidToken("You must pass the token you received from https://t.me/Botfather!")
self._token: str = token
- self._base_url: str = base_url + self._token
- self._base_file_url: str = base_file_url + self._token
+ self._base_url: str = _parse_base_url(base_url, self._token)
+ self._base_file_url: str = _parse_base_url(base_file_url, self._token)
+ self._LOGGER.debug("Set Bot API URL: %s", self._base_url)
+ self._LOGGER.debug("Set Bot API File URL: %s", self._base_file_url)
+
self._local_mode: bool = local_mode
self._bot_user: Optional[User] = None
self._private_key: Optional[bytes] = None
self._initialized: bool = False
- self._request: Tuple[BaseRequest, BaseRequest] = (
+ self._request: tuple[BaseRequest, BaseRequest] = (
HTTPXRequest() if get_updates_request is None else get_updates_request,
HTTPXRequest() if request is None else request,
)
- # this section is about issuing a warning when using HTTP/2 and connect to a self hosted
+ # this section is about issuing a warning when using HTTP/2 and connect to a self-hosted
# bot api instance, which currently only supports HTTP/1.1. Checking if a custom base url
# is set is the best way to do that.
@@ -276,14 +345,14 @@ def __init__(
if (
isinstance(self._request[0], HTTPXRequest)
and self._request[0].http_version == "2"
- and not base_url.startswith("https://api.telegram.org/bot")
+ and not self.base_url.startswith("https://api.telegram.org/bot")
):
warning_string = "get_updates_request"
if (
isinstance(self._request[1], HTTPXRequest)
and self._request[1].http_version == "2"
- and not base_url.startswith("https://api.telegram.org/bot")
+ and not self.base_url.startswith("https://api.telegram.org/bot")
):
if warning_string:
warning_string += " and request"
@@ -324,14 +393,14 @@ async def __aenter__(self: BT) -> BT:
"""
try:
await self.initialize()
- return self
- except Exception as exc:
+ except Exception:
await self.shutdown()
- raise exc
+ raise
+ return self
async def __aexit__(
self,
- exc_type: Optional[Type[BaseException]],
+ exc_type: Optional[type[BaseException]],
exc_val: Optional[BaseException],
exc_tb: Optional[TracebackType],
) -> None:
@@ -351,7 +420,7 @@ def __reduce__(self) -> NoReturn:
"""
raise pickle.PicklingError("Bot objects cannot be pickled!")
- def __deepcopy__(self, memodict: Dict[int, object]) -> NoReturn:
+ def __deepcopy__(self, memodict: dict[int, object]) -> NoReturn:
"""Customizes how :func:`copy.deepcopy` processes objects of this type. Bots can not
be deepcopied and this method will always raise an exception.
@@ -527,7 +596,7 @@ def name(self) -> str:
def _warn(
cls,
message: Union[str, PTBUserWarning],
- category: Type[Warning] = PTBUserWarning,
+ category: type[Warning] = PTBUserWarning,
stacklevel: int = 0,
) -> None:
"""Convenience method to issue a warning. This method is here mostly to make it easier
@@ -538,7 +607,7 @@ def _warn(
def _parse_file_input(
self,
file_input: Union[FileInput, "TelegramObject"],
- tg_type: Optional[Type["TelegramObject"]] = None,
+ tg_type: Optional[type["TelegramObject"]] = None,
filename: Optional[str] = None,
attach: bool = False,
) -> Union[str, "InputFile", Any]:
@@ -550,7 +619,7 @@ def _parse_file_input(
local_mode=self._local_mode,
)
- def _insert_defaults(self, data: Dict[str, object]) -> None:
+ def _insert_defaults(self, data: dict[str, object]) -> None:
"""This method is here to make ext.Defaults work. Because we need to be able to tell
e.g. `send_message(chat_id, text)` from `send_message(chat_id, text, parse_mode=None)`, the
default values for `parse_mode` etc are not `None` but `DEFAULT_NONE`. While this *could*
@@ -604,7 +673,7 @@ async def _post(
pool_timeout: ODVInput[float] = DEFAULT_NONE,
api_kwargs: Optional[JSONDict] = None,
) -> Any:
- # We know that the return type is Union[bool, JSONDict, List[JSONDict]], but it's hard
+ # We know that the return type is Union[bool, JSONDict, list[JSONDict]], but it's hard
# to tell mypy which methods expects which of these return values and `Any` saves us a
# lot of `type: ignore` comments
if data is None:
@@ -637,7 +706,7 @@ async def _do_post(
write_timeout: ODVInput[float] = DEFAULT_NONE,
connect_timeout: ODVInput[float] = DEFAULT_NONE,
pool_timeout: ODVInput[float] = DEFAULT_NONE,
- ) -> Union[bool, JSONDict, List[JSONDict]]:
+ ) -> Union[bool, JSONDict, list[JSONDict]]:
# This also converts datetimes into timestamps.
# We don't do this earlier so that _insert_defaults (see above) has a chance to convert
# to the default timezone in case this is called by ExtBot
@@ -677,6 +746,7 @@ async def _send_message(
reply_parameters: Optional["ReplyParameters"] = None,
business_connection_id: Optional[str] = None,
message_effect_id: Optional[str] = None,
+ allow_paid_broadcast: Optional[bool] = None,
*,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
reply_to_message_id: Optional[int] = None,
@@ -713,33 +783,22 @@ async def _send_message(
allow_sending_without_reply=allow_sending_without_reply,
)
- data["disable_notification"] = disable_notification
- data["protect_content"] = protect_content
- data["parse_mode"] = parse_mode
-
- if reply_parameters is not None:
- data["reply_parameters"] = reply_parameters
-
- if link_preview_options is not None:
- data["link_preview_options"] = link_preview_options
-
- if reply_markup is not None:
- data["reply_markup"] = reply_markup
-
- if message_thread_id is not None:
- data["message_thread_id"] = message_thread_id
-
- if caption is not None:
- data["caption"] = caption
-
- if caption_entities is not None:
- data["caption_entities"] = caption_entities
-
- if business_connection_id is not None:
- data["business_connection_id"] = business_connection_id
-
- if message_effect_id is not None:
- data["message_effect_id"] = message_effect_id
+ data.update(
+ {
+ "allow_paid_broadcast": allow_paid_broadcast,
+ "business_connection_id": business_connection_id,
+ "caption": caption,
+ "caption_entities": caption_entities,
+ "disable_notification": disable_notification,
+ "link_preview_options": link_preview_options,
+ "message_thread_id": message_thread_id,
+ "message_effect_id": message_effect_id,
+ "parse_mode": parse_mode,
+ "protect_content": protect_content,
+ "reply_markup": reply_markup,
+ "reply_parameters": reply_parameters,
+ }
+ )
result = await self._post(
endpoint,
@@ -770,13 +829,15 @@ async def initialize(self) -> None:
return
await asyncio.gather(self._request[0].initialize(), self._request[1].initialize())
+ # this needs to be set before we call get_me, since this can trigger an error in the
+ # request backend, which would then NOT lead to a proper shutdown if this flag isn't set
+ self._initialized = True
# Since the bot is to be initialized only once, we can also use it for
# verifying the token passed and raising an exception if it's invalid.
try:
await self.get_me()
except InvalidToken as exc:
raise InvalidToken(f"The token `{self._token}` was rejected by the server.") from exc
- self._initialized = True
async def shutdown(self) -> None:
"""Stop & clear resources used by this class. Currently just calls
@@ -797,7 +858,7 @@ async def do_api_request(
self,
endpoint: str,
api_kwargs: Optional[JSONDict] = None,
- return_type: Optional[Type[TelegramObject]] = None,
+ return_type: Optional[type[TelegramObject]] = None,
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -914,7 +975,7 @@ async def get_me(
api_kwargs=api_kwargs,
)
self._bot_user = User.de_json(result, self)
- return self._bot_user # type: ignore[return-value]
+ return self._bot_user
async def send_message(
self,
@@ -930,6 +991,7 @@ async def send_message(
reply_parameters: Optional["ReplyParameters"] = None,
business_connection_id: Optional[str] = None,
message_effect_id: Optional[str] = None,
+ allow_paid_broadcast: Optional[bool] = None,
*,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
reply_to_message_id: Optional[int] = None,
@@ -981,6 +1043,9 @@ async def send_message(
message_effect_id (:obj:`str`, optional): |message_effect_id|
.. versionadded:: 21.3
+ allow_paid_broadcast (:obj:`bool`, optional): |allow_paid_broadcast|
+
+ .. versionadded:: 21.7
Keyword Args:
allow_sending_without_reply (:obj:`bool`, optional): |allow_sending_without_reply|
@@ -1039,6 +1104,7 @@ async def send_message(
link_preview_options=link_preview_options,
reply_parameters=reply_parameters,
message_effect_id=message_effect_id,
+ allow_paid_broadcast=allow_paid_broadcast,
read_timeout=read_timeout,
write_timeout=write_timeout,
connect_timeout=connect_timeout,
@@ -1154,6 +1220,7 @@ async def forward_message(
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,
@@ -1178,6 +1245,10 @@ async def forward_message(
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|
@@ -1196,6 +1267,7 @@ async def forward_message(
"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(
@@ -1225,7 +1297,7 @@ async def forward_messages(
connect_timeout: ODVInput[float] = DEFAULT_NONE,
pool_timeout: ODVInput[float] = DEFAULT_NONE,
api_kwargs: Optional[JSONDict] = None,
- ) -> Tuple[MessageId, ...]:
+ ) -> tuple[MessageId, ...]:
"""
Use this method to forward messages of any kind. If some of the specified messages can't be
found or forwarded, they are skipped. Service messages and messages with protected content
@@ -1247,7 +1319,7 @@ async def forward_messages(
message_thread_id (:obj:`int`, optional): |message_thread_id_arg|
Returns:
- Tuple[:class:`telegram.Message`]: On success, a tuple of ``MessageId`` of sent messages
+ tuple[:class:`telegram.Message`]: On success, a tuple of ``MessageId`` of sent messages
is returned.
Raises:
@@ -1288,6 +1360,7 @@ async def send_photo(
reply_parameters: Optional["ReplyParameters"] = None,
business_connection_id: Optional[str] = None,
message_effect_id: Optional[str] = None,
+ allow_paid_broadcast: Optional[bool] = None,
show_caption_above_media: Optional[bool] = None,
*,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
@@ -1305,8 +1378,8 @@ async def send_photo(
Args:
chat_id (:obj:`int` | :obj:`str`): |chat_id_channel|
- photo (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | \
- :class:`telegram.PhotoSize`): Photo to send.
+ photo (:obj:`str` | :term:`file object` | :class:`~telegram.InputFile` | :obj:`bytes` \
+ | :class:`pathlib.Path` | :class:`telegram.PhotoSize`): Photo to send.
|fileinput|
Lastly you can pass an existing :class:`telegram.PhotoSize` object to send.
@@ -1355,6 +1428,9 @@ async def send_photo(
message_effect_id (:obj:`str`, optional): |message_effect_id|
.. versionadded:: 21.3
+ allow_paid_broadcast (:obj:`bool`, optional): |allow_paid_broadcast|
+
+ .. versionadded:: 21.7
show_caption_above_media (:obj:`bool`, optional): Pass |show_cap_above_med|
.. versionadded:: 21.3
@@ -1418,13 +1494,14 @@ async def send_photo(
api_kwargs=api_kwargs,
business_connection_id=business_connection_id,
message_effect_id=message_effect_id,
+ allow_paid_broadcast=allow_paid_broadcast,
)
async def send_audio(
self,
chat_id: Union[int, str],
audio: Union[FileInput, "Audio"],
- duration: Optional[int] = None,
+ duration: Optional[TimePeriod] = None,
performer: Optional[str] = None,
title: Optional[str] = None,
caption: Optional[str] = None,
@@ -1438,6 +1515,7 @@ async def send_audio(
reply_parameters: Optional["ReplyParameters"] = None,
business_connection_id: Optional[str] = None,
message_effect_id: Optional[str] = None,
+ allow_paid_broadcast: Optional[bool] = None,
*,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
reply_to_message_id: Optional[int] = None,
@@ -1465,9 +1543,9 @@ async def send_audio(
Args:
chat_id (:obj:`int` | :obj:`str`): |chat_id_channel|
- audio (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | \
- :class:`telegram.Audio`): Audio file to send.
- |fileinput|
+ audio (:obj:`str` | :term:`file object` | :class:`~telegram.InputFile` | \
+ :obj:`bytes` | :class:`pathlib.Path` | :class:`telegram.Audio`): Audio file to
+ send. |fileinput|
Lastly you can pass an existing :class:`telegram.Audio` object to send.
.. versionchanged:: 13.2
@@ -1485,7 +1563,11 @@ async def send_audio(
.. versionchanged:: 20.0
|sequenceargs|
- duration (:obj:`int`, optional): Duration of sent audio in seconds.
+ duration (:obj:`int` | :class:`datetime.timedelta`, optional): Duration of sent audio
+ in seconds.
+
+ .. versionchanged:: 21.11
+ |time-period-input|
performer (:obj:`str`, optional): Performer.
title (:obj:`str`, optional): Track name.
disable_notification (:obj:`bool`, optional): |disable_notification|
@@ -1513,6 +1595,9 @@ async def send_audio(
message_effect_id (:obj:`str`, optional): |message_effect_id|
.. versionadded:: 21.3
+ allow_paid_broadcast (:obj:`bool`, optional): |allow_paid_broadcast|
+
+ .. versionadded:: 21.7
Keyword Args:
allow_sending_without_reply (:obj:`bool`, optional): |allow_sending_without_reply|
@@ -1575,6 +1660,7 @@ async def send_audio(
api_kwargs=api_kwargs,
business_connection_id=business_connection_id,
message_effect_id=message_effect_id,
+ allow_paid_broadcast=allow_paid_broadcast,
)
async def send_document(
@@ -1593,6 +1679,7 @@ async def send_document(
reply_parameters: Optional["ReplyParameters"] = None,
business_connection_id: Optional[str] = None,
message_effect_id: Optional[str] = None,
+ allow_paid_broadcast: Optional[bool] = None,
*,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
reply_to_message_id: Optional[int] = None,
@@ -1617,8 +1704,8 @@ async def send_document(
Args:
chat_id (:obj:`int` | :obj:`str`): |chat_id_channel|
- document (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | \
- :class:`telegram.Document`): File to send.
+ document (:obj:`str` | :term:`file object` | :class:`~telegram.InputFile` | \
+ :obj:`bytes` | :class:`pathlib.Path` | :class:`telegram.Document`): File to send.
|fileinput|
Lastly you can pass an existing :class:`telegram.Document` object to send.
@@ -1667,6 +1754,9 @@ async def send_document(
message_effect_id (:obj:`str`, optional): |message_effect_id|
.. versionadded:: 21.3
+ allow_paid_broadcast (:obj:`bool`, optional): |allow_paid_broadcast|
+
+ .. versionadded:: 21.7
Keyword Args:
allow_sending_without_reply (:obj:`bool`, optional): |allow_sending_without_reply|
@@ -1725,6 +1815,7 @@ async def send_document(
api_kwargs=api_kwargs,
business_connection_id=business_connection_id,
message_effect_id=message_effect_id,
+ allow_paid_broadcast=allow_paid_broadcast,
)
async def send_sticker(
@@ -1739,6 +1830,7 @@ async def send_sticker(
reply_parameters: Optional["ReplyParameters"] = None,
business_connection_id: Optional[str] = None,
message_effect_id: Optional[str] = None,
+ allow_paid_broadcast: Optional[bool] = None,
*,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
reply_to_message_id: Optional[int] = None,
@@ -1755,8 +1847,8 @@ async def send_sticker(
Args:
chat_id (:obj:`int` | :obj:`str`): |chat_id_channel|
- sticker (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | \
- :class:`telegram.Sticker`): Sticker to send.
+ sticker (:obj:`str` | :term:`file object` | :class:`~telegram.InputFile` | \
+ :obj:`bytes` | :class:`pathlib.Path` | :class:`telegram.Sticker`): Sticker to send.
|fileinput| Video stickers can only be sent by a ``file_id``. Video and animated
stickers can't be sent via an HTTP URL.
@@ -1793,6 +1885,9 @@ async def send_sticker(
message_effect_id (:obj:`str`, optional): |message_effect_id|
.. versionadded:: 21.3
+ allow_paid_broadcast (:obj:`bool`, optional): |allow_paid_broadcast|
+
+ .. versionadded:: 21.7
Keyword Args:
allow_sending_without_reply (:obj:`bool`, optional): |allow_sending_without_reply|
@@ -1843,13 +1938,14 @@ async def send_sticker(
api_kwargs=api_kwargs,
business_connection_id=business_connection_id,
message_effect_id=message_effect_id,
+ allow_paid_broadcast=allow_paid_broadcast,
)
async def send_video(
self,
chat_id: Union[int, str],
video: Union[FileInput, "Video"],
- duration: Optional[int] = None,
+ duration: Optional[TimePeriod] = None,
caption: Optional[str] = None,
disable_notification: ODVInput[bool] = DEFAULT_NONE,
reply_markup: Optional[ReplyMarkup] = None,
@@ -1865,7 +1961,10 @@ async def send_video(
reply_parameters: Optional["ReplyParameters"] = None,
business_connection_id: Optional[str] = None,
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,
@@ -1895,8 +1994,8 @@ async def send_video(
Args:
chat_id (:obj:`int` | :obj:`str`): |chat_id_channel|
- video (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | \
- :class:`telegram.Video`): Video file to send.
+ video (:obj:`str` | :term:`file object` | :class:`~telegram.InputFile` | :obj:`bytes` \
+ | :class:`pathlib.Path` | :class:`telegram.Video`): Video file to send.
|fileinput|
Lastly you can pass an existing :class:`telegram.Video` object to send.
@@ -1906,9 +2005,20 @@ async def send_video(
.. versionchanged:: 20.0
File paths as input is also accepted for bots *not* running in
:paramref:`~telegram.Bot.local_mode`.
- duration (:obj:`int`, optional): Duration of sent video in seconds.
+ duration (:obj:`int` | :class:`datetime.timedelta`, optional): Duration of sent video
+ in seconds.
+
+ .. versionchanged:: 21.11
+ |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.
@@ -1949,6 +2059,9 @@ async def send_video(
message_effect_id (:obj:`str`, optional): |message_effect_id|
.. versionadded:: 21.3
+ allow_paid_broadcast (:obj:`bool`, optional): |allow_paid_broadcast|
+
+ .. versionadded:: 21.7
show_caption_above_media (:obj:`bool`, optional): Pass |show_cap_above_med|
.. versionadded:: 21.3
@@ -1992,6 +2105,8 @@ async def send_video(
"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,
@@ -2017,13 +2132,14 @@ async def send_video(
api_kwargs=api_kwargs,
business_connection_id=business_connection_id,
message_effect_id=message_effect_id,
+ allow_paid_broadcast=allow_paid_broadcast,
)
async def send_video_note(
self,
chat_id: Union[int, str],
video_note: Union[FileInput, "VideoNote"],
- duration: Optional[int] = None,
+ duration: Optional[TimePeriod] = None,
length: Optional[int] = None,
disable_notification: ODVInput[bool] = DEFAULT_NONE,
reply_markup: Optional[ReplyMarkup] = None,
@@ -2033,6 +2149,7 @@ async def send_video_note(
reply_parameters: Optional["ReplyParameters"] = None,
business_connection_id: Optional[str] = None,
message_effect_id: Optional[str] = None,
+ allow_paid_broadcast: Optional[bool] = None,
*,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
reply_to_message_id: Optional[int] = None,
@@ -2059,8 +2176,9 @@ async def send_video_note(
Args:
chat_id (:obj:`int` | :obj:`str`): |chat_id_channel|
- video_note (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | \
- :class:`telegram.VideoNote`): Video note to send.
+ video_note (:obj:`str` | :term:`file object` | :class:`~telegram.InputFile` | \
+ :obj:`bytes` | :class:`pathlib.Path` | :class:`telegram.VideoNote`): Video note
+ to send.
Pass a file_id as String to send a video note that exists on the Telegram
servers (recommended) or upload a new video using multipart/form-data.
|uploadinput|
@@ -2073,7 +2191,11 @@ async def send_video_note(
.. versionchanged:: 20.0
File paths as input is also accepted for bots *not* running in
:paramref:`~telegram.Bot.local_mode`.
- duration (:obj:`int`, optional): Duration of sent video in seconds.
+ duration (:obj:`int` | :class:`datetime.timedelta`, optional): Duration of sent video
+ in seconds.
+
+ .. versionchanged:: 21.11
+ |time-period-input|
length (:obj:`int`, optional): Video width and height, i.e. diameter of the video
message.
disable_notification (:obj:`bool`, optional): |disable_notification|
@@ -2101,6 +2223,9 @@ async def send_video_note(
message_effect_id (:obj:`str`, optional): |message_effect_id|
.. versionadded:: 21.3
+ allow_paid_broadcast (:obj:`bool`, optional): |allow_paid_broadcast|
+
+ .. versionadded:: 21.7
Keyword Args:
allow_sending_without_reply (:obj:`bool`, optional): |allow_sending_without_reply|
@@ -2159,13 +2284,14 @@ async def send_video_note(
api_kwargs=api_kwargs,
business_connection_id=business_connection_id,
message_effect_id=message_effect_id,
+ allow_paid_broadcast=allow_paid_broadcast,
)
async def send_animation(
self,
chat_id: Union[int, str],
animation: Union[FileInput, "Animation"],
- duration: Optional[int] = None,
+ duration: Optional[TimePeriod] = None,
width: Optional[int] = None,
height: Optional[int] = None,
caption: Optional[str] = None,
@@ -2180,6 +2306,7 @@ async def send_animation(
reply_parameters: Optional["ReplyParameters"] = None,
business_connection_id: Optional[str] = None,
message_effect_id: Optional[str] = None,
+ allow_paid_broadcast: Optional[bool] = None,
show_caption_above_media: Optional[bool] = None,
*,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
@@ -2209,14 +2336,18 @@ async def send_animation(
Args:
chat_id (:obj:`int` | :obj:`str`): |chat_id_channel|
- animation (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | \
- :class:`telegram.Animation`): Animation to send.
- |fileinput|
+ animation (:obj:`str` | :term:`file object` | :class:`~telegram.InputFile` | \
+ :obj:`bytes` | :class:`pathlib.Path` | :class:`telegram.Animation`): Animation to
+ send. |fileinput|
Lastly you can pass an existing :class:`telegram.Animation` object to send.
.. versionchanged:: 13.2
Accept :obj:`bytes` as input.
- duration (:obj:`int`, optional): Duration of sent animation in seconds.
+ duration (:obj:`int` | :class:`datetime.timedelta`, optional): Duration of sent
+ animation in seconds.
+
+ .. versionchanged:: 21.11
+ |time-period-input|
width (:obj:`int`, optional): Animation width.
height (:obj:`int`, optional): Animation height.
caption (:obj:`str`, optional): Animation caption (may also be used when resending
@@ -2258,6 +2389,9 @@ async def send_animation(
message_effect_id (:obj:`str`, optional): |message_effect_id|
.. versionadded:: 21.3
+ allow_paid_broadcast (:obj:`bool`, optional): |allow_paid_broadcast|
+
+ .. versionadded:: 21.7
show_caption_above_media (:obj:`bool`, optional): Pass |show_cap_above_med|
.. versionadded:: 21.3
@@ -2325,13 +2459,14 @@ async def send_animation(
api_kwargs=api_kwargs,
business_connection_id=business_connection_id,
message_effect_id=message_effect_id,
+ allow_paid_broadcast=allow_paid_broadcast,
)
async def send_voice(
self,
chat_id: Union[int, str],
voice: Union[FileInput, "Voice"],
- duration: Optional[int] = None,
+ duration: Optional[TimePeriod] = None,
caption: Optional[str] = None,
disable_notification: ODVInput[bool] = DEFAULT_NONE,
reply_markup: Optional[ReplyMarkup] = None,
@@ -2342,6 +2477,7 @@ async def send_voice(
reply_parameters: Optional["ReplyParameters"] = None,
business_connection_id: Optional[str] = None,
message_effect_id: Optional[str] = None,
+ allow_paid_broadcast: Optional[bool] = None,
*,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
reply_to_message_id: Optional[int] = None,
@@ -2371,8 +2507,8 @@ async def send_voice(
Args:
chat_id (:obj:`int` | :obj:`str`): |chat_id_channel|
- voice (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | \
- :class:`telegram.Voice`): Voice file to send.
+ voice (:obj:`str` | :term:`file object` | :class:`~telegram.InputFile` | :obj:`bytes` \
+ | :class:`pathlib.Path` | :class:`telegram.Voice`): Voice file to send.
|fileinput|
Lastly you can pass an existing :class:`telegram.Voice` object to send.
@@ -2391,7 +2527,11 @@ async def send_voice(
.. versionchanged:: 20.0
|sequenceargs|
- duration (:obj:`int`, optional): Duration of the voice message in seconds.
+ duration (:obj:`int` | :class:`datetime.timedelta`, optional): Duration of the voice
+ message in seconds.
+
+ .. versionchanged:: 21.11
+ |time-period-input|
disable_notification (:obj:`bool`, optional): |disable_notification|
protect_content (:obj:`bool`, optional): |protect_content|
@@ -2413,6 +2553,9 @@ async def send_voice(
message_effect_id (:obj:`str`, optional): |message_effect_id|
.. versionadded:: 21.3
+ allow_paid_broadcast (:obj:`bool`, optional): |allow_paid_broadcast|
+
+ .. versionadded:: 21.7
Keyword Args:
allow_sending_without_reply (:obj:`bool`, optional): |allow_sending_without_reply|
@@ -2472,6 +2615,7 @@ async def send_voice(
api_kwargs=api_kwargs,
business_connection_id=business_connection_id,
message_effect_id=message_effect_id,
+ allow_paid_broadcast=allow_paid_broadcast,
)
async def send_media_group(
@@ -2486,6 +2630,7 @@ async def send_media_group(
reply_parameters: Optional["ReplyParameters"] = None,
business_connection_id: Optional[str] = None,
message_effect_id: Optional[str] = None,
+ allow_paid_broadcast: Optional[bool] = None,
*,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
reply_to_message_id: Optional[int] = None,
@@ -2497,7 +2642,7 @@ async def send_media_group(
caption: Optional[str] = None,
parse_mode: ODVInput[str] = DEFAULT_NONE,
caption_entities: Optional[Sequence["MessageEntity"]] = None,
- ) -> Tuple[Message, ...]:
+ ) -> tuple[Message, ...]:
"""Use this method to send a group of photos, videos, documents or audios as an album.
Documents and audio files can be only grouped in an album with messages of the same type.
@@ -2539,6 +2684,9 @@ async def send_media_group(
message_effect_id (:obj:`str`, optional): |message_effect_id|
.. versionadded:: 21.3
+ allow_paid_broadcast (:obj:`bool`, optional): |allow_paid_broadcast|
+
+ .. versionadded:: 21.7
Keyword Args:
allow_sending_without_reply (:obj:`bool`, optional): |allow_sending_without_reply|
@@ -2579,7 +2727,7 @@ async def send_media_group(
.. versionadded:: 20.0
Returns:
- Tuple[:class:`telegram.Message`]: An array of the sent Messages.
+ tuple[:class:`telegram.Message`]: An array of the sent Messages.
Raises:
:class:`telegram.error.TelegramError`
@@ -2633,6 +2781,7 @@ async def send_media_group(
"reply_parameters": reply_parameters,
"business_connection_id": business_connection_id,
"message_effect_id": message_effect_id,
+ "allow_paid_broadcast": allow_paid_broadcast,
}
result = await self._post(
@@ -2654,7 +2803,7 @@ async def send_location(
longitude: Optional[float] = None,
disable_notification: ODVInput[bool] = DEFAULT_NONE,
reply_markup: Optional[ReplyMarkup] = None,
- live_period: Optional[int] = None,
+ live_period: Optional[TimePeriod] = None,
horizontal_accuracy: Optional[float] = None,
heading: Optional[int] = None,
proximity_alert_radius: Optional[int] = None,
@@ -2663,6 +2812,7 @@ async def send_location(
reply_parameters: Optional["ReplyParameters"] = None,
business_connection_id: Optional[str] = None,
message_effect_id: Optional[str] = None,
+ allow_paid_broadcast: Optional[bool] = None,
*,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
reply_to_message_id: Optional[int] = None,
@@ -2686,12 +2836,16 @@ async def send_location(
horizontal_accuracy (:obj:`int`, optional): The radius of uncertainty for the location,
measured in meters;
0-:tg-const:`telegram.constants.LocationLimit.HORIZONTAL_ACCURACY`.
- live_period (:obj:`int`, optional): Period in seconds for which the location will be
+ live_period (:obj:`int` | :class:`datetime.timedelta`, optional): Period in seconds for
+ which the location will be
updated, should be between
:tg-const:`telegram.constants.LocationLimit.MIN_LIVE_PERIOD` and
:tg-const:`telegram.constants.LocationLimit.MAX_LIVE_PERIOD`, or
:tg-const:`telegram.constants.LocationLimit.LIVE_PERIOD_FOREVER` for live
locations that can be edited indefinitely.
+
+ .. versionchanged:: 21.11
+ |time-period-input|
heading (:obj:`int`, optional): For live locations, a direction in which the user is
moving, in degrees. Must be between
:tg-const:`telegram.constants.LocationLimit.MIN_HEADING` and
@@ -2722,6 +2876,9 @@ async def send_location(
message_effect_id (:obj:`str`, optional): |message_effect_id|
.. versionadded:: 21.3
+ allow_paid_broadcast (:obj:`bool`, optional): |allow_paid_broadcast|
+
+ .. versionadded:: 21.7
Keyword Args:
allow_sending_without_reply (:obj:`bool`, optional): |allow_sending_without_reply|
@@ -2792,6 +2949,7 @@ async def send_location(
api_kwargs=api_kwargs,
business_connection_id=business_connection_id,
message_effect_id=message_effect_id,
+ allow_paid_broadcast=allow_paid_broadcast,
)
async def edit_message_live_location(
@@ -2805,7 +2963,7 @@ async def edit_message_live_location(
horizontal_accuracy: Optional[float] = None,
heading: Optional[int] = None,
proximity_alert_radius: Optional[int] = None,
- live_period: Optional[int] = None,
+ live_period: Optional[TimePeriod] = None,
business_connection_id: Optional[str] = None,
*,
location: Optional[Location] = None,
@@ -2845,7 +3003,8 @@ async def edit_message_live_location(
if specified.
reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): An object for a new
inline keyboard.
- live_period (:obj:`int`, optional): New period in seconds during which the location
+ live_period (:obj:`int` | :class:`datetime.timedelta`, optional): New period in seconds
+ during which the location
can be updated, starting from the message send date. If
:tg-const:`telegram.constants.LocationLimit.LIVE_PERIOD_FOREVER` is specified,
then the location can be updated forever. Otherwise, the new value must not exceed
@@ -2854,6 +3013,9 @@ async def edit_message_live_location(
remains unchanged
.. versionadded:: 21.2.
+
+ .. versionchanged:: 21.11
+ |time-period-input|
business_connection_id (:obj:`str`, optional): |business_id_str_edit|
.. versionadded:: 21.4
@@ -2974,6 +3136,7 @@ async def send_venue(
reply_parameters: Optional["ReplyParameters"] = None,
business_connection_id: Optional[str] = None,
message_effect_id: Optional[str] = None,
+ allow_paid_broadcast: Optional[bool] = None,
*,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
reply_to_message_id: Optional[int] = None,
@@ -3029,6 +3192,9 @@ async def send_venue(
message_effect_id (:obj:`str`, optional): |message_effect_id|
.. versionadded:: 21.3
+ allow_paid_broadcast (:obj:`bool`, optional): |allow_paid_broadcast|
+
+ .. versionadded:: 21.7
Keyword Args:
allow_sending_without_reply (:obj:`bool`, optional): |allow_sending_without_reply|
@@ -3110,6 +3276,7 @@ async def send_venue(
api_kwargs=api_kwargs,
business_connection_id=business_connection_id,
message_effect_id=message_effect_id,
+ allow_paid_broadcast=allow_paid_broadcast,
)
async def send_contact(
@@ -3126,6 +3293,7 @@ async def send_contact(
reply_parameters: Optional["ReplyParameters"] = None,
business_connection_id: Optional[str] = None,
message_effect_id: Optional[str] = None,
+ allow_paid_broadcast: Optional[bool] = None,
*,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
reply_to_message_id: Optional[int] = None,
@@ -3171,6 +3339,9 @@ async def send_contact(
message_effect_id (:obj:`str`, optional): |message_effect_id|
.. versionadded:: 21.3
+ allow_paid_broadcast (:obj:`bool`, optional): |allow_paid_broadcast|
+
+ .. versionadded:: 21.7
Keyword Args:
allow_sending_without_reply (:obj:`bool`, optional): |allow_sending_without_reply|
@@ -3243,6 +3414,7 @@ async def send_contact(
api_kwargs=api_kwargs,
business_connection_id=business_connection_id,
message_effect_id=message_effect_id,
+ allow_paid_broadcast=allow_paid_broadcast,
)
async def send_game(
@@ -3256,6 +3428,7 @@ async def send_game(
reply_parameters: Optional["ReplyParameters"] = None,
business_connection_id: Optional[str] = None,
message_effect_id: Optional[str] = None,
+ allow_paid_broadcast: Optional[bool] = None,
*,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
reply_to_message_id: Optional[int] = None,
@@ -3291,6 +3464,9 @@ async def send_game(
message_effect_id (:obj:`str`, optional): |message_effect_id|
.. versionadded:: 21.3
+ allow_paid_broadcast (:obj:`bool`, optional): |allow_paid_broadcast|
+
+ .. versionadded:: 21.7
Keyword Args:
allow_sending_without_reply (:obj:`bool`, optional): |allow_sending_without_reply|
@@ -3338,6 +3514,7 @@ async def send_game(
api_kwargs=api_kwargs,
business_connection_id=business_connection_id,
message_effect_id=message_effect_id,
+ allow_paid_broadcast=allow_paid_broadcast,
)
async def send_chat_action(
@@ -3401,7 +3578,7 @@ def _effective_inline_results(
],
next_offset: Optional[str] = None,
current_offset: Optional[str] = None,
- ) -> Tuple[Sequence["InlineQueryResult"], Optional[str]]:
+ ) -> tuple[Sequence["InlineQueryResult"], Optional[str]]:
"""
Builds the effective results from the results input.
We make this a stand-alone method so tg.ext.ExtBot can wrap it.
@@ -3494,7 +3671,7 @@ async def answer_inline_query(
results: Union[
Sequence["InlineQueryResult"], Callable[[int], Optional[Sequence["InlineQueryResult"]]]
],
- cache_time: Optional[int] = None,
+ cache_time: Optional[TimePeriod] = None,
is_personal: Optional[bool] = None,
next_offset: Optional[str] = None,
button: Optional[InlineQueryResultsButton] = None,
@@ -3524,14 +3701,18 @@ async def answer_inline_query(
Args:
inline_query_id (:obj:`str`): Unique identifier for the answered query.
- results (List[:class:`telegram.InlineQueryResult`] | Callable): A list of results for
+ results (list[:class:`telegram.InlineQueryResult`] | Callable): A list of results for
the inline query. In case :paramref:`current_offset` is passed,
:paramref:`results` may also be
a callable that accepts the current page index starting from 0. It must return
either a list of :class:`telegram.InlineQueryResult` instances or :obj:`None` if
there are no more results.
- cache_time (:obj:`int`, optional): The maximum amount of time in seconds that the
+ cache_time (:obj:`int` | :class:`datetime.timedelta`, optional): The maximum amount of
+ time in seconds that the
result of the inline query may be cached on the server. Defaults to ``300``.
+
+ .. versionchanged:: 21.11
+ |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,
results may be returned to any user who sends the same query.
@@ -3585,6 +3766,65 @@ async def answer_inline_query(
api_kwargs=api_kwargs,
)
+ async def save_prepared_inline_message(
+ self,
+ user_id: int,
+ result: "InlineQueryResult",
+ allow_user_chats: Optional[bool] = None,
+ allow_bot_chats: Optional[bool] = None,
+ allow_group_chats: Optional[bool] = None,
+ allow_channel_chats: Optional[bool] = None,
+ *,
+ read_timeout: ODVInput[float] = DEFAULT_NONE,
+ write_timeout: ODVInput[float] = DEFAULT_NONE,
+ connect_timeout: ODVInput[float] = DEFAULT_NONE,
+ pool_timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: Optional[JSONDict] = None,
+ ) -> PreparedInlineMessage:
+ """Stores a message that can be sent by a user of a Mini App.
+
+ .. versionadded:: 21.8
+
+ Args:
+ user_id (:obj:`int`): Unique identifier of the target user that can use the prepared
+ message.
+ result (:class:`telegram.InlineQueryResult`): The result to store.
+ allow_user_chats (:obj:`bool`, optional): Pass :obj:`True` if the message can be sent
+ to private chats with users
+ allow_bot_chats (:obj:`bool`, optional): Pass :obj:`True` if the message can be sent
+ to private chats with bots
+ allow_group_chats (:obj:`bool`, optional): Pass :obj:`True` if the message can be sent
+ to group and supergroup chats
+ allow_channel_chats (:obj:`bool`, optional): Pass :obj:`True` if the message can be
+ sent to channels
+
+ Returns:
+ :class:`telegram.PreparedInlineMessage`: On success, the prepared message is returned.
+
+ Raises:
+ :class:`telegram.error.TelegramError`
+ """
+ data: JSONDict = {
+ "user_id": user_id,
+ "result": result,
+ "allow_user_chats": allow_user_chats,
+ "allow_bot_chats": allow_bot_chats,
+ "allow_group_chats": allow_group_chats,
+ "allow_channel_chats": allow_channel_chats,
+ }
+ return PreparedInlineMessage.de_json(
+ await self._post(
+ "savePreparedInlineMessage",
+ data,
+ read_timeout=read_timeout,
+ write_timeout=write_timeout,
+ connect_timeout=connect_timeout,
+ pool_timeout=pool_timeout,
+ api_kwargs=api_kwargs,
+ ),
+ self,
+ )
+
async def get_user_profile_photos(
self,
user_id: int,
@@ -3627,7 +3867,7 @@ async def get_user_profile_photos(
api_kwargs=api_kwargs,
)
- return UserProfilePhotos.de_json(result, self) # type: ignore[return-value]
+ return UserProfilePhotos.de_json(result, self)
async def get_file(
self,
@@ -3692,13 +3932,13 @@ async def get_file(
if file_path and not is_local_file(file_path):
result["file_path"] = f"{self._base_file_url}/{file_path}"
- return File.de_json(result, self) # type: ignore[return-value]
+ return File.de_json(result, self)
async def ban_chat_member(
self,
chat_id: Union[str, int],
user_id: int,
- until_date: Optional[Union[int, datetime]] = None,
+ until_date: Optional[Union[int, dtm.datetime]] = None,
revoke_messages: Optional[bool] = None,
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -3723,9 +3963,7 @@ async def ban_chat_member(
be unbanned, unix time. If user is banned for more than 366 days or less than 30
seconds from the current time they are considered to be banned forever. Applied
for supergroups and channels only.
- 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.
+ |tz-naive-dtms|
revoke_messages (:obj:`bool`, optional): Pass :obj:`True` to delete all messages from
the chat for the user that is being removed. If :obj:`False`, the user will be able
to see messages in the group that were sent before the user was removed.
@@ -3890,7 +4128,7 @@ async def answer_callback_query(
text: Optional[str] = None,
show_alert: Optional[bool] = None,
url: Optional[str] = None,
- cache_time: Optional[int] = None,
+ cache_time: Optional[TimePeriod] = None,
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -3921,9 +4159,13 @@ async def answer_callback_query(
opens your game - note that this will only work if the query comes from a callback
game button. Otherwise, you may use links like t.me/your_bot?start=XXXX that open
your bot with a parameter.
- cache_time (:obj:`int`, optional): The maximum amount of time in seconds that the
+ cache_time (:obj:`int` | :class:`datetime.timedelta`, optional): The maximum amount of
+ time in seconds that the
result of the callback query may be cached client-side. Defaults to 0.
+ .. versionchanged:: 21.11
+ |time-period-input|
+
Returns:
:obj:`bool` On success, :obj:`True` is returned.
@@ -4152,7 +4394,8 @@ async def edit_message_media(
api_kwargs: Optional[JSONDict] = None,
) -> Union[Message, bool]:
"""
- Use this method to edit animation, audio, document, photo, or video messages. If a message
+ Use this method to edit animation, audio, document, photo, or video messages, or to add
+ media to text messages. If a message
is part of a message album, then it can be edited only to an audio for audio albums, only
to a document for document albums and to a photo or a video otherwise. When an inline
message is edited, a new file can't be uploaded; use a previously uploaded file via its
@@ -4278,7 +4521,7 @@ async def get_updates(
connect_timeout: ODVInput[float] = DEFAULT_NONE,
pool_timeout: ODVInput[float] = DEFAULT_NONE,
api_kwargs: Optional[JSONDict] = None,
- ) -> Tuple[Update, ...]:
+ ) -> tuple[Update, ...]:
"""Use this method to receive incoming updates using long polling.
Note:
@@ -4323,7 +4566,7 @@ async def get_updates(
|sequenceargs|
Returns:
- Tuple[:class:`telegram.Update`]
+ tuple[:class:`telegram.Update`]
Raises:
:class:`telegram.error.TelegramError`
@@ -4340,19 +4583,7 @@ async def get_updates(
if not isinstance(read_timeout, DefaultValue):
arg_read_timeout: float = read_timeout or 0
else:
- 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,
- )
+ arg_read_timeout = self._request[0].read_timeout or 0
# Ideally we'd use an aggressive read timeout for the polling. However,
# * Short polling should return within 2 seconds.
@@ -4360,7 +4591,7 @@ async def get_updates(
# waiting for the server to return and there's no way of knowing the connection had been
# dropped in real time.
result = cast(
- List[JSONDict],
+ list[JSONDict],
await self._post(
"getUpdates",
data,
@@ -4386,7 +4617,7 @@ async def get_updates(
self._LOGGER.critical(
"Error while parsing updates! Received data was %r", result, exc_info=exc
)
- raise exc
+ raise
async def set_webhook(
self,
@@ -4407,8 +4638,11 @@ async def set_webhook(
"""
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,
- Telegram will give up after a reasonable amount of attempts.
+ specified url, containing An Update. In case of an unsuccessful request
+ (a request with response
+ `HTTP status code `_different
+ from ``2XY``),
+ Telegram will repeat the request and 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
@@ -4426,18 +4660,6 @@ async def set_webhook(
If you're having any trouble setting up webhooks, please check out this `guide to
Webhooks`_.
- Note:
- 1. You will not be able to receive updates using :meth:`get_updates` for long as an
- outgoing webhook is set up.
- 2. To use a self-signed certificate, you need to upload your public key certificate
- using certificate parameter. Please upload as InputFile, sending a String will not
- work.
- 3. Ports currently supported for Webhooks:
- :attr:`telegram.constants.SUPPORTED_WEBHOOK_PORTS`.
-
- If you're having any trouble setting up webhooks, please check out this `guide to
- Webhooks`_.
-
.. seealso:: :meth:`telegram.ext.Application.run_webhook`,
:meth:`telegram.ext.Updater.start_webhook`
@@ -4625,7 +4847,7 @@ async def get_chat(
api_kwargs=api_kwargs,
)
- return ChatFullInfo.de_json(result, self) # type: ignore[return-value]
+ return ChatFullInfo.de_json(result, self)
async def get_chat_administrators(
self,
@@ -4636,7 +4858,7 @@ async def get_chat_administrators(
connect_timeout: ODVInput[float] = DEFAULT_NONE,
pool_timeout: ODVInput[float] = DEFAULT_NONE,
api_kwargs: Optional[JSONDict] = None,
- ) -> Tuple[ChatMember, ...]:
+ ) -> tuple[ChatMember, ...]:
"""
Use this method to get a list of administrators in a chat.
@@ -4647,7 +4869,7 @@ async def get_chat_administrators(
chat_id (:obj:`int` | :obj:`str`): |chat_id_channel|
Returns:
- Tuple[:class:`telegram.ChatMember`]: On success, returns a tuple of ``ChatMember``
+ tuple[:class:`telegram.ChatMember`]: On success, returns a tuple of ``ChatMember``
objects that contains information about all chat administrators except
other bots. If the chat is a group or a supergroup and no administrators were
appointed, only the creator will be returned.
@@ -4738,7 +4960,7 @@ async def get_chat_member(
pool_timeout=pool_timeout,
api_kwargs=api_kwargs,
)
- return ChatMember.de_json(result, self) # type: ignore[return-value]
+ return ChatMember.de_json(result, self)
async def set_chat_sticker_set(
self,
@@ -4833,7 +5055,7 @@ async def get_webhook_info(
pool_timeout=pool_timeout,
api_kwargs=api_kwargs,
)
- return WebhookInfo.de_json(result, self) # type: ignore[return-value]
+ return WebhookInfo.de_json(result, self)
async def set_game_score(
self,
@@ -4911,7 +5133,7 @@ async def get_game_high_scores(
connect_timeout: ODVInput[float] = DEFAULT_NONE,
pool_timeout: ODVInput[float] = DEFAULT_NONE,
api_kwargs: Optional[JSONDict] = None,
- ) -> Tuple[GameHighScore, ...]:
+ ) -> tuple[GameHighScore, ...]:
"""
Use this method to get data for high score tables. Will return the score of the specified
user and several of their neighbors in a game.
@@ -4934,7 +5156,7 @@ async def get_game_high_scores(
:paramref:`message_id` are not specified. Identifier of the inline message.
Returns:
- Tuple[:class:`telegram.GameHighScore`]
+ tuple[:class:`telegram.GameHighScore`]
Raises:
:class:`telegram.error.TelegramError`
@@ -4965,9 +5187,9 @@ async def send_invoice(
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,
@@ -4989,6 +5211,7 @@ async def send_invoice(
message_thread_id: Optional[int] = None,
reply_parameters: Optional["ReplyParameters"] = None,
message_effect_id: Optional[str] = None,
+ allow_paid_broadcast: Optional[bool] = None,
*,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
reply_to_message_id: Optional[int] = None,
@@ -5018,14 +5241,14 @@ async def send_invoice(
payload (:obj:`str`): Bot-defined invoice payload.
:tg-const:`telegram.Invoice.MIN_PAYLOAD_LENGTH`-
:tg-const:`telegram.Invoice.MAX_PAYLOAD_LENGTH` bytes. This will not be
- displayed to the user, use for your internal processes.
- provider_token (:obj:`str`): Payments provider token, obtained via
+ displayed to the user, use it for your internal processes.
+ provider_token (:obj:`str`, optional): Payments provider token, obtained via
`@BotFather `_. Pass an empty string for payments in
|tg_stars|.
- .. 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.
+ .. versionchanged:: 21.11
+ Bot API 7.4 made this parameter is optional and this is now reflected in the
+ function signature.
currency (:obj:`str`): Three-letter ISO 4217 currency code, see `more on currencies
`_. Pass ``XTR`` for
@@ -5106,6 +5329,9 @@ async def send_invoice(
message_effect_id (:obj:`str`, optional): |message_effect_id|
.. versionadded:: 21.3
+ allow_paid_broadcast (:obj:`bool`, optional): |allow_paid_broadcast|
+
+ .. versionadded:: 21.7
Keyword Args:
allow_sending_without_reply (:obj:`bool`, optional): |allow_sending_without_reply|
@@ -5175,6 +5401,7 @@ async def send_invoice(
pool_timeout=pool_timeout,
api_kwargs=api_kwargs,
message_effect_id=message_effect_id,
+ allow_paid_broadcast=allow_paid_broadcast,
)
async def answer_shipping_query(
@@ -5335,14 +5562,14 @@ async def answer_web_app_query(
api_kwargs=api_kwargs,
)
- return SentWebAppMessage.de_json(api_result, self) # type: ignore[return-value]
+ return SentWebAppMessage.de_json(api_result, self)
async def restrict_chat_member(
self,
chat_id: Union[str, int],
user_id: int,
permissions: ChatPermissions,
- until_date: Optional[Union[int, datetime]] = None,
+ until_date: Optional[Union[int, dtm.datetime]] = None,
use_independent_chat_permissions: Optional[bool] = None,
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -5365,9 +5592,7 @@ async def restrict_chat_member(
will be lifted for the user, unix time. If user is restricted for more than 366
days or less than 30 seconds from the current time, they are considered to be
restricted forever.
- 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.
+ |tz-naive-dtms|
permissions (:class:`telegram.ChatPermissions`): An object for new user
permissions.
use_independent_chat_permissions (:obj:`bool`, optional): Pass :obj:`True` if chat
@@ -5681,7 +5906,7 @@ async def export_chat_invite_link(
async def create_chat_invite_link(
self,
chat_id: Union[str, int],
- expire_date: Optional[Union[int, datetime]] = None,
+ expire_date: Optional[Union[int, dtm.datetime]] = None,
member_limit: Optional[int] = None,
name: Optional[str] = None,
creates_join_request: Optional[bool] = None,
@@ -5711,9 +5936,7 @@ async def create_chat_invite_link(
chat_id (:obj:`int` | :obj:`str`): |chat_id_channel|
expire_date (:obj:`int` | :obj:`datetime.datetime`, optional): Date when the link will
expire. Integer input will be interpreted as Unix timestamp.
- For timezone naive :obj:`datetime.datetime` objects, the default timezone of the
- bot will be used, which is UTC unless :attr:`telegram.ext.Defaults.tzinfo` is
- used.
+ |tz-naive-dtms|
member_limit (:obj:`int`, optional): Maximum number of users that can be members of
the chat simultaneously after joining the chat via this invite link;
:tg-const:`telegram.constants.ChatInviteLinkLimit.MIN_MEMBER_LIMIT`-
@@ -5753,13 +5976,13 @@ async def create_chat_invite_link(
api_kwargs=api_kwargs,
)
- return ChatInviteLink.de_json(result, self) # type: ignore[return-value]
+ return ChatInviteLink.de_json(result, self)
async def edit_chat_invite_link(
self,
chat_id: Union[str, int],
invite_link: Union[str, "ChatInviteLink"],
- expire_date: Optional[Union[int, datetime]] = None,
+ expire_date: Optional[Union[int, dtm.datetime]] = None,
member_limit: Optional[int] = None,
name: Optional[str] = None,
creates_join_request: Optional[bool] = None,
@@ -5784,15 +6007,13 @@ async def edit_chat_invite_link(
Args:
chat_id (:obj:`int` | :obj:`str`): |chat_id_channel|
- invite_link (:obj:`str` | :obj:`telegram.ChatInviteLink`): The invite link to edit.
+ invite_link (:obj:`str` | :class:`telegram.ChatInviteLink`): The invite link to edit.
.. versionchanged:: 20.0
- Now also accepts :obj:`telegram.ChatInviteLink` instances.
+ Now also accepts :class:`telegram.ChatInviteLink` instances.
expire_date (:obj:`int` | :obj:`datetime.datetime`, optional): Date when the link will
expire.
- 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.
+ |tz-naive-dtms|
member_limit (:obj:`int`, optional): Maximum number of users that can be members of
the chat simultaneously after joining the chat via this invite link;
:tg-const:`telegram.constants.ChatInviteLinkLimit.MIN_MEMBER_LIMIT`-
@@ -5834,7 +6055,7 @@ async def edit_chat_invite_link(
api_kwargs=api_kwargs,
)
- return ChatInviteLink.de_json(result, self) # type: ignore[return-value]
+ return ChatInviteLink.de_json(result, self)
async def revoke_chat_invite_link(
self,
@@ -5856,10 +6077,10 @@ async def revoke_chat_invite_link(
Args:
chat_id (:obj:`int` | :obj:`str`): |chat_id_channel|
- invite_link (:obj:`str` | :obj:`telegram.ChatInviteLink`): The invite link to revoke.
+ invite_link (:obj:`str` | :class:`telegram.ChatInviteLink`): The invite link to revoke.
.. versionchanged:: 20.0
- Now also accepts :obj:`telegram.ChatInviteLink` instances.
+ Now also accepts :class:`telegram.ChatInviteLink` instances.
Returns:
:class:`telegram.ChatInviteLink`
@@ -5881,7 +6102,7 @@ async def revoke_chat_invite_link(
api_kwargs=api_kwargs,
)
- return ChatInviteLink.de_json(result, self) # type: ignore[return-value]
+ return ChatInviteLink.de_json(result, self)
async def approve_chat_join_request(
self,
@@ -6126,11 +6347,62 @@ async def set_chat_description(
api_kwargs=api_kwargs,
)
+ async def set_user_emoji_status(
+ self,
+ user_id: int,
+ emoji_status_custom_emoji_id: Optional[str] = None,
+ emoji_status_expiration_date: Optional[Union[int, dtm.datetime]] = None,
+ *,
+ read_timeout: ODVInput[float] = DEFAULT_NONE,
+ write_timeout: ODVInput[float] = DEFAULT_NONE,
+ connect_timeout: ODVInput[float] = DEFAULT_NONE,
+ pool_timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: Optional[JSONDict] = None,
+ ) -> bool:
+ """Changes the emoji status for a given user that previously allowed the bot to manage
+ their emoji status via the Mini App method
+ `requestEmojiStatusAccess `_
+ .
+
+ .. versionadded:: 21.8
+
+ Args:
+ user_id (:obj:`int`): Unique identifier of the target user
+ emoji_status_custom_emoji_id (:obj:`str`, optional): Custom emoji identifier of the
+ emoji status to set. Pass an empty string to remove the status.
+ emoji_status_expiration_date (Union[:obj:`int`, :obj:`datetime.datetime`], optional):
+ Expiration date of the emoji status, if any, as unix timestamp or
+ :class:`datetime.datetime` object.
+ |tz-naive-dtms|
+
+ Returns:
+ :obj:`bool`: On success, :obj:`True` is returned.
+
+ Raises:
+ :class:`telegram.error.TelegramError`
+
+ """
+ data: JSONDict = {
+ "user_id": user_id,
+ "emoji_status_custom_emoji_id": emoji_status_custom_emoji_id,
+ "emoji_status_expiration_date": emoji_status_expiration_date,
+ }
+ return await self._post(
+ "setUserEmojiStatus",
+ data,
+ read_timeout=read_timeout,
+ write_timeout=write_timeout,
+ connect_timeout=connect_timeout,
+ pool_timeout=pool_timeout,
+ api_kwargs=api_kwargs,
+ )
+
async def pin_chat_message(
self,
chat_id: Union[str, int],
message_id: int,
disable_notification: ODVInput[bool] = DEFAULT_NONE,
+ business_connection_id: Optional[str] = None,
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -6151,6 +6423,10 @@ async def pin_chat_message(
disable_notification (:obj:`bool`, optional): Pass :obj:`True`, if it is not necessary
to send a notification to all chat members about the new pinned message.
Notifications are always disabled in channels and private chats.
+ business_connection_id (:obj:`str`, optional): Unique identifier of the business
+ connection on behalf of which the message will be pinned.
+
+ .. versionadded:: 21.5
Returns:
:obj:`bool`: On success, :obj:`True` is returned.
@@ -6163,6 +6439,7 @@ async def pin_chat_message(
"chat_id": chat_id,
"message_id": message_id,
"disable_notification": disable_notification,
+ "business_connection_id": business_connection_id,
}
return await self._post(
@@ -6179,6 +6456,7 @@ async def unpin_chat_message(
self,
chat_id: Union[str, int],
message_id: Optional[int] = None,
+ business_connection_id: Optional[str] = None,
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -6195,8 +6473,13 @@ async def unpin_chat_message(
Args:
chat_id (:obj:`int` | :obj:`str`): |chat_id_channel|
- message_id (:obj:`int`, optional): Identifier of a message to unpin. If not specified,
+ message_id (:obj:`int`, optional): Identifier of the message to unpin. Required if
+ :paramref:`business_connection_id` is specified. If not specified,
the most recent pinned message (by sending date) will be unpinned.
+ business_connection_id (:obj:`str`, optional): Unique identifier of the business
+ connection on behalf of which the message will be unpinned.
+
+ .. versionadded:: 21.5
Returns:
:obj:`bool`: On success, :obj:`True` is returned.
@@ -6205,7 +6488,11 @@ async def unpin_chat_message(
:class:`telegram.error.TelegramError`
"""
- data: JSONDict = {"chat_id": chat_id, "message_id": message_id}
+ data: JSONDict = {
+ "chat_id": chat_id,
+ "message_id": message_id,
+ "business_connection_id": business_connection_id,
+ }
return await self._post(
"unpinChatMessage",
@@ -6287,7 +6574,7 @@ async def get_sticker_set(
pool_timeout=pool_timeout,
api_kwargs=api_kwargs,
)
- return StickerSet.de_json(result, self) # type: ignore[return-value]
+ return StickerSet.de_json(result, self)
async def get_custom_emoji_stickers(
self,
@@ -6298,7 +6585,7 @@ async def get_custom_emoji_stickers(
connect_timeout: ODVInput[float] = DEFAULT_NONE,
pool_timeout: ODVInput[float] = DEFAULT_NONE,
api_kwargs: Optional[JSONDict] = None,
- ) -> Tuple[Sticker, ...]:
+ ) -> tuple[Sticker, ...]:
"""
Use this method to get information about emoji stickers by their identifiers.
@@ -6314,7 +6601,7 @@ async def get_custom_emoji_stickers(
|sequenceargs|
Returns:
- Tuple[:class:`telegram.Sticker`]
+ tuple[:class:`telegram.Sticker`]
Raises:
:class:`telegram.error.TelegramError`
@@ -6354,8 +6641,9 @@ async def upload_sticker_file(
Args:
user_id (:obj:`int`): User identifier of sticker file owner.
- sticker (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path`):
- A file with the sticker in the ``".WEBP"``, ``".PNG"``, ``".TGS"`` or ``".WEBM"``
+ sticker (:obj:`str` | :term:`file object` | :class:`~telegram.InputFile` | \
+ :obj:`bytes` | :class:`pathlib.Path`): A file with the sticker in the
+ ``".WEBP"``, ``".PNG"``, ``".TGS"`` or ``".WEBM"``
format. See `here `_ for technical requirements
. |uploadinput|
@@ -6389,7 +6677,7 @@ async def upload_sticker_file(
pool_timeout=pool_timeout,
api_kwargs=api_kwargs,
)
- return File.de_json(result, self) # type: ignore[return-value]
+ return File.de_json(result, self)
async def add_sticker_to_set(
self,
@@ -6452,7 +6740,7 @@ async def add_sticker_to_set(
async def set_sticker_position_in_set(
self,
- sticker: str,
+ sticker: Union[str, "Sticker"],
position: int,
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -6464,7 +6752,11 @@ async def set_sticker_position_in_set(
"""Use this method to move a sticker in a set created by the bot to a specific position.
Args:
- sticker (:obj:`str`): File identifier of the sticker.
+ sticker (:obj:`str` | :class:`~telegram.Sticker`): File identifier of the sticker or
+ the sticker object.
+
+ .. versionchanged:: 21.10
+ Accepts also :class:`telegram.Sticker` instances.
position (:obj:`int`): New sticker position in the set, zero-based.
Returns:
@@ -6474,7 +6766,10 @@ async def set_sticker_position_in_set(
:class:`telegram.error.TelegramError`
"""
- data: JSONDict = {"sticker": sticker, "position": position}
+ data: JSONDict = {
+ "sticker": sticker if isinstance(sticker, str) else sticker.file_id,
+ "position": position,
+ }
return await self._post(
"setStickerPositionInSet",
data,
@@ -6579,7 +6874,7 @@ async def create_new_sticker_set(
async def delete_sticker_from_set(
self,
- sticker: str,
+ sticker: Union[str, "Sticker"],
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -6590,7 +6885,11 @@ async def delete_sticker_from_set(
"""Use this method to delete a sticker from a set created by the bot.
Args:
- sticker (:obj:`str`): File identifier of the sticker.
+ sticker (:obj:`str` | :class:`telegram.Sticker`): File identifier of the sticker or
+ the sticker object.
+
+ .. versionchanged:: 21.10
+ Accepts also :class:`telegram.Sticker` instances.
Returns:
:obj:`bool`: On success, :obj:`True` is returned.
@@ -6599,7 +6898,7 @@ async def delete_sticker_from_set(
:class:`telegram.error.TelegramError`
"""
- data: JSONDict = {"sticker": sticker}
+ data: JSONDict = {"sticker": sticker if isinstance(sticker, str) else sticker.file_id}
return await self._post(
"deleteStickerFromSet",
data,
@@ -6675,12 +6974,13 @@ async def set_sticker_set_thumbnail(
: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
- thumbnail (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path`, \
- optional): A **.WEBP** or **.PNG** image with the thumbnail, must
+ thumbnail (:obj:`str` | :term:`file object` | :class:`~telegram.InputFile` | \
+ :obj:`bytes` | :class:`pathlib.Path`, optional): A **.WEBP** or **.PNG** image
+ with the thumbnail, must
be up to :tg-const:`telegram.constants.StickerSetLimit.MAX_STATIC_THUMBNAIL_SIZE`
kilobytes in size and have width and height of exactly
:tg-const:`telegram.constants.StickerSetLimit.STATIC_THUMB_DIMENSIONS` px, or a
@@ -6688,7 +6988,7 @@ async def set_sticker_set_thumbnail(
:tg-const:`telegram.constants.StickerSetLimit.MAX_ANIMATED_THUMBNAIL_SIZE`
kilobytes in size; see
`the docs `_ 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 `_ for video sticker
@@ -6766,7 +7066,7 @@ async def set_sticker_set_title(
async def set_sticker_emoji_list(
self,
- sticker: str,
+ sticker: Union[str, "Sticker"],
emoji_list: Sequence[str],
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -6782,7 +7082,11 @@ async def set_sticker_emoji_list(
.. versionadded:: 20.2
Args:
- sticker (:obj:`str`): File identifier of the sticker.
+ sticker (:obj:`str` | :class:`~telegram.Sticker`): File identifier of the sticker or
+ the sticker object.
+
+ .. versionchanged:: 21.10
+ Accepts also :class:`telegram.Sticker` instances.
emoji_list (Sequence[:obj:`str`]): A sequence of
:tg-const:`telegram.constants.StickerLimit.MIN_STICKER_EMOJI`-
:tg-const:`telegram.constants.StickerLimit.MAX_STICKER_EMOJI` emoji associated with
@@ -6794,7 +7098,10 @@ async def set_sticker_emoji_list(
Raises:
:class:`telegram.error.TelegramError`
"""
- data: JSONDict = {"sticker": sticker, "emoji_list": emoji_list}
+ data: JSONDict = {
+ "sticker": sticker if isinstance(sticker, str) else sticker.file_id,
+ "emoji_list": emoji_list,
+ }
return await self._post(
"setStickerEmojiList",
data,
@@ -6807,7 +7114,7 @@ async def set_sticker_emoji_list(
async def set_sticker_keywords(
self,
- sticker: str,
+ sticker: Union[str, "Sticker"],
keywords: Optional[Sequence[str]] = None,
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -6823,7 +7130,11 @@ async def set_sticker_keywords(
.. versionadded:: 20.2
Args:
- sticker (:obj:`str`): File identifier of the sticker.
+ sticker (:obj:`str` | :class:`~telegram.Sticker`): File identifier of the sticker or
+ the sticker object.
+
+ .. versionchanged:: 21.10
+ Accepts also :class:`telegram.Sticker` instances.
keywords (Sequence[:obj:`str`]): A sequence of
0-:tg-const:`telegram.constants.StickerLimit.MAX_SEARCH_KEYWORDS` search keywords
for the sticker with total length up to
@@ -6835,7 +7146,10 @@ async def set_sticker_keywords(
Raises:
:class:`telegram.error.TelegramError`
"""
- data: JSONDict = {"sticker": sticker, "keywords": keywords}
+ data: JSONDict = {
+ "sticker": sticker if isinstance(sticker, str) else sticker.file_id,
+ "keywords": keywords,
+ }
return await self._post(
"setStickerKeywords",
data,
@@ -6848,7 +7162,7 @@ async def set_sticker_keywords(
async def set_sticker_mask_position(
self,
- sticker: str,
+ sticker: Union[str, "Sticker"],
mask_position: Optional[MaskPosition] = None,
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -6864,7 +7178,11 @@ async def set_sticker_mask_position(
.. versionadded:: 20.2
Args:
- sticker (:obj:`str`): File identifier of the sticker.
+ sticker (:obj:`str` | :class:`~telegram.Sticker`): File identifier of the sticker or
+ the sticker object.
+
+ .. versionchanged:: 21.10
+ Accepts also :class:`telegram.Sticker` instances.
mask_position (:class:`telegram.MaskPosition`, optional): A object with the position
where the mask should be placed on faces. Omit the parameter to remove the mask
position.
@@ -6875,7 +7193,10 @@ async def set_sticker_mask_position(
Raises:
:class:`telegram.error.TelegramError`
"""
- data: JSONDict = {"sticker": sticker, "mask_position": mask_position}
+ data: JSONDict = {
+ "sticker": sticker if isinstance(sticker, str) else sticker.file_id,
+ "mask_position": mask_position,
+ }
return await self._post(
"setStickerMaskPosition",
data,
@@ -6987,8 +7308,8 @@ async def send_poll(
reply_markup: Optional[ReplyMarkup] = None,
explanation: Optional[str] = None,
explanation_parse_mode: ODVInput[str] = DEFAULT_NONE,
- open_period: Optional[int] = None,
- close_date: Optional[Union[int, datetime]] = None,
+ open_period: Optional[TimePeriod] = None,
+ close_date: Optional[Union[int, dtm.datetime]] = None,
explanation_entities: Optional[Sequence["MessageEntity"]] = None,
protect_content: ODVInput[bool] = DEFAULT_NONE,
message_thread_id: Optional[int] = None,
@@ -6997,6 +7318,7 @@ async def send_poll(
question_parse_mode: ODVInput[str] = DEFAULT_NONE,
question_entities: Optional[Sequence["MessageEntity"]] = None,
message_effect_id: Optional[str] = None,
+ allow_paid_broadcast: Optional[bool] = None,
*,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
reply_to_message_id: Optional[int] = None,
@@ -7049,18 +7371,20 @@ async def send_poll(
.. versionchanged:: 20.0
|sequenceargs|
- open_period (:obj:`int`, optional): Amount of time in seconds the poll will be active
+ open_period (:obj:`int` | :class:`datetime.timedelta`, optional): Amount of time in
+ seconds the poll will be active
after creation, :tg-const:`telegram.Poll.MIN_OPEN_PERIOD`-
:tg-const:`telegram.Poll.MAX_OPEN_PERIOD`. Can't be used together with
:paramref:`close_date`.
+
+ .. versionchanged:: 21.11
+ |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
:tg-const:`telegram.Poll.MIN_OPEN_PERIOD` and no more than
:tg-const:`telegram.Poll.MAX_OPEN_PERIOD` seconds in the future.
Can't be used together with :paramref:`open_period`.
- 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.
+ |tz-naive-dtms|
is_closed (:obj:`bool`, optional): Pass :obj:`True`, if the poll needs to be
immediately closed. This can be useful for poll preview.
disable_notification (:obj:`bool`, optional): |disable_notification|
@@ -7094,6 +7418,9 @@ async def send_poll(
message_effect_id (:obj:`str`, optional): |message_effect_id|
.. versionadded:: 21.3
+ allow_paid_broadcast (:obj:`bool`, optional): |allow_paid_broadcast|
+
+ .. versionadded:: 21.7
Keyword Args:
allow_sending_without_reply (:obj:`bool`, optional): |allow_sending_without_reply|
@@ -7160,6 +7487,7 @@ async def send_poll(
api_kwargs=api_kwargs,
business_connection_id=business_connection_id,
message_effect_id=message_effect_id,
+ allow_paid_broadcast=allow_paid_broadcast,
)
async def stop_poll(
@@ -7210,7 +7538,7 @@ async def stop_poll(
pool_timeout=pool_timeout,
api_kwargs=api_kwargs,
)
- return Poll.de_json(result, self) # type: ignore[return-value]
+ return Poll.de_json(result, self)
async def send_dice(
self,
@@ -7223,6 +7551,7 @@ async def send_dice(
reply_parameters: Optional["ReplyParameters"] = None,
business_connection_id: Optional[str] = None,
message_effect_id: Optional[str] = None,
+ allow_paid_broadcast: Optional[bool] = None,
*,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
reply_to_message_id: Optional[int] = None,
@@ -7272,6 +7601,9 @@ async def send_dice(
message_effect_id (:obj:`str`, optional): |message_effect_id|
.. versionadded:: 21.3
+ allow_paid_broadcast (:obj:`bool`, optional): |allow_paid_broadcast|
+
+ .. versionadded:: 21.7
Keyword Args:
allow_sending_without_reply (:obj:`bool`, optional): |allow_sending_without_reply|
@@ -7320,6 +7652,7 @@ async def send_dice(
api_kwargs=api_kwargs,
business_connection_id=business_connection_id,
message_effect_id=message_effect_id,
+ allow_paid_broadcast=allow_paid_broadcast,
)
async def get_my_default_administrator_rights(
@@ -7361,7 +7694,7 @@ async def get_my_default_administrator_rights(
api_kwargs=api_kwargs,
)
- return ChatAdministratorRights.de_json(result, self) # type: ignore[return-value]
+ return ChatAdministratorRights.de_json(result, self)
async def set_my_default_administrator_rights(
self,
@@ -7383,8 +7716,9 @@ async def set_my_default_administrator_rights(
.. versionadded:: 20.0
Args:
- rights (:obj:`telegram.ChatAdministratorRights`, optional): A
- :obj:`telegram.ChatAdministratorRights` object describing new default administrator
+ rights (:class:`telegram.ChatAdministratorRights`, optional): A
+ :class:`telegram.ChatAdministratorRights` object describing new default
+ administrator
rights. If not specified, the default administrator rights will be cleared.
for_channels (:obj:`bool`, optional): Pass :obj:`True` to change the default
administrator rights of the bot in channels. Otherwise, the default administrator
@@ -7394,7 +7728,7 @@ async def set_my_default_administrator_rights(
:obj:`bool`: Returns :obj:`True` on success.
Raises:
- :obj:`telegram.error.TelegramError`
+ :exc:`telegram.error.TelegramError`
"""
data: JSONDict = {"rights": rights, "for_channels": for_channels}
@@ -7418,7 +7752,7 @@ async def get_my_commands(
connect_timeout: ODVInput[float] = DEFAULT_NONE,
pool_timeout: ODVInput[float] = DEFAULT_NONE,
api_kwargs: Optional[JSONDict] = None,
- ) -> Tuple[BotCommand, ...]:
+ ) -> tuple[BotCommand, ...]:
"""
Use this method to get the current list of the bot's commands for the given scope and user
language.
@@ -7440,7 +7774,7 @@ async def get_my_commands(
.. versionadded:: 13.7
Returns:
- Tuple[:class:`telegram.BotCommand`]: On success, the commands set for the bot. An empty
+ tuple[:class:`telegram.BotCommand`]: On success, the commands set for the bot. An empty
tuple is returned if commands are not set.
Raises:
@@ -7463,7 +7797,7 @@ async def get_my_commands(
async def set_my_commands(
self,
- commands: Sequence[Union[BotCommand, Tuple[str, str]]],
+ commands: Sequence[Union[BotCommand, tuple[str, str]]],
scope: Optional[BotCommandScope] = None,
language_code: Optional[str] = None,
*,
@@ -7649,6 +7983,8 @@ async def copy_message(
message_thread_id: Optional[int] = None,
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,
@@ -7668,6 +8004,10 @@ async def copy_message(
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.
@@ -7696,6 +8036,9 @@ async def copy_message(
show_caption_above_media (:obj:`bool`, optional): Pass |show_cap_above_med|
.. versionadded:: 21.3
+ allow_paid_broadcast (:obj:`bool`, optional): |allow_paid_broadcast|
+
+ .. versionadded:: 21.7
Keyword Args:
allow_sending_without_reply (:obj:`bool`, optional): |allow_sending_without_reply|
@@ -7754,6 +8097,8 @@ async def copy_message(
"message_thread_id": message_thread_id,
"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(
@@ -7765,7 +8110,7 @@ async def copy_message(
pool_timeout=pool_timeout,
api_kwargs=api_kwargs,
)
- return MessageId.de_json(result, self) # type: ignore[return-value]
+ return MessageId.de_json(result, self)
async def copy_messages(
self,
@@ -7782,7 +8127,7 @@ async def copy_messages(
connect_timeout: ODVInput[float] = DEFAULT_NONE,
pool_timeout: ODVInput[float] = DEFAULT_NONE,
api_kwargs: Optional[JSONDict] = None,
- ) -> Tuple["MessageId", ...]:
+ ) -> tuple["MessageId", ...]:
"""
Use this method to copy messages of any kind. If some of the specified messages can't be
found or copied, they are skipped. Service messages, paid media messages, giveaway
@@ -7810,7 +8155,7 @@ async def copy_messages(
their captions.
Returns:
- Tuple[:class:`telegram.MessageId`]: On success, a tuple of :class:`~telegram.MessageId`
+ tuple[:class:`telegram.MessageId`]: On success, a tuple of :class:`~telegram.MessageId`
of the sent messages is returned.
Raises:
@@ -7916,16 +8261,16 @@ async def get_chat_menu_button(
pool_timeout=pool_timeout,
api_kwargs=api_kwargs,
)
- return MenuButton.de_json(result, bot=self) # type: ignore[return-value]
+ return MenuButton.de_json(result, bot=self)
async def create_invoice_link(
self,
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,
@@ -7940,6 +8285,8 @@ async def create_invoice_link(
send_phone_number_to_provider: Optional[bool] = None,
send_email_to_provider: Optional[bool] = None,
is_flexible: Optional[bool] = None,
+ subscription_period: Optional[TimePeriod] = None,
+ business_connection_id: Optional[str] = None,
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -7952,6 +8299,10 @@ async def create_invoice_link(
.. versionadded:: 20.0
Args:
+ business_connection_id (:obj:`str`, optional): |business_id_str|
+ For payments in |tg_stars| only.
+
+ .. versionadded:: 21.8
title (:obj:`str`): Product name. :tg-const:`telegram.Invoice.MIN_TITLE_LENGTH`-
:tg-const:`telegram.Invoice.MAX_TITLE_LENGTH` characters.
description (:obj:`str`): Product description.
@@ -7960,14 +8311,14 @@ async def create_invoice_link(
payload (:obj:`str`): Bot-defined invoice payload.
:tg-const:`telegram.Invoice.MIN_PAYLOAD_LENGTH`-
:tg-const:`telegram.Invoice.MAX_PAYLOAD_LENGTH` bytes. This will not be
- displayed to the user, use for your internal processes.
- provider_token (:obj:`str`): Payments provider token, obtained via
+ displayed to the user, use it for your internal processes.
+ provider_token (:obj:`str`, optional): Payments provider token, obtained via
`@BotFather `_. Pass an empty string for payments in
|tg_stars|.
- .. 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.
+ .. versionchanged:: 21.11
+ Bot API 7.4 made this parameter is optional and this is now reflected in the
+ function signature.
currency (:obj:`str`): Three-letter ISO 4217 currency code, see `more on currencies
`_. Pass ``XTR`` for
@@ -7978,6 +8329,18 @@ async def create_invoice_link(
.. versionchanged:: 20.0
|sequenceargs|
+ subscription_period (:obj:`int` | :class:`datetime.timedelta`, optional): The time the
+ subscription will be active for before the next payment, either as number of
+ seconds or as :class:`datetime.timedelta` object. The currency must be set to
+ ``“XTR”`` (Telegram Stars) if the parameter is used. Currently, it must always be
+ :tg-const:`telegram.constants.InvoiceLimit.SUBSCRIPTION_PERIOD` if specified. Any
+ number of subscriptions can be active for a given bot at the same time, including
+ multiple concurrent subscriptions from the same user. Subscription price must
+ not exceed
+ :tg-const:`telegram.constants.InvoiceLimit.SUBSCRIPTION_MAX_PRICE`
+ Telegram Stars.
+
+ .. versionadded:: 21.8
max_tip_amount (:obj:`int`, optional): The maximum accepted amount for tips in the
*smallest units* of the currency (integer, **not** float/double). For example, for
a maximum tip of ``US$ 1.45`` pass ``max_tip_amount = 145``. See the ``exp``
@@ -8043,6 +8406,8 @@ async def create_invoice_link(
"is_flexible": is_flexible,
"send_phone_number_to_provider": send_phone_number_to_provider,
"send_email_to_provider": send_email_to_provider,
+ "subscription_period": subscription_period,
+ "business_connection_id": business_connection_id,
}
return await self._post(
@@ -8063,14 +8428,14 @@ async def get_forum_topic_icon_stickers(
connect_timeout: ODVInput[float] = DEFAULT_NONE,
pool_timeout: ODVInput[float] = DEFAULT_NONE,
api_kwargs: Optional[JSONDict] = None,
- ) -> Tuple[Sticker, ...]:
+ ) -> tuple[Sticker, ...]:
"""Use this method to get custom emoji stickers, which can be used as a forum topic
icon by any user. Requires no parameters.
.. versionadded:: 20.0
Returns:
- Tuple[:class:`telegram.Sticker`]
+ tuple[:class:`telegram.Sticker`]
Raises:
:class:`telegram.error.TelegramError`
@@ -8143,7 +8508,7 @@ async def create_forum_topic(
pool_timeout=pool_timeout,
api_kwargs=api_kwargs,
)
- return ForumTopic.de_json(result, self) # type: ignore[return-value]
+ return ForumTopic.de_json(result, self)
async def edit_forum_topic(
self,
@@ -8160,7 +8525,7 @@ async def edit_forum_topic(
) -> bool:
"""
Use this method to edit name and icon of a topic in a forum supergroup chat. The bot must
- be an administrator in the chat for this to work and must have
+ be an administrator in the chat for this to work and must have the
:paramref:`~telegram.ChatAdministratorRights.can_manage_topics` administrator rights,
unless it is the creator of the topic.
@@ -8428,7 +8793,7 @@ async def edit_general_forum_topic(
) -> bool:
"""
Use this method to edit the name of the 'General' topic in a forum supergroup chat. The bot
- must be an administrator in the chat for this to work and must have
+ must be an administrator in the chat for this to work and must have the
:attr:`~telegram.ChatAdministratorRights.can_manage_topics` administrator rights.
.. versionadded:: 20.0
@@ -8731,7 +9096,7 @@ async def get_my_description(
"""
data = {"language_code": language_code}
- return BotDescription.de_json( # type: ignore[return-value]
+ return BotDescription.de_json(
await self._post(
"getMyDescription",
data,
@@ -8770,7 +9135,7 @@ async def get_my_short_description(
"""
data = {"language_code": language_code}
- return BotShortDescription.de_json( # type: ignore[return-value]
+ return BotShortDescription.de_json(
await self._post(
"getMyShortDescription",
data,
@@ -8856,7 +9221,7 @@ async def get_my_name(
"""
data = {"language_code": language_code}
- return BotName.de_json( # type: ignore[return-value]
+ return BotName.de_json(
await self._post(
"getMyName",
data,
@@ -8898,7 +9263,7 @@ async def get_user_chat_boosts(
:class:`telegram.error.TelegramError`
"""
data: JSONDict = {"chat_id": chat_id, "user_id": user_id}
- return UserChatBoosts.de_json( # type: ignore[return-value]
+ return UserChatBoosts.de_json(
await self._post(
"getUserChatBoosts",
data,
@@ -8925,9 +9290,10 @@ async def set_message_reaction(
api_kwargs: Optional[JSONDict] = None,
) -> bool:
"""
- Use this method to change the chosen reactions on a message. Service messages can't be
+ Use this method to change the chosen reactions on a message. Service messages of some types
+ 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.
+ the same available reactions as messages in the channel. Bots can't use paid reactions.
.. versionadded:: 20.8
@@ -8940,7 +9306,8 @@ async def set_message_reaction(
:class:`telegram.ReactionType` | :obj:`str`, optional): A list of reaction
types to set on the message. Currently, as non-premium users, bots can set up to
one reaction per message. A custom emoji reaction can be used if it is either
- already present on the message or explicitly allowed by chat administrators.
+ already present on the message or explicitly allowed by chat administrators. Paid
+ reactions can't be used by bots.
Tip:
Passed :obj:`str` values will be converted to either
@@ -8958,7 +9325,7 @@ async def set_message_reaction(
Raises:
:class:`telegram.error.TelegramError`
"""
- allowed_reactions: Set[str] = set(ReactionEmoji)
+ allowed_reactions: set[str] = set(ReactionEmoji)
parsed_reaction = (
[
(
@@ -9021,7 +9388,7 @@ async def get_business_connection(
:class:`telegram.error.TelegramError`
"""
data: JSONDict = {"business_connection_id": business_connection_id}
- return BusinessConnection.de_json( # type: ignore[return-value]
+ return BusinessConnection.de_json(
await self._post(
"getBusinessConnection",
data,
@@ -9038,7 +9405,7 @@ async def replace_sticker_in_set(
self,
user_id: int,
name: str,
- old_sticker: str,
+ old_sticker: Union[str, "Sticker"],
sticker: "InputSticker",
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -9056,8 +9423,12 @@ async def replace_sticker_in_set(
Args:
user_id (:obj:`int`): User identifier of the sticker set owner.
name (:obj:`str`): Sticker set name.
- old_sticker (:obj:`str`): File identifier of the replaced sticker.
- sticker (:obj:`telegram.InputSticker`): An object with information about the added
+ old_sticker (:obj:`str` | :class:`~telegram.Sticker`): File identifier of the replaced
+ sticker or the sticker object itself.
+
+ .. versionchanged:: 21.10
+ Accepts also :class:`telegram.Sticker` instances.
+ sticker (:class:`telegram.InputSticker`): An object with information about the added
sticker. If exactly the same sticker had already been added to the set, then the
set remains unchanged.
@@ -9070,7 +9441,7 @@ async def replace_sticker_in_set(
data: JSONDict = {
"user_id": user_id,
"name": name,
- "old_sticker": old_sticker,
+ "old_sticker": old_sticker if isinstance(old_sticker, str) else old_sticker.file_id,
"sticker": sticker,
}
@@ -9156,7 +9527,7 @@ async def get_star_transactions(
data: JSONDict = {"offset": offset, "limit": limit}
- return StarTransactions.de_json( # type: ignore[return-value]
+ return StarTransactions.de_json(
await self._post(
"getStarTransactions",
data,
@@ -9169,6 +9540,53 @@ async def get_star_transactions(
bot=self,
)
+ async def edit_user_star_subscription(
+ self,
+ user_id: int,
+ telegram_payment_charge_id: str,
+ is_canceled: bool,
+ *,
+ read_timeout: ODVInput[float] = DEFAULT_NONE,
+ write_timeout: ODVInput[float] = DEFAULT_NONE,
+ connect_timeout: ODVInput[float] = DEFAULT_NONE,
+ pool_timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: Optional[JSONDict] = None,
+ ) -> bool:
+ """Allows the bot to cancel or re-enable extension of a subscription paid in Telegram
+ Stars.
+
+ .. versionadded:: 21.8
+
+ Args:
+ user_id (:obj:`int`): Identifier of the user whose subscription will be edited.
+ telegram_payment_charge_id (:obj:`str`): Telegram payment identifier for the
+ subscription.
+ is_canceled (:obj:`bool`): Pass :obj:`True` to cancel extension of the user
+ subscription; the subscription must be active up to the end of the current
+ subscription period. Pass :obj:`False` to allow the user to re-enable a
+ subscription that was previously canceled by the bot.
+
+ Returns:
+ :obj:`bool`: On success, :obj:`True` is returned.
+
+ Raises:
+ :class:`telegram.error.TelegramError`
+ """
+ data: JSONDict = {
+ "user_id": user_id,
+ "telegram_payment_charge_id": telegram_payment_charge_id,
+ "is_canceled": is_canceled,
+ }
+ return await self._post(
+ "editUserStarSubscription",
+ data,
+ read_timeout=read_timeout,
+ write_timeout=write_timeout,
+ connect_timeout=connect_timeout,
+ pool_timeout=pool_timeout,
+ api_kwargs=api_kwargs,
+ )
+
async def send_paid_media(
self,
chat_id: Union[str, int],
@@ -9182,6 +9600,9 @@ async def send_paid_media(
protect_content: ODVInput[bool] = DEFAULT_NONE,
reply_parameters: Optional["ReplyParameters"] = None,
reply_markup: Optional[ReplyMarkup] = None,
+ business_connection_id: Optional[str] = None,
+ payload: Optional[str] = None,
+ allow_paid_broadcast: Optional[bool] = None,
*,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
reply_to_message_id: Optional[int] = None,
@@ -9191,16 +9612,24 @@ async def send_paid_media(
pool_timeout: ODVInput[float] = DEFAULT_NONE,
api_kwargs: Optional[JSONDict] = None,
) -> Message:
- """Use this method to send paid media to channel chats.
+ """Use this method to send paid media.
.. versionadded:: 21.4
Args:
- chat_id (:obj:`int` | :obj:`str`): |chat_id_channel|
+ chat_id (:obj:`int` | :obj:`str`): |chat_id_channel| If the chat is a channel, all
+ Telegram Star proceeds from this media will be credited to the chat's balance.
+ Otherwise, they will be credited to the bot's balance.
star_count (:obj:`int`): The number of Telegram Stars that must be paid to buy access
- to the media.
+ to the media; :tg-const:`telegram.constants.InvoiceLimit.MIN_STAR_COUNT` -
+ :tg-const:`telegram.constants.InvoiceLimit.MAX_STAR_COUNT`.
media (Sequence[:class:`telegram.InputPaidMedia`]): A list describing the media to be
sent; up to :tg-const:`telegram.constants.MediaGroupLimit.MAX_MEDIA_LENGTH` items.
+ payload (:obj:`str`, optional): Bot-defined paid media payload,
+ 0-:tg-const:`telegram.constants.InvoiceLimit.MAX_PAYLOAD_LENGTH` bytes. This will
+ not be displayed to the user, use it for your internal processes.
+
+ .. versionadded:: 21.6
caption (:obj:`str`, optional): Caption of the media to be sent,
0-:tg-const:`telegram.constants.MessageLimit.CAPTION_LENGTH` characters.
parse_mode (:obj:`str`, optional): |parse_mode|
@@ -9214,6 +9643,12 @@ async def send_paid_media(
:class:`ReplyKeyboardRemove` | :class:`ForceReply`, optional):
Additional interface options. An object for an inline keyboard, custom reply
keyboard, instructions to remove reply keyboard or to force a reply from the user.
+ business_connection_id (:obj:`str`, optional): |business_id_str|
+
+ .. versionadded:: 21.5
+ allow_paid_broadcast (:obj:`bool`, optional): |allow_paid_broadcast|
+
+ .. versionadded:: 21.7
Keyword Args:
allow_sending_without_reply (:obj:`bool`, optional): |allow_sending_without_reply|
@@ -9236,6 +9671,7 @@ async def send_paid_media(
"star_count": star_count,
"media": media,
"show_caption_above_media": show_caption_above_media,
+ "payload": payload,
}
return await self._send_message(
@@ -9255,6 +9691,399 @@ async def send_paid_media(
connect_timeout=connect_timeout,
pool_timeout=pool_timeout,
api_kwargs=api_kwargs,
+ business_connection_id=business_connection_id,
+ allow_paid_broadcast=allow_paid_broadcast,
+ )
+
+ async def create_chat_subscription_invite_link(
+ self,
+ chat_id: Union[str, int],
+ subscription_period: TimePeriod,
+ subscription_price: int,
+ name: Optional[str] = None,
+ *,
+ read_timeout: ODVInput[float] = DEFAULT_NONE,
+ write_timeout: ODVInput[float] = DEFAULT_NONE,
+ connect_timeout: ODVInput[float] = DEFAULT_NONE,
+ pool_timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: Optional[JSONDict] = None,
+ ) -> ChatInviteLink:
+ """
+ Use this method to create a `subscription invite link `_ for a channel chat.
+ The bot must have the :attr:`~telegram.ChatPermissions.can_invite_users` administrator
+ right. The link can be edited using the :meth:`edit_chat_subscription_invite_link` or
+ revoked using the :meth:`revoke_chat_invite_link`.
+
+ .. versionadded:: 21.5
+
+ Args:
+ chat_id (:obj:`int` | :obj:`str`): |chat_id_channel|
+ subscription_period (:obj:`int` | :class:`datetime.timedelta`): The number of seconds
+ the subscription will be
+ active for before the next payment. Currently, it must always be
+ :tg-const:`telegram.constants.ChatSubscriptionLimit.SUBSCRIPTION_PERIOD` (30 days).
+
+ .. versionchanged:: 21.11
+ |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;
+ :tg-const:`telegram.constants.ChatSubscriptionLimit.MIN_PRICE`-
+ :tg-const:`telegram.constants.ChatSubscriptionLimit.MAX_PRICE`.
+ name (:obj:`str`, optional): Invite link name;
+ 0-:tg-const:`telegram.constants.ChatInviteLinkLimit.NAME_LENGTH` characters.
+
+ Returns:
+ :class:`telegram.ChatInviteLink`
+
+ Raises:
+ :class:`telegram.error.TelegramError`
+
+ """
+ data: JSONDict = {
+ "chat_id": chat_id,
+ "subscription_period": subscription_period,
+ "subscription_price": subscription_price,
+ "name": name,
+ }
+
+ result = await self._post(
+ "createChatSubscriptionInviteLink",
+ data,
+ read_timeout=read_timeout,
+ write_timeout=write_timeout,
+ connect_timeout=connect_timeout,
+ pool_timeout=pool_timeout,
+ api_kwargs=api_kwargs,
+ )
+
+ return ChatInviteLink.de_json(result, self)
+
+ async def edit_chat_subscription_invite_link(
+ self,
+ chat_id: Union[str, int],
+ invite_link: Union[str, "ChatInviteLink"],
+ name: Optional[str] = None,
+ *,
+ read_timeout: ODVInput[float] = DEFAULT_NONE,
+ write_timeout: ODVInput[float] = DEFAULT_NONE,
+ connect_timeout: ODVInput[float] = DEFAULT_NONE,
+ pool_timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: Optional[JSONDict] = None,
+ ) -> ChatInviteLink:
+ """
+ Use this method to edit a subscription invite link created by the bot. The bot must have
+ :attr:`telegram.ChatPermissions.can_invite_users` administrator right.
+
+ .. versionadded:: 21.5
+
+ Args:
+ chat_id (:obj:`int` | :obj:`str`): |chat_id_channel|
+ invite_link (:obj:`str` | :obj:`telegram.ChatInviteLink`): The invite link to edit.
+ name (:obj:`str`, optional): Invite link name;
+ 0-:tg-const:`telegram.constants.ChatInviteLinkLimit.NAME_LENGTH` characters.
+
+ Tip:
+ Omitting this argument removes the name of the invite link.
+
+ Returns:
+ :class:`telegram.ChatInviteLink`
+
+ Raises:
+ :class:`telegram.error.TelegramError`
+
+ """
+ link = invite_link.invite_link if isinstance(invite_link, ChatInviteLink) else invite_link
+ data: JSONDict = {
+ "chat_id": chat_id,
+ "invite_link": link,
+ "name": name,
+ }
+
+ result = await self._post(
+ "editChatSubscriptionInviteLink",
+ data,
+ read_timeout=read_timeout,
+ write_timeout=write_timeout,
+ connect_timeout=connect_timeout,
+ pool_timeout=pool_timeout,
+ api_kwargs=api_kwargs,
+ )
+
+ return ChatInviteLink.de_json(result, self)
+
+ async def get_available_gifts(
+ self,
+ *,
+ read_timeout: ODVInput[float] = DEFAULT_NONE,
+ write_timeout: ODVInput[float] = DEFAULT_NONE,
+ connect_timeout: ODVInput[float] = DEFAULT_NONE,
+ pool_timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: Optional[JSONDict] = None,
+ ) -> Gifts:
+ """Returns the list of gifts that can be sent by the bot to users and channel chats.
+ Requires no parameters.
+
+ .. versionadded:: 21.8
+
+ Returns:
+ :class:`telegram.Gifts`
+
+ Raises:
+ :class:`telegram.error.TelegramError`
+ """
+ return Gifts.de_json(
+ await self._post(
+ "getAvailableGifts",
+ read_timeout=read_timeout,
+ write_timeout=write_timeout,
+ connect_timeout=connect_timeout,
+ pool_timeout=pool_timeout,
+ api_kwargs=api_kwargs,
+ )
+ )
+
+ async def send_gift(
+ self,
+ 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,
+ user_id: Optional[int] = None,
+ *,
+ read_timeout: ODVInput[float] = DEFAULT_NONE,
+ write_timeout: ODVInput[float] = DEFAULT_NONE,
+ connect_timeout: ODVInput[float] = DEFAULT_NONE,
+ pool_timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: 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.
+
+ .. versionadded:: 21.8
+ .. versionchanged:: NEXT.VERSION
+ Bot API 8.3 made :paramref:`user_id` optional. In version NEXT.VERSION, the methods
+ signature was changed accordingly.
+
+ Args:
+ gift_id (:obj:`str` | :class:`~telegram.Gift`): Identifier of the gift or a
+ :class:`~telegram.Gift` object
+ 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.
+ 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.
+ See :class:`telegram.constants.ParseMode` and
+ `formatting options `__ for
+ more details. Entities other than :attr:`~MessageEntity.BOLD`,
+ :attr:`~MessageEntity.ITALIC`, :attr:`~MessageEntity.UNDERLINE`,
+ :attr:`~MessageEntity.STRIKETHROUGH`, :attr:`~MessageEntity.SPOILER`, and
+ :attr:`~MessageEntity.CUSTOM_EMOJI` are ignored.
+ text_entities (Sequence[:class:`telegram.MessageEntity`], optional): A list of special
+ entities that appear in the gift text. It can be specified instead of
+ :paramref:`text_parse_mode`. Entities other than :attr:`~MessageEntity.BOLD`,
+ :attr:`~MessageEntity.ITALIC`, :attr:`~MessageEntity.UNDERLINE`,
+ :attr:`~MessageEntity.STRIKETHROUGH`, :attr:`~MessageEntity.SPOILER`, and
+ :attr:`~MessageEntity.CUSTOM_EMOJI` are ignored.
+ pay_for_upgrade (:obj:`bool`, optional): Pass :obj:`True` to pay for the gift upgrade
+ from the bot's balance, thereby making the upgrade free for the receiver.
+
+ .. versionadded:: 21.10
+
+ Returns:
+ :obj:`bool`: On success, :obj:`True` is returned.
+
+ Raises:
+ :class:`telegram.error.TelegramError`
+ """
+ data: JSONDict = {
+ "user_id": user_id,
+ "gift_id": gift_id.id if isinstance(gift_id, Gift) else gift_id,
+ "text": text,
+ "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",
+ data,
+ read_timeout=read_timeout,
+ write_timeout=write_timeout,
+ connect_timeout=connect_timeout,
+ pool_timeout=pool_timeout,
+ api_kwargs=api_kwargs,
+ )
+
+ async def verify_chat(
+ self,
+ chat_id: Union[int, str],
+ custom_description: Optional[str] = None,
+ *,
+ read_timeout: ODVInput[float] = DEFAULT_NONE,
+ write_timeout: ODVInput[float] = DEFAULT_NONE,
+ connect_timeout: ODVInput[float] = DEFAULT_NONE,
+ pool_timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: Optional[JSONDict] = None,
+ ) -> bool:
+ """Verifies a chat |org-verify| which is represented by the bot.
+
+ .. versionadded:: 21.10
+
+ Args:
+ chat_id (:obj:`int` | :obj:`str`): |chat_id_channel|
+ custom_description (:obj:`str`, optional): Custom description for the verification;
+ 0- :tg-const:`telegram.constants.VerifyLimit.MAX_TEXT_LENGTH` characters. Must be
+ empty if the organization isn't allowed to provide a custom verification
+ description.
+
+ Returns:
+ :obj:`bool`: On success, :obj:`True` is returned.
+
+ Raises:
+ :class:`telegram.error.TelegramError`
+ """
+ data: JSONDict = {
+ "chat_id": chat_id,
+ "custom_description": custom_description,
+ }
+ return await self._post(
+ "verifyChat",
+ data,
+ read_timeout=read_timeout,
+ write_timeout=write_timeout,
+ connect_timeout=connect_timeout,
+ pool_timeout=pool_timeout,
+ api_kwargs=api_kwargs,
+ )
+
+ async def verify_user(
+ self,
+ user_id: int,
+ custom_description: Optional[str] = None,
+ *,
+ read_timeout: ODVInput[float] = DEFAULT_NONE,
+ write_timeout: ODVInput[float] = DEFAULT_NONE,
+ connect_timeout: ODVInput[float] = DEFAULT_NONE,
+ pool_timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: Optional[JSONDict] = None,
+ ) -> bool:
+ """Verifies a user |org-verify| which is represented by the bot.
+
+ .. versionadded:: 21.10
+
+ Args:
+ user_id (:obj:`int`): Unique identifier of the target user.
+ custom_description (:obj:`str`, optional): Custom description for the verification;
+ 0- :tg-const:`telegram.constants.VerifyLimit.MAX_TEXT_LENGTH` characters. Must be
+ empty if the organization isn't allowed to provide a custom verification
+ description.
+
+ Returns:
+ :obj:`bool`: On success, :obj:`True` is returned.
+
+ Raises:
+ :class:`telegram.error.TelegramError`
+ """
+ data: JSONDict = {
+ "user_id": user_id,
+ "custom_description": custom_description,
+ }
+ return await self._post(
+ "verifyUser",
+ data,
+ read_timeout=read_timeout,
+ write_timeout=write_timeout,
+ connect_timeout=connect_timeout,
+ pool_timeout=pool_timeout,
+ api_kwargs=api_kwargs,
+ )
+
+ async def remove_chat_verification(
+ self,
+ chat_id: Union[int, str],
+ *,
+ read_timeout: ODVInput[float] = DEFAULT_NONE,
+ write_timeout: ODVInput[float] = DEFAULT_NONE,
+ connect_timeout: ODVInput[float] = DEFAULT_NONE,
+ pool_timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: Optional[JSONDict] = None,
+ ) -> bool:
+ """Removes verification from a chat that is currently verified |org-verify|
+ represented by the bot.
+
+
+
+ .. versionadded:: 21.10
+
+ Args:
+ chat_id (:obj:`int` | :obj:`str`): |chat_id_channel|
+
+ Returns:
+ :obj:`bool`: On success, :obj:`True` is returned.
+
+ Raises:
+ :class:`telegram.error.TelegramError`
+ """
+ data: JSONDict = {
+ "chat_id": chat_id,
+ }
+ return await self._post(
+ "removeChatVerification",
+ data,
+ read_timeout=read_timeout,
+ write_timeout=write_timeout,
+ connect_timeout=connect_timeout,
+ pool_timeout=pool_timeout,
+ api_kwargs=api_kwargs,
+ )
+
+ async def remove_user_verification(
+ self,
+ user_id: int,
+ *,
+ read_timeout: ODVInput[float] = DEFAULT_NONE,
+ write_timeout: ODVInput[float] = DEFAULT_NONE,
+ connect_timeout: ODVInput[float] = DEFAULT_NONE,
+ pool_timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: Optional[JSONDict] = None,
+ ) -> bool:
+ """Removes verification from a user who is currently verified |org-verify|
+ represented by the bot.
+
+
+
+ .. versionadded:: 21.10
+
+ Args:
+ user_id (:obj:`int`): Unique identifier of the target user.
+
+ Returns:
+ :obj:`bool`: On success, :obj:`True` is returned.
+
+ Raises:
+ :class:`telegram.error.TelegramError`
+ """
+ data: JSONDict = {
+ "user_id": user_id,
+ }
+ return await self._post(
+ "removeUserVerification",
+ data,
+ read_timeout=read_timeout,
+ write_timeout=write_timeout,
+ connect_timeout=connect_timeout,
+ pool_timeout=pool_timeout,
+ api_kwargs=api_kwargs,
)
def to_dict(self, recursive: bool = True) -> JSONDict: # noqa: ARG002
@@ -9313,6 +10142,8 @@ def to_dict(self, recursive: bool = True) -> JSONDict: # noqa: ARG002
"""Alias for :meth:`send_chat_action`"""
answerInlineQuery = answer_inline_query
"""Alias for :meth:`answer_inline_query`"""
+ savePreparedInlineMessage = save_prepared_inline_message
+ """Alias for :meth:`save_prepared_inline_message`"""
getUserProfilePhotos = get_user_profile_photos
"""Alias for :meth:`get_user_profile_photos`"""
getFile = get_file
@@ -9397,6 +10228,8 @@ def to_dict(self, recursive: bool = True) -> JSONDict: # noqa: ARG002
"""Alias for :meth:`set_chat_title`"""
setChatDescription = set_chat_description
"""Alias for :meth:`set_chat_description`"""
+ setUserEmojiStatus = set_user_emoji_status
+ """Alias for :meth:`set_user_emoji_status`"""
pinChatMessage = pin_chat_message
"""Alias for :meth:`pin_chat_message`"""
unpinChatMessage = unpin_chat_message
@@ -9511,5 +10344,23 @@ def to_dict(self, recursive: bool = True) -> JSONDict: # noqa: ARG002
"""Alias for :meth:`refund_star_payment`"""
getStarTransactions = get_star_transactions
"""Alias for :meth:`get_star_transactions`"""
+ editUserStarSubscription = edit_user_star_subscription
+ """Alias for :meth:`edit_user_star_subscription`"""
sendPaidMedia = send_paid_media
"""Alias for :meth:`send_paid_media`"""
+ createChatSubscriptionInviteLink = create_chat_subscription_invite_link
+ """Alias for :meth:`create_chat_subscription_invite_link`"""
+ editChatSubscriptionInviteLink = edit_chat_subscription_invite_link
+ """Alias for :meth:`edit_chat_subscription_invite_link`"""
+ getAvailableGifts = get_available_gifts
+ """Alias for :meth:`get_available_gifts`"""
+ sendGift = send_gift
+ """Alias for :meth:`send_gift`"""
+ verifyChat = verify_chat
+ """Alias for :meth:`verify_chat`"""
+ verifyUser = verify_user
+ """Alias for :meth:`verify_user`"""
+ removeChatVerification = remove_chat_verification
+ """Alias for :meth:`remove_chat_verification`"""
+ removeUserVerification = remove_user_verification
+ """Alias for :meth:`remove_user_verification`"""
diff --git a/telegram/_botcommand.py b/telegram/_botcommand.py
index 972db7c2402..059740803d8 100644
--- a/telegram/_botcommand.py
+++ b/telegram/_botcommand.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2024
+# Copyright (C) 2015-2025
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
diff --git a/telegram/_botcommandscope.py b/telegram/_botcommandscope.py
index 73cafd17599..dbce54c32c4 100644
--- a/telegram/_botcommandscope.py
+++ b/telegram/_botcommandscope.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2024
+# Copyright (C) 2015-2025
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -18,7 +18,7 @@
# along with this program. If not, see [http://www.gnu.org/licenses/].
# pylint: disable=redefined-builtin
"""This module contains objects representing Telegram bot command scopes."""
-from typing import TYPE_CHECKING, Dict, Final, Optional, Type, Union
+from typing import TYPE_CHECKING, Final, Optional, Union
from telegram import constants
from telegram._telegramobject import TelegramObject
@@ -84,14 +84,12 @@ def __init__(self, type: str, *, api_kwargs: Optional[JSONDict] = None):
self._freeze()
@classmethod
- def de_json(
- cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
- ) -> Optional["BotCommandScope"]:
+ def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "BotCommandScope":
"""Converts JSON data to the appropriate :class:`BotCommandScope` object, i.e. takes
care of selecting the correct subclass.
Args:
- data (Dict[:obj:`str`, ...]): The JSON data.
+ data (dict[:obj:`str`, ...]): The JSON data.
bot (:class:`telegram.Bot`, optional): The bot associated with this object. Defaults to
:obj:`None`, in which case shortcut methods will not be available.
@@ -104,10 +102,7 @@ def de_json(
"""
data = cls._parse_data(data)
- if not data:
- return None
-
- _class_mapping: Dict[str, Type[BotCommandScope]] = {
+ _class_mapping: dict[str, type[BotCommandScope]] = {
cls.DEFAULT: BotCommandScopeDefault,
cls.ALL_PRIVATE_CHATS: BotCommandScopeAllPrivateChats,
cls.ALL_GROUP_CHATS: BotCommandScopeAllGroupChats,
diff --git a/telegram/_botdescription.py b/telegram/_botdescription.py
index e2a9d36df1d..9f53ef1be86 100644
--- a/telegram/_botdescription.py
+++ b/telegram/_botdescription.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2024
+# Copyright (C) 2015-2025
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
diff --git a/telegram/_botname.py b/telegram/_botname.py
index 2a57ea39f0d..a297027eae6 100644
--- a/telegram/_botname.py
+++ b/telegram/_botname.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2024
+# Copyright (C) 2015-2025
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
diff --git a/telegram/_business.py b/telegram/_business.py
index 22c89e024b4..95607c24344 100644
--- a/telegram/_business.py
+++ b/telegram/_business.py
@@ -2,7 +2,7 @@
# pylint: disable=redefined-builtin
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2024
+# Copyright (C) 2015-2025
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -18,16 +18,16 @@
# 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 Telegram Business related classes."""
-
-from datetime import datetime
-from typing import TYPE_CHECKING, Optional, Sequence, Tuple
+import datetime as dtm
+from collections.abc import Sequence
+from typing import TYPE_CHECKING, Optional
from telegram._chat import Chat
from telegram._files.location import Location
from telegram._files.sticker import Sticker
from telegram._telegramobject import TelegramObject
from telegram._user import User
-from telegram._utils.argumentparsing import parse_sequence_arg
+from telegram._utils.argumentparsing import de_json_optional, de_list_optional, parse_sequence_arg
from telegram._utils.datetime import extract_tzinfo_from_defaults, from_timestamp
from telegram._utils.types import JSONDict
@@ -80,7 +80,7 @@ def __init__(
id: str,
user: "User",
user_chat_id: int,
- date: datetime,
+ date: dtm.datetime,
can_reply: bool,
is_enabled: bool,
*,
@@ -90,7 +90,7 @@ def __init__(
self.id: str = id
self.user: User = user
self.user_chat_id: int = user_chat_id
- self.date: datetime = date
+ self.date: dtm.datetime = date
self.can_reply: bool = can_reply
self.is_enabled: bool = is_enabled
@@ -106,20 +106,15 @@ def __init__(
self._freeze()
@classmethod
- def de_json(
- cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
- ) -> Optional["BusinessConnection"]:
+ def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "BusinessConnection":
"""See :meth:`telegram.TelegramObject.de_json`."""
data = cls._parse_data(data)
- if not data:
- return None
-
# Get the local timezone from the bot if it has defaults
loc_tzinfo = extract_tzinfo_from_defaults(bot)
data["date"] = from_timestamp(data.get("date"), tzinfo=loc_tzinfo)
- data["user"] = User.de_json(data.get("user"), bot)
+ data["user"] = de_json_optional(data.get("user"), User, bot)
return super().de_json(data=data, bot=bot)
@@ -145,7 +140,7 @@ class BusinessMessagesDeleted(TelegramObject):
business_connection_id (:obj:`str`): Unique identifier of the business connection.
chat (:class:`telegram.Chat`): Information about a chat in the business account. The bot
may not have access to the chat or the corresponding user.
- message_ids (Tuple[:obj:`int`]): A list of identifiers of the deleted messages in the
+ message_ids (tuple[:obj:`int`]): A list of identifiers of the deleted messages in the
chat of the business account.
"""
@@ -166,7 +161,7 @@ def __init__(
super().__init__(api_kwargs=api_kwargs)
self.business_connection_id: str = business_connection_id
self.chat: Chat = chat
- self.message_ids: Tuple[int, ...] = parse_sequence_arg(message_ids)
+ self.message_ids: tuple[int, ...] = parse_sequence_arg(message_ids)
self._id_attrs = (
self.business_connection_id,
@@ -177,16 +172,11 @@ def __init__(
self._freeze()
@classmethod
- def de_json(
- cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
- ) -> Optional["BusinessMessagesDeleted"]:
+ def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "BusinessMessagesDeleted":
"""See :meth:`telegram.TelegramObject.de_json`."""
data = cls._parse_data(data)
- if not data:
- return None
-
- data["chat"] = Chat.de_json(data.get("chat"), bot)
+ data["chat"] = de_json_optional(data.get("chat"), Chat, bot)
return super().de_json(data=data, bot=bot)
@@ -236,16 +226,11 @@ def __init__(
self._freeze()
@classmethod
- def de_json(
- cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
- ) -> Optional["BusinessIntro"]:
+ def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "BusinessIntro":
"""See :meth:`telegram.TelegramObject.de_json`."""
data = cls._parse_data(data)
- if not data:
- return None
-
- data["sticker"] = Sticker.de_json(data.get("sticker"), bot)
+ data["sticker"] = de_json_optional(data.get("sticker"), Sticker, bot)
return super().de_json(data=data, bot=bot)
@@ -290,16 +275,11 @@ def __init__(
self._freeze()
@classmethod
- def de_json(
- cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
- ) -> Optional["BusinessLocation"]:
+ def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "BusinessLocation":
"""See :meth:`telegram.TelegramObject.de_json`."""
data = cls._parse_data(data)
- if not data:
- return None
-
- data["location"] = Location.de_json(data.get("location"), bot)
+ data["location"] = de_json_optional(data.get("location"), Location, bot)
return super().de_json(data=data, bot=bot)
@@ -359,37 +339,37 @@ def __init__(
self.opening_minute: int = opening_minute
self.closing_minute: int = closing_minute
- self._opening_time: Optional[Tuple[int, int, int]] = None
- self._closing_time: Optional[Tuple[int, int, int]] = None
+ self._opening_time: Optional[tuple[int, int, int]] = None
+ self._closing_time: Optional[tuple[int, int, int]] = None
self._id_attrs = (self.opening_minute, self.closing_minute)
self._freeze()
- def _parse_minute(self, minute: int) -> Tuple[int, int, int]:
+ def _parse_minute(self, minute: int) -> tuple[int, int, int]:
return (minute // 1440, minute % 1440 // 60, minute % 1440 % 60)
@property
- def opening_time(self) -> Tuple[int, int, int]:
+ def opening_time(self) -> tuple[int, int, int]:
"""Convenience attribute. A :obj:`tuple` parsed from :attr:`opening_minute`. It contains
the `weekday`, `hour` and `minute` in the same ranges as :attr:`datetime.datetime.weekday`,
:attr:`datetime.datetime.hour` and :attr:`datetime.datetime.minute`
Returns:
- Tuple[:obj:`int`, :obj:`int`, :obj:`int`]:
+ tuple[:obj:`int`, :obj:`int`, :obj:`int`]:
"""
if self._opening_time is None:
self._opening_time = self._parse_minute(self.opening_minute)
return self._opening_time
@property
- def closing_time(self) -> Tuple[int, int, int]:
+ def closing_time(self) -> tuple[int, int, int]:
"""Convenience attribute. A :obj:`tuple` parsed from :attr:`closing_minute`. It contains
the `weekday`, `hour` and `minute` in the same ranges as :attr:`datetime.datetime.weekday`,
:attr:`datetime.datetime.hour` and :attr:`datetime.datetime.minute`
Returns:
- Tuple[:obj:`int`, :obj:`int`, :obj:`int`]:
+ tuple[:obj:`int`, :obj:`int`, :obj:`int`]:
"""
if self._closing_time is None:
self._closing_time = self._parse_minute(self.closing_minute)
@@ -439,17 +419,12 @@ def __init__(
self._freeze()
@classmethod
- def de_json(
- cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
- ) -> Optional["BusinessOpeningHours"]:
+ def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "BusinessOpeningHours":
"""See :meth:`telegram.TelegramObject.de_json`."""
data = cls._parse_data(data)
- if not data:
- return None
-
- data["opening_hours"] = BusinessOpeningHoursInterval.de_list(
- data.get("opening_hours"), bot
+ data["opening_hours"] = de_list_optional(
+ data.get("opening_hours"), BusinessOpeningHoursInterval, bot
)
return super().de_json(data=data, bot=bot)
diff --git a/telegram/_callbackquery.py b/telegram/_callbackquery.py
index bdfa569dbfd..99b4ad115b5 100644
--- a/telegram/_callbackquery.py
+++ b/telegram/_callbackquery.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2024
+# Copyright (C) 2015-2025
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -18,15 +18,17 @@
# along with this program. If not, see [http://www.gnu.org/licenses/].
# pylint: disable=redefined-builtin
"""This module contains an object that represents a Telegram CallbackQuery"""
-from typing import TYPE_CHECKING, Final, Optional, Sequence, Tuple, Union
+from collections.abc import Sequence
+from typing import TYPE_CHECKING, Final, Optional, Union
from telegram import constants
from telegram._files.location import Location
from telegram._message import MaybeInaccessibleMessage, Message
from telegram._telegramobject import TelegramObject
from telegram._user import User
+from telegram._utils.argumentparsing import de_json_optional
from telegram._utils.defaultvalue import DEFAULT_NONE
-from telegram._utils.types import JSONDict, ODVInput, ReplyMarkup
+from telegram._utils.types import JSONDict, ODVInput, ReplyMarkup, TimePeriod
if TYPE_CHECKING:
from telegram import (
@@ -148,17 +150,12 @@ def __init__(
self._freeze()
@classmethod
- def de_json(
- cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
- ) -> Optional["CallbackQuery"]:
+ def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "CallbackQuery":
"""See :meth:`telegram.TelegramObject.de_json`."""
data = cls._parse_data(data)
- if not data:
- return None
-
- data["from_user"] = User.de_json(data.pop("from", None), bot)
- data["message"] = Message.de_json(data.get("message"), bot)
+ data["from_user"] = de_json_optional(data.pop("from", None), User, bot)
+ data["message"] = de_json_optional(data.get("message"), Message, bot)
return super().de_json(data=data, bot=bot)
@@ -167,7 +164,7 @@ async def answer(
text: Optional[str] = None,
show_alert: Optional[bool] = None,
url: Optional[str] = None,
- cache_time: Optional[int] = None,
+ cache_time: Optional[TimePeriod] = None,
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -474,7 +471,7 @@ async def edit_message_live_location(
horizontal_accuracy: Optional[float] = None,
heading: Optional[int] = None,
proximity_alert_radius: Optional[int] = None,
- live_period: Optional[int] = None,
+ live_period: Optional[TimePeriod] = None,
*,
location: Optional[Location] = None,
read_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -676,7 +673,7 @@ async def get_game_high_scores(
connect_timeout: ODVInput[float] = DEFAULT_NONE,
pool_timeout: ODVInput[float] = DEFAULT_NONE,
api_kwargs: Optional[JSONDict] = None,
- ) -> Tuple["GameHighScore", ...]:
+ ) -> tuple["GameHighScore", ...]:
"""Shortcut for either::
await update.callback_query.message.get_game_high_score(*args, **kwargs)
@@ -695,7 +692,7 @@ async def get_game_high_scores(
Raises :exc:`TypeError` if :attr:`message` is not accessible.
Returns:
- Tuple[:class:`telegram.GameHighScore`]
+ tuple[:class:`telegram.GameHighScore`]
Raises:
:exc:`TypeError` if :attr:`message` is not accessible.
@@ -833,6 +830,8 @@ async def copy_message(
message_thread_id: Optional[int] = None,
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,
@@ -866,6 +865,7 @@ async def copy_message(
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,
@@ -880,6 +880,7 @@ async def copy_message(
message_thread_id=message_thread_id,
reply_parameters=reply_parameters,
show_caption_above_media=show_caption_above_media,
+ allow_paid_broadcast=allow_paid_broadcast,
)
MAX_ANSWER_TEXT_LENGTH: Final[int] = (
diff --git a/telegram/_chat.py b/telegram/_chat.py
index 200e192c95f..fe49dc3593e 100644
--- a/telegram/_chat.py
+++ b/telegram/_chat.py
@@ -2,7 +2,7 @@
# pylint: disable=redefined-builtin
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2024
+# Copyright (C) 2015-2025
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -18,9 +18,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/].
"""This module contains an object that represents a Telegram Chat."""
-from datetime import datetime
+import datetime as dtm
+from collections.abc import Sequence
from html import escape
-from typing import TYPE_CHECKING, Final, Optional, Sequence, Tuple, Union
+from typing import TYPE_CHECKING, Final, Optional, Union
from telegram import constants
from telegram._chatpermissions import ChatPermissions
@@ -30,7 +31,14 @@
from telegram._telegramobject import TelegramObject
from telegram._utils import enum
from telegram._utils.defaultvalue import DEFAULT_NONE
-from telegram._utils.types import CorrectOptionID, FileInput, JSONDict, ODVInput, ReplyMarkup
+from telegram._utils.types import (
+ CorrectOptionID,
+ FileInput,
+ JSONDict,
+ ODVInput,
+ ReplyMarkup,
+ TimePeriod,
+)
from telegram.helpers import escape_markdown
from telegram.helpers import mention_html as helpers_mention_html
from telegram.helpers import mention_markdown as helpers_mention_markdown
@@ -43,6 +51,7 @@
ChatMember,
Contact,
Document,
+ Gift,
InlineKeyboardMarkup,
InputMediaAudio,
InputMediaDocument,
@@ -296,7 +305,7 @@ async def get_administrators(
connect_timeout: ODVInput[float] = DEFAULT_NONE,
pool_timeout: ODVInput[float] = DEFAULT_NONE,
api_kwargs: Optional[JSONDict] = None,
- ) -> Tuple["ChatMember", ...]:
+ ) -> tuple["ChatMember", ...]:
"""Shortcut for::
await bot.get_chat_administrators(update.effective_chat.id, *args, **kwargs)
@@ -305,7 +314,7 @@ async def get_administrators(
:meth:`telegram.Bot.get_chat_administrators`.
Returns:
- Tuple[:class:`telegram.ChatMember`]: A tuple of administrators in a chat. An Array of
+ tuple[:class:`telegram.ChatMember`]: A tuple of administrators in a chat. An Array of
:class:`telegram.ChatMember` objects that contains information about all
chat administrators except other bots. If the chat is a group or a supergroup
and no administrators were appointed, only the creator will be returned.
@@ -382,7 +391,7 @@ async def ban_member(
self,
user_id: int,
revoke_messages: Optional[bool] = None,
- until_date: Optional[Union[int, datetime]] = None,
+ until_date: Optional[Union[int, dtm.datetime]] = None,
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -654,7 +663,7 @@ async def restrict_member(
self,
user_id: int,
permissions: ChatPermissions,
- until_date: Optional[Union[int, datetime]] = None,
+ until_date: Optional[Union[int, dtm.datetime]] = None,
use_independent_chat_permissions: Optional[bool] = None,
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -905,6 +914,7 @@ async def pin_message(
self,
message_id: int,
disable_notification: ODVInput[bool] = DEFAULT_NONE,
+ business_connection_id: Optional[str] = None,
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -932,11 +942,13 @@ async def pin_message(
connect_timeout=connect_timeout,
pool_timeout=pool_timeout,
api_kwargs=api_kwargs,
+ business_connection_id=business_connection_id,
)
async def unpin_message(
self,
message_id: Optional[int] = None,
+ business_connection_id: Optional[str] = None,
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -963,6 +975,7 @@ async def unpin_message(
pool_timeout=pool_timeout,
api_kwargs=api_kwargs,
message_id=message_id,
+ business_connection_id=business_connection_id,
)
async def unpin_all_messages(
@@ -1007,6 +1020,7 @@ async def send_message(
reply_parameters: Optional["ReplyParameters"] = None,
business_connection_id: Optional[str] = None,
message_effect_id: Optional[str] = None,
+ allow_paid_broadcast: Optional[bool] = None,
*,
reply_to_message_id: Optional[int] = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
@@ -1048,6 +1062,7 @@ async def send_message(
api_kwargs=api_kwargs,
business_connection_id=business_connection_id,
message_effect_id=message_effect_id,
+ allow_paid_broadcast=allow_paid_broadcast,
)
async def delete_message(
@@ -1125,6 +1140,7 @@ async def send_media_group(
reply_parameters: Optional["ReplyParameters"] = None,
business_connection_id: Optional[str] = None,
message_effect_id: Optional[str] = None,
+ allow_paid_broadcast: Optional[bool] = None,
*,
reply_to_message_id: Optional[int] = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
@@ -1136,7 +1152,7 @@ async def send_media_group(
caption: Optional[str] = None,
parse_mode: ODVInput[str] = DEFAULT_NONE,
caption_entities: Optional[Sequence["MessageEntity"]] = None,
- ) -> Tuple["Message", ...]:
+ ) -> tuple["Message", ...]:
"""Shortcut for::
await bot.send_media_group(update.effective_chat.id, *args, **kwargs)
@@ -1144,7 +1160,7 @@ async def send_media_group(
For the documentation of the arguments, please see :meth:`telegram.Bot.send_media_group`.
Returns:
- Tuple[:class:`telegram.Message`]: On success, a tuple of :class:`~telegram.Message`
+ tuple[:class:`telegram.Message`]: On success, a tuple of :class:`~telegram.Message`
instances that were sent is returned.
"""
@@ -1167,6 +1183,7 @@ async def send_media_group(
reply_parameters=reply_parameters,
business_connection_id=business_connection_id,
message_effect_id=message_effect_id,
+ allow_paid_broadcast=allow_paid_broadcast,
)
async def send_chat_action(
@@ -1220,6 +1237,7 @@ async def send_photo(
reply_parameters: Optional["ReplyParameters"] = None,
business_connection_id: Optional[str] = None,
message_effect_id: Optional[str] = None,
+ allow_paid_broadcast: Optional[bool] = None,
show_caption_above_media: Optional[bool] = None,
*,
reply_to_message_id: Optional[int] = None,
@@ -1263,6 +1281,7 @@ async def send_photo(
has_spoiler=has_spoiler,
business_connection_id=business_connection_id,
message_effect_id=message_effect_id,
+ allow_paid_broadcast=allow_paid_broadcast,
show_caption_above_media=show_caption_above_media,
)
@@ -1279,6 +1298,7 @@ async def send_contact(
reply_parameters: Optional["ReplyParameters"] = None,
business_connection_id: Optional[str] = None,
message_effect_id: Optional[str] = None,
+ allow_paid_broadcast: Optional[bool] = None,
*,
reply_to_message_id: Optional[int] = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
@@ -1320,12 +1340,13 @@ async def send_contact(
message_thread_id=message_thread_id,
business_connection_id=business_connection_id,
message_effect_id=message_effect_id,
+ allow_paid_broadcast=allow_paid_broadcast,
)
async def send_audio(
self,
audio: Union[FileInput, "Audio"],
- duration: Optional[int] = None,
+ duration: Optional[TimePeriod] = None,
performer: Optional[str] = None,
title: Optional[str] = None,
caption: Optional[str] = None,
@@ -1339,6 +1360,7 @@ async def send_audio(
reply_parameters: Optional["ReplyParameters"] = None,
business_connection_id: Optional[str] = None,
message_effect_id: Optional[str] = None,
+ allow_paid_broadcast: Optional[bool] = None,
*,
reply_to_message_id: Optional[int] = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
@@ -1384,6 +1406,7 @@ async def send_audio(
thumbnail=thumbnail,
business_connection_id=business_connection_id,
message_effect_id=message_effect_id,
+ allow_paid_broadcast=allow_paid_broadcast,
)
async def send_document(
@@ -1401,6 +1424,7 @@ async def send_document(
reply_parameters: Optional["ReplyParameters"] = None,
business_connection_id: Optional[str] = None,
message_effect_id: Optional[str] = None,
+ allow_paid_broadcast: Optional[bool] = None,
*,
reply_to_message_id: Optional[int] = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
@@ -1444,6 +1468,7 @@ async def send_document(
message_thread_id=message_thread_id,
business_connection_id=business_connection_id,
message_effect_id=message_effect_id,
+ allow_paid_broadcast=allow_paid_broadcast,
)
async def send_dice(
@@ -1456,6 +1481,7 @@ async def send_dice(
reply_parameters: Optional["ReplyParameters"] = None,
business_connection_id: Optional[str] = None,
message_effect_id: Optional[str] = None,
+ allow_paid_broadcast: Optional[bool] = None,
*,
reply_to_message_id: Optional[int] = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
@@ -1492,6 +1518,7 @@ async def send_dice(
message_thread_id=message_thread_id,
business_connection_id=business_connection_id,
message_effect_id=message_effect_id,
+ allow_paid_broadcast=allow_paid_broadcast,
)
async def send_game(
@@ -1504,6 +1531,7 @@ async def send_game(
reply_parameters: Optional["ReplyParameters"] = None,
business_connection_id: Optional[str] = None,
message_effect_id: Optional[str] = None,
+ allow_paid_broadcast: Optional[bool] = None,
*,
reply_to_message_id: Optional[int] = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
@@ -1540,6 +1568,7 @@ async def send_game(
message_thread_id=message_thread_id,
business_connection_id=business_connection_id,
message_effect_id=message_effect_id,
+ allow_paid_broadcast=allow_paid_broadcast,
)
async def send_invoice(
@@ -1547,9 +1576,9 @@ async def send_invoice(
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,
@@ -1571,6 +1600,7 @@ async def send_invoice(
message_thread_id: Optional[int] = None,
reply_parameters: Optional["ReplyParameters"] = None,
message_effect_id: Optional[str] = None,
+ allow_paid_broadcast: Optional[bool] = None,
*,
reply_to_message_id: Optional[int] = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
@@ -1636,6 +1666,7 @@ async def send_invoice(
message_thread_id=message_thread_id,
reply_parameters=reply_parameters,
message_effect_id=message_effect_id,
+ allow_paid_broadcast=allow_paid_broadcast,
)
async def send_location(
@@ -1644,7 +1675,7 @@ async def send_location(
longitude: Optional[float] = None,
disable_notification: ODVInput[bool] = DEFAULT_NONE,
reply_markup: Optional[ReplyMarkup] = None,
- live_period: Optional[int] = None,
+ live_period: Optional[TimePeriod] = None,
horizontal_accuracy: Optional[float] = None,
heading: Optional[int] = None,
proximity_alert_radius: Optional[int] = None,
@@ -1653,6 +1684,7 @@ async def send_location(
reply_parameters: Optional["ReplyParameters"] = None,
business_connection_id: Optional[str] = None,
message_effect_id: Optional[str] = None,
+ allow_paid_broadcast: Optional[bool] = None,
*,
reply_to_message_id: Optional[int] = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
@@ -1696,12 +1728,13 @@ async def send_location(
message_thread_id=message_thread_id,
business_connection_id=business_connection_id,
message_effect_id=message_effect_id,
+ allow_paid_broadcast=allow_paid_broadcast,
)
async def send_animation(
self,
animation: Union[FileInput, "Animation"],
- duration: Optional[int] = None,
+ duration: Optional[TimePeriod] = None,
width: Optional[int] = None,
height: Optional[int] = None,
caption: Optional[str] = None,
@@ -1716,6 +1749,7 @@ async def send_animation(
reply_parameters: Optional["ReplyParameters"] = None,
business_connection_id: Optional[str] = None,
message_effect_id: Optional[str] = None,
+ allow_paid_broadcast: Optional[bool] = None,
show_caption_above_media: Optional[bool] = None,
*,
reply_to_message_id: Optional[int] = None,
@@ -1763,6 +1797,7 @@ async def send_animation(
thumbnail=thumbnail,
business_connection_id=business_connection_id,
message_effect_id=message_effect_id,
+ allow_paid_broadcast=allow_paid_broadcast,
show_caption_above_media=show_caption_above_media,
)
@@ -1777,6 +1812,7 @@ async def send_sticker(
reply_parameters: Optional["ReplyParameters"] = None,
business_connection_id: Optional[str] = None,
message_effect_id: Optional[str] = None,
+ allow_paid_broadcast: Optional[bool] = None,
*,
reply_to_message_id: Optional[int] = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
@@ -1814,6 +1850,7 @@ async def send_sticker(
emoji=emoji,
business_connection_id=business_connection_id,
message_effect_id=message_effect_id,
+ allow_paid_broadcast=allow_paid_broadcast,
)
async def send_venue(
@@ -1833,6 +1870,7 @@ async def send_venue(
reply_parameters: Optional["ReplyParameters"] = None,
business_connection_id: Optional[str] = None,
message_effect_id: Optional[str] = None,
+ allow_paid_broadcast: Optional[bool] = None,
*,
reply_to_message_id: Optional[int] = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
@@ -1878,12 +1916,13 @@ async def send_venue(
message_thread_id=message_thread_id,
business_connection_id=business_connection_id,
message_effect_id=message_effect_id,
+ allow_paid_broadcast=allow_paid_broadcast,
)
async def send_video(
self,
video: Union[FileInput, "Video"],
- duration: Optional[int] = None,
+ duration: Optional[TimePeriod] = None,
caption: Optional[str] = None,
disable_notification: ODVInput[bool] = DEFAULT_NONE,
reply_markup: Optional[ReplyMarkup] = None,
@@ -1899,7 +1938,10 @@ async def send_video(
reply_parameters: Optional["ReplyParameters"] = None,
business_connection_id: Optional[str] = None,
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,
@@ -1938,6 +1980,8 @@ async def send_video(
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,
@@ -1947,13 +1991,14 @@ async def send_video(
has_spoiler=has_spoiler,
business_connection_id=business_connection_id,
message_effect_id=message_effect_id,
+ allow_paid_broadcast=allow_paid_broadcast,
show_caption_above_media=show_caption_above_media,
)
async def send_video_note(
self,
video_note: Union[FileInput, "VideoNote"],
- duration: Optional[int] = None,
+ duration: Optional[TimePeriod] = None,
length: Optional[int] = None,
disable_notification: ODVInput[bool] = DEFAULT_NONE,
reply_markup: Optional[ReplyMarkup] = None,
@@ -1963,6 +2008,7 @@ async def send_video_note(
reply_parameters: Optional["ReplyParameters"] = None,
business_connection_id: Optional[str] = None,
message_effect_id: Optional[str] = None,
+ allow_paid_broadcast: Optional[bool] = None,
*,
reply_to_message_id: Optional[int] = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
@@ -2004,12 +2050,13 @@ async def send_video_note(
message_thread_id=message_thread_id,
business_connection_id=business_connection_id,
message_effect_id=message_effect_id,
+ allow_paid_broadcast=allow_paid_broadcast,
)
async def send_voice(
self,
voice: Union[FileInput, "Voice"],
- duration: Optional[int] = None,
+ duration: Optional[TimePeriod] = None,
caption: Optional[str] = None,
disable_notification: ODVInput[bool] = DEFAULT_NONE,
reply_markup: Optional[ReplyMarkup] = None,
@@ -2020,6 +2067,7 @@ async def send_voice(
reply_parameters: Optional["ReplyParameters"] = None,
business_connection_id: Optional[str] = None,
message_effect_id: Optional[str] = None,
+ allow_paid_broadcast: Optional[bool] = None,
*,
reply_to_message_id: Optional[int] = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
@@ -2062,6 +2110,7 @@ async def send_voice(
message_thread_id=message_thread_id,
business_connection_id=business_connection_id,
message_effect_id=message_effect_id,
+ allow_paid_broadcast=allow_paid_broadcast,
)
async def send_poll(
@@ -2077,8 +2126,8 @@ async def send_poll(
reply_markup: Optional[ReplyMarkup] = None,
explanation: Optional[str] = None,
explanation_parse_mode: ODVInput[str] = DEFAULT_NONE,
- open_period: Optional[int] = None,
- close_date: Optional[Union[int, datetime]] = None,
+ open_period: Optional[TimePeriod] = None,
+ close_date: Optional[Union[int, dtm.datetime]] = None,
explanation_entities: Optional[Sequence["MessageEntity"]] = None,
protect_content: ODVInput[bool] = DEFAULT_NONE,
message_thread_id: Optional[int] = None,
@@ -2087,6 +2136,7 @@ async def send_poll(
question_parse_mode: ODVInput[str] = DEFAULT_NONE,
question_entities: Optional[Sequence["MessageEntity"]] = None,
message_effect_id: Optional[str] = None,
+ allow_paid_broadcast: Optional[bool] = None,
*,
reply_to_message_id: Optional[int] = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
@@ -2124,6 +2174,7 @@ async def send_poll(
connect_timeout=connect_timeout,
pool_timeout=pool_timeout,
message_effect_id=message_effect_id,
+ allow_paid_broadcast=allow_paid_broadcast,
explanation=explanation,
explanation_parse_mode=explanation_parse_mode,
open_period=open_period,
@@ -2151,6 +2202,8 @@ async def send_copy(
message_thread_id: Optional[int] = None,
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,
@@ -2177,6 +2230,7 @@ async def send_copy(
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,
@@ -2192,6 +2246,7 @@ async def send_copy(
protect_content=protect_content,
message_thread_id=message_thread_id,
show_caption_above_media=show_caption_above_media,
+ allow_paid_broadcast=allow_paid_broadcast,
)
async def copy_message(
@@ -2207,6 +2262,8 @@ async def copy_message(
message_thread_id: Optional[int] = None,
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,
@@ -2233,6 +2290,7 @@ async def copy_message(
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,
@@ -2248,6 +2306,7 @@ async def copy_message(
protect_content=protect_content,
message_thread_id=message_thread_id,
show_caption_above_media=show_caption_above_media,
+ allow_paid_broadcast=allow_paid_broadcast,
)
async def send_copies(
@@ -2264,7 +2323,7 @@ async def send_copies(
connect_timeout: ODVInput[float] = DEFAULT_NONE,
pool_timeout: ODVInput[float] = DEFAULT_NONE,
api_kwargs: Optional[JSONDict] = None,
- ) -> Tuple["MessageId", ...]:
+ ) -> tuple["MessageId", ...]:
"""Shortcut for::
await bot.copy_messages(chat_id=update.effective_chat.id, *args, **kwargs)
@@ -2276,7 +2335,7 @@ async def send_copies(
.. versionadded:: 20.8
Returns:
- Tuple[:class:`telegram.MessageId`]: On success, a tuple of :class:`~telegram.MessageId`
+ tuple[:class:`telegram.MessageId`]: On success, a tuple of :class:`~telegram.MessageId`
of the sent messages is returned.
"""
@@ -2309,7 +2368,7 @@ async def copy_messages(
connect_timeout: ODVInput[float] = DEFAULT_NONE,
pool_timeout: ODVInput[float] = DEFAULT_NONE,
api_kwargs: Optional[JSONDict] = None,
- ) -> Tuple["MessageId", ...]:
+ ) -> tuple["MessageId", ...]:
"""Shortcut for::
await bot.copy_messages(from_chat_id=update.effective_chat.id, *args, **kwargs)
@@ -2321,7 +2380,7 @@ async def copy_messages(
.. versionadded:: 20.8
Returns:
- Tuple[:class:`telegram.MessageId`]: On success, a tuple of :class:`~telegram.MessageId`
+ tuple[:class:`telegram.MessageId`]: On success, a tuple of :class:`~telegram.MessageId`
of the sent messages is returned.
"""
@@ -2347,6 +2406,7 @@ async def forward_from(
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,
@@ -2372,6 +2432,7 @@ async def forward_from(
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,
@@ -2389,6 +2450,7 @@ async def forward_to(
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,
@@ -2415,6 +2477,7 @@ async def forward_to(
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,
@@ -2438,7 +2501,7 @@ async def forward_messages_from(
connect_timeout: ODVInput[float] = DEFAULT_NONE,
pool_timeout: ODVInput[float] = DEFAULT_NONE,
api_kwargs: Optional[JSONDict] = None,
- ) -> Tuple["MessageId", ...]:
+ ) -> tuple["MessageId", ...]:
"""Shortcut for::
await bot.forward_messages(chat_id=update.effective_chat.id, *args, **kwargs)
@@ -2450,7 +2513,7 @@ async def forward_messages_from(
.. versionadded:: 20.8
Returns:
- Tuple[:class:`telegram.MessageId`]: On success, a tuple of :class:`~telegram.MessageId`
+ tuple[:class:`telegram.MessageId`]: On success, a tuple of :class:`~telegram.MessageId`
of sent messages is returned.
"""
@@ -2481,7 +2544,7 @@ async def forward_messages_to(
connect_timeout: ODVInput[float] = DEFAULT_NONE,
pool_timeout: ODVInput[float] = DEFAULT_NONE,
api_kwargs: Optional[JSONDict] = None,
- ) -> Tuple["MessageId", ...]:
+ ) -> tuple["MessageId", ...]:
"""Shortcut for::
await bot.forward_messages(from_chat_id=update.effective_chat.id, *args, **kwargs)
@@ -2493,7 +2556,7 @@ async def forward_messages_to(
.. versionadded:: 20.8
Returns:
- Tuple[:class:`telegram.MessageId`]: On success, a tuple of :class:`~telegram.MessageId`
+ tuple[:class:`telegram.MessageId`]: On success, a tuple of :class:`~telegram.MessageId`
of sent messages is returned.
"""
@@ -2544,7 +2607,7 @@ async def export_invite_link(
async def create_invite_link(
self,
- expire_date: Optional[Union[int, datetime]] = None,
+ expire_date: Optional[Union[int, dtm.datetime]] = None,
member_limit: Optional[int] = None,
name: Optional[str] = None,
creates_join_request: Optional[bool] = None,
@@ -2588,7 +2651,7 @@ async def create_invite_link(
async def edit_invite_link(
self,
invite_link: Union[str, "ChatInviteLink"],
- expire_date: Optional[Union[int, datetime]] = None,
+ expire_date: Optional[Union[int, dtm.datetime]] = None,
member_limit: Optional[int] = None,
name: Optional[str] = None,
creates_join_request: Optional[bool] = None,
@@ -2662,6 +2725,81 @@ async def revoke_invite_link(
api_kwargs=api_kwargs,
)
+ async def create_subscription_invite_link(
+ self,
+ subscription_period: TimePeriod,
+ subscription_price: int,
+ name: Optional[str] = None,
+ *,
+ read_timeout: ODVInput[float] = DEFAULT_NONE,
+ write_timeout: ODVInput[float] = DEFAULT_NONE,
+ connect_timeout: ODVInput[float] = DEFAULT_NONE,
+ pool_timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: Optional[JSONDict] = None,
+ ) -> "ChatInviteLink":
+ """Shortcut for::
+
+ await bot.create_chat_subscription_invite_link(
+ chat_id=update.effective_chat.id, *args, **kwargs
+ )
+
+ For the documentation of the arguments, please see
+ :meth:`telegram.Bot.create_chat_subscription_invite_link`.
+
+ .. versionadded:: 21.5
+
+ Returns:
+ :class:`telegram.ChatInviteLink`
+ """
+ return await self.get_bot().create_chat_subscription_invite_link(
+ chat_id=self.id,
+ subscription_period=subscription_period,
+ subscription_price=subscription_price,
+ name=name,
+ read_timeout=read_timeout,
+ write_timeout=write_timeout,
+ connect_timeout=connect_timeout,
+ pool_timeout=pool_timeout,
+ api_kwargs=api_kwargs,
+ )
+
+ async def edit_subscription_invite_link(
+ self,
+ invite_link: Union[str, "ChatInviteLink"],
+ name: Optional[str] = None,
+ *,
+ read_timeout: ODVInput[float] = DEFAULT_NONE,
+ write_timeout: ODVInput[float] = DEFAULT_NONE,
+ connect_timeout: ODVInput[float] = DEFAULT_NONE,
+ pool_timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: Optional[JSONDict] = None,
+ ) -> "ChatInviteLink":
+ """Shortcut for::
+
+ await bot.edit_chat_subscription_invite_link(
+ chat_id=update.effective_chat.id, *args, **kwargs
+ )
+
+ For the documentation of the arguments, please see
+ :meth:`telegram.Bot.edit_chat_subscription_invite_link`.
+
+ .. versionadded:: 21.5
+
+ Returns:
+ :class:`telegram.ChatInviteLink`
+
+ """
+ return await self.get_bot().edit_chat_subscription_invite_link(
+ chat_id=self.id,
+ invite_link=invite_link,
+ read_timeout=read_timeout,
+ write_timeout=write_timeout,
+ connect_timeout=connect_timeout,
+ pool_timeout=pool_timeout,
+ api_kwargs=api_kwargs,
+ name=name,
+ )
+
async def approve_join_request(
self,
user_id: int,
@@ -3270,6 +3408,9 @@ async def send_paid_media(
protect_content: ODVInput[bool] = DEFAULT_NONE,
reply_parameters: Optional["ReplyParameters"] = None,
reply_markup: Optional[ReplyMarkup] = None,
+ business_connection_id: Optional[str] = None,
+ payload: Optional[str] = None,
+ allow_paid_broadcast: Optional[bool] = None,
*,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
reply_to_message_id: Optional[int] = None,
@@ -3310,6 +3451,121 @@ async def send_paid_media(
connect_timeout=connect_timeout,
pool_timeout=pool_timeout,
api_kwargs=api_kwargs,
+ business_connection_id=business_connection_id,
+ payload=payload,
+ allow_paid_broadcast=allow_paid_broadcast,
+ )
+
+ async def send_gift(
+ self,
+ 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,
+ *,
+ read_timeout: ODVInput[float] = DEFAULT_NONE,
+ write_timeout: ODVInput[float] = DEFAULT_NONE,
+ connect_timeout: ODVInput[float] = DEFAULT_NONE,
+ pool_timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: Optional[JSONDict] = None,
+ ) -> bool:
+ """Shortcut for::
+
+ 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`.
+
+ .. 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(
+ gift_id=gift_id,
+ text=text,
+ text_parse_mode=text_parse_mode,
+ text_entities=text_entities,
+ pay_for_upgrade=pay_for_upgrade,
+ read_timeout=read_timeout,
+ write_timeout=write_timeout,
+ 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(
+ self,
+ custom_description: Optional[str] = None,
+ *,
+ read_timeout: ODVInput[float] = DEFAULT_NONE,
+ write_timeout: ODVInput[float] = DEFAULT_NONE,
+ connect_timeout: ODVInput[float] = DEFAULT_NONE,
+ pool_timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: Optional[JSONDict] = None,
+ ) -> bool:
+ """Shortcut for::
+
+ await bot.verify_chat(chat_id=update.effective_chat.id, *args, **kwargs)
+
+ For the documentation of the arguments, please see
+ :meth:`telegram.Bot.verify_chat`.
+
+ .. versionadded:: 21.10
+
+ Returns:
+ :obj:`bool`: On success, :obj:`True` is returned.
+ """
+ return await self.get_bot().verify_chat(
+ chat_id=self.id,
+ custom_description=custom_description,
+ read_timeout=read_timeout,
+ write_timeout=write_timeout,
+ connect_timeout=connect_timeout,
+ pool_timeout=pool_timeout,
+ api_kwargs=api_kwargs,
+ )
+
+ async def remove_verification(
+ self,
+ *,
+ read_timeout: ODVInput[float] = DEFAULT_NONE,
+ write_timeout: ODVInput[float] = DEFAULT_NONE,
+ connect_timeout: ODVInput[float] = DEFAULT_NONE,
+ pool_timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: Optional[JSONDict] = None,
+ ) -> bool:
+ """Shortcut for::
+
+ await bot.remove_chat_verification(chat_id=update.effective_chat.id, *args, **kwargs)
+
+ For the documentation of the arguments, please see
+ :meth:`telegram.Bot.remove_chat_verification`.
+
+ .. versionadded:: 21.10
+
+ Returns:
+ :obj:`bool`: On success, :obj:`True` is returned.
+ """
+ return await self.get_bot().remove_chat_verification(
+ chat_id=self.id,
+ read_timeout=read_timeout,
+ write_timeout=write_timeout,
+ connect_timeout=connect_timeout,
+ pool_timeout=pool_timeout,
+ api_kwargs=api_kwargs,
)
diff --git a/telegram/_chatadministratorrights.py b/telegram/_chatadministratorrights.py
index f0d0b033f62..6b6c43715eb 100644
--- a/telegram/_chatadministratorrights.py
+++ b/telegram/_chatadministratorrights.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2024
+# Copyright (C) 2015-2025
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
diff --git a/telegram/_chatbackground.py b/telegram/_chatbackground.py
index f9c77619f4c..a4bbf5b0836 100644
--- a/telegram/_chatbackground.py
+++ b/telegram/_chatbackground.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2024
+# Copyright (C) 2015-2025
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -17,13 +17,14 @@
# 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 objects related to chat backgrounds."""
-from typing import TYPE_CHECKING, Dict, Final, Optional, Sequence, Tuple, Type
+from collections.abc import Sequence
+from typing import TYPE_CHECKING, Final, Optional
from telegram import constants
from telegram._files.document import Document
from telegram._telegramobject import TelegramObject
from telegram._utils import enum
-from telegram._utils.argumentparsing import parse_sequence_arg
+from telegram._utils.argumentparsing import de_json_optional, parse_sequence_arg
from telegram._utils.types import JSONDict
if TYPE_CHECKING:
@@ -78,16 +79,11 @@ def __init__(
self._freeze()
@classmethod
- def de_json(
- cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
- ) -> Optional["BackgroundFill"]:
+ def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "BackgroundFill":
"""See :meth:`telegram.TelegramObject.de_json`."""
data = cls._parse_data(data)
- if not data:
- return None
-
- _class_mapping: Dict[str, Type[BackgroundFill]] = {
+ _class_mapping: dict[str, type[BackgroundFill]] = {
cls.SOLID: BackgroundFillSolid,
cls.GRADIENT: BackgroundFillGradient,
cls.FREEFORM_GRADIENT: BackgroundFillFreeformGradient,
@@ -212,7 +208,7 @@ def __init__(
super().__init__(type=self.FREEFORM_GRADIENT, api_kwargs=api_kwargs)
with self._unfrozen():
- self.colors: Tuple[int, ...] = parse_sequence_arg(colors)
+ self.colors: tuple[int, ...] = parse_sequence_arg(colors)
self._id_attrs = (self.colors,)
@@ -269,16 +265,11 @@ def __init__(
self._freeze()
@classmethod
- def de_json(
- cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
- ) -> Optional["BackgroundType"]:
+ def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "BackgroundType":
"""See :meth:`telegram.TelegramObject.de_json`."""
data = cls._parse_data(data)
- if not data:
- return None
-
- _class_mapping: Dict[str, Type[BackgroundType]] = {
+ _class_mapping: dict[str, type[BackgroundType]] = {
cls.FILL: BackgroundTypeFill,
cls.WALLPAPER: BackgroundTypeWallpaper,
cls.PATTERN: BackgroundTypePattern,
@@ -289,10 +280,10 @@ def de_json(
return _class_mapping[data.pop("type")].de_json(data=data, bot=bot)
if "fill" in data:
- data["fill"] = BackgroundFill.de_json(data.get("fill"), bot)
+ data["fill"] = de_json_optional(data.get("fill"), BackgroundFill, bot)
if "document" in data:
- data["document"] = Document.de_json(data.get("document"), bot)
+ data["document"] = de_json_optional(data.get("document"), Document, bot)
return super().de_json(data=data, bot=bot)
@@ -307,7 +298,7 @@ class BackgroundTypeFill(BackgroundType):
.. versionadded:: 21.2
Args:
- fill (:obj:`telegram.BackgroundFill`): The background fill.
+ fill (:class:`telegram.BackgroundFill`): The background fill.
dark_theme_dimming (:obj:`int`): Dimming of the background in dark themes, as a
percentage;
0-:tg-const:`telegram.constants.BackgroundTypeLimit.MAX_DIMMING`.
@@ -315,7 +306,7 @@ class BackgroundTypeFill(BackgroundType):
Attributes:
type (:obj:`str`): Type of the background. Always
:attr:`~telegram.BackgroundType.FILL`.
- fill (:obj:`telegram.BackgroundFill`): The background fill.
+ fill (:class:`telegram.BackgroundFill`): The background fill.
dark_theme_dimming (:obj:`int`): Dimming of the background in dark themes, as a
percentage;
0-:tg-const:`telegram.constants.BackgroundTypeLimit.MAX_DIMMING`.
@@ -349,7 +340,7 @@ class BackgroundTypeWallpaper(BackgroundType):
.. versionadded:: 21.2
Args:
- document (:obj:`telegram.Document`): Document with the wallpaper
+ document (:class:`telegram.Document`): Document with the wallpaper
dark_theme_dimming (:obj:`int`): Dimming of the background in dark themes, as a
percentage;
0-:tg-const:`telegram.constants.BackgroundTypeLimit.MAX_DIMMING`.
@@ -361,7 +352,7 @@ class BackgroundTypeWallpaper(BackgroundType):
Attributes:
type (:obj:`str`): Type of the background. Always
:attr:`~telegram.BackgroundType.WALLPAPER`.
- document (:obj:`telegram.Document`): Document with the wallpaper
+ document (:class:`telegram.Document`): Document with the wallpaper
dark_theme_dimming (:obj:`int`): Dimming of the background in dark themes, as a
percentage;
0-:tg-const:`telegram.constants.BackgroundTypeLimit.MAX_DIMMING`.
@@ -397,8 +388,8 @@ def __init__(
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
@@ -407,8 +398,8 @@ class BackgroundTypePattern(BackgroundType):
.. versionadded:: 21.2
Args:
- document (:obj:`telegram.Document`): Document with the pattern.
- fill (:obj:`telegram.BackgroundFill`): The background fill that is combined with
+ document (:class:`telegram.Document`): Document with the pattern.
+ fill (:class:`telegram.BackgroundFill`): The background fill that is combined with
the pattern.
intensity (:obj:`int`): Intensity of the pattern when it is shown above the filled
background;
@@ -422,8 +413,8 @@ class BackgroundTypePattern(BackgroundType):
Attributes:
type (:obj:`str`): Type of the background. Always
:attr:`~telegram.BackgroundType.PATTERN`.
- document (:obj:`telegram.Document`): Document with the pattern.
- fill (:obj:`telegram.BackgroundFill`): The background fill that is combined with
+ document (:class:`telegram.Document`): Document with the pattern.
+ fill (:class:`telegram.BackgroundFill`): The background fill that is combined with
the pattern.
intensity (:obj:`int`): Intensity of the pattern when it is shown above the filled
background;
@@ -511,10 +502,10 @@ class ChatBackground(TelegramObject):
.. versionadded:: 21.2
Args:
- type (:obj:`telegram.BackgroundType`): Type of the background.
+ type (:class:`telegram.BackgroundType`): Type of the background.
Attributes:
- type (:obj:`telegram.BackgroundType`): Type of the background.
+ type (:class:`telegram.BackgroundType`): Type of the background.
"""
__slots__ = ("type",)
@@ -532,15 +523,10 @@ def __init__(
self._freeze()
@classmethod
- def de_json(
- cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
- ) -> Optional["ChatBackground"]:
+ def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "ChatBackground":
"""See :meth:`telegram.TelegramObject.de_json`."""
data = cls._parse_data(data)
- if not data:
- return None
-
- data["type"] = BackgroundType.de_json(data.get("type"), bot)
+ data["type"] = de_json_optional(data.get("type"), BackgroundType, bot)
return super().de_json(data=data, bot=bot)
diff --git a/telegram/_chatboost.py b/telegram/_chatboost.py
index 7b972eec6d8..678b713afc3 100644
--- a/telegram/_chatboost.py
+++ b/telegram/_chatboost.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2024
+# Copyright (C) 2015-2025
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -17,16 +17,16 @@
# 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 classes that represent Telegram ChatBoosts."""
-
-from datetime import datetime
-from typing import TYPE_CHECKING, Dict, Final, Optional, Sequence, Tuple, Type
+import datetime as dtm
+from collections.abc import Sequence
+from typing import TYPE_CHECKING, Final, Optional
from telegram import constants
from telegram._chat import Chat
from telegram._telegramobject import TelegramObject
from telegram._user import User
from telegram._utils import enum
-from telegram._utils.argumentparsing import parse_sequence_arg
+from telegram._utils.argumentparsing import de_json_optional, de_list_optional, parse_sequence_arg
from telegram._utils.datetime import extract_tzinfo_from_defaults, from_timestamp
from telegram._utils.types import JSONDict
@@ -110,16 +110,11 @@ def __init__(self, source: str, *, api_kwargs: Optional[JSONDict] = None):
self._freeze()
@classmethod
- def de_json(
- cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
- ) -> Optional["ChatBoostSource"]:
+ def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "ChatBoostSource":
"""See :meth:`telegram.TelegramObject.de_json`."""
data = cls._parse_data(data)
- if not data:
- return None
-
- _class_mapping: Dict[str, Type[ChatBoostSource]] = {
+ _class_mapping: dict[str, type[ChatBoostSource]] = {
cls.PREMIUM: ChatBoostSourcePremium,
cls.GIFT_CODE: ChatBoostSourceGiftCode,
cls.GIVEAWAY: ChatBoostSourceGiveaway,
@@ -129,7 +124,7 @@ def de_json(
return _class_mapping[data.pop("source")].de_json(data=data, bot=bot)
if "user" in data:
- data["user"] = User.de_json(data.get("user"), bot)
+ data["user"] = de_json_optional(data.get("user"), User, bot)
return super().de_json(data=data, bot=bot)
@@ -187,15 +182,22 @@ def __init__(self, user: User, *, api_kwargs: Optional[JSONDict] = None):
class ChatBoostSourceGiveaway(ChatBoostSource):
"""
- The boost was obtained by the creation of a Telegram Premium giveaway. This boosts the chat 4
- times for the duration of the corresponding Telegram Premium subscription.
+ The boost was obtained by the creation of a Telegram Premium giveaway or a Telegram Star.
+ This boosts the chat 4 times for the duration of the corresponding Telegram Premium
+ subscription for Telegram Premium giveaways and :attr:`prize_star_count` / 500 times for
+ one year for Telegram Star giveaways.
.. versionadded:: 20.8
Args:
giveaway_message_id (:obj:`int`): Identifier of a message in the chat with the giveaway;
the message could have been deleted already. May be 0 if the message isn't sent yet.
- user (:class:`telegram.User`, optional): User that won the prize in the giveaway if any.
+ user (:class:`telegram.User`, optional): User that won the prize in the giveaway if any;
+ for Telegram Premium giveaways only.
+ prize_star_count (:obj:`int`, optional): The number of Telegram Stars to be split between
+ giveaway winners; for Telegram Star giveaways only.
+
+ .. versionadded:: 21.6
is_unclaimed (:obj:`bool`, optional): :obj:`True`, if the giveaway was completed, but
there was no user to win the prize.
@@ -205,17 +207,22 @@ class ChatBoostSourceGiveaway(ChatBoostSource):
giveaway_message_id (:obj:`int`): Identifier of a message in the chat with the giveaway;
the message could have been deleted already. May be 0 if the message isn't sent yet.
user (:class:`telegram.User`): Optional. User that won the prize in the giveaway if any.
+ prize_star_count (:obj:`int`): Optional. The number of Telegram Stars to be split between
+ giveaway winners; for Telegram Star giveaways only.
+
+ .. versionadded:: 21.6
is_unclaimed (:obj:`bool`): Optional. :obj:`True`, if the giveaway was completed, but
there was no user to win the prize.
"""
- __slots__ = ("giveaway_message_id", "is_unclaimed", "user")
+ __slots__ = ("giveaway_message_id", "is_unclaimed", "prize_star_count", "user")
def __init__(
self,
giveaway_message_id: int,
user: Optional[User] = None,
is_unclaimed: Optional[bool] = None,
+ prize_star_count: Optional[int] = None,
*,
api_kwargs: Optional[JSONDict] = None,
):
@@ -224,6 +231,7 @@ def __init__(
with self._unfrozen():
self.giveaway_message_id: int = giveaway_message_id
self.user: Optional[User] = user
+ self.prize_star_count: Optional[int] = prize_star_count
self.is_unclaimed: Optional[bool] = is_unclaimed
@@ -260,8 +268,8 @@ class ChatBoost(TelegramObject):
def __init__(
self,
boost_id: str,
- add_date: datetime,
- expiration_date: datetime,
+ add_date: dtm.datetime,
+ expiration_date: dtm.datetime,
source: ChatBoostSource,
*,
api_kwargs: Optional[JSONDict] = None,
@@ -269,27 +277,22 @@ def __init__(
super().__init__(api_kwargs=api_kwargs)
self.boost_id: str = boost_id
- self.add_date: datetime = add_date
- self.expiration_date: datetime = expiration_date
+ self.add_date: dtm.datetime = add_date
+ self.expiration_date: dtm.datetime = expiration_date
self.source: ChatBoostSource = source
self._id_attrs = (self.boost_id, self.add_date, self.expiration_date, self.source)
self._freeze()
@classmethod
- def de_json(
- cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
- ) -> Optional["ChatBoost"]:
+ def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "ChatBoost":
"""See :meth:`telegram.TelegramObject.de_json`."""
data = cls._parse_data(data)
- if not data:
- return None
-
- data["source"] = ChatBoostSource.de_json(data.get("source"), bot)
+ data["source"] = de_json_optional(data.get("source"), ChatBoostSource, bot)
loc_tzinfo = extract_tzinfo_from_defaults(bot)
- data["add_date"] = from_timestamp(data["add_date"], tzinfo=loc_tzinfo)
- data["expiration_date"] = from_timestamp(data["expiration_date"], tzinfo=loc_tzinfo)
+ data["add_date"] = from_timestamp(data.get("add_date"), tzinfo=loc_tzinfo)
+ data["expiration_date"] = from_timestamp(data.get("expiration_date"), tzinfo=loc_tzinfo)
return super().de_json(data=data, bot=bot)
@@ -329,17 +332,12 @@ def __init__(
self._freeze()
@classmethod
- def de_json(
- cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
- ) -> Optional["ChatBoostUpdated"]:
+ def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "ChatBoostUpdated":
"""See :meth:`telegram.TelegramObject.de_json`."""
data = cls._parse_data(data)
- if not data:
- return None
-
- data["chat"] = Chat.de_json(data.get("chat"), bot)
- data["boost"] = ChatBoost.de_json(data.get("boost"), bot)
+ data["chat"] = de_json_optional(data.get("chat"), Chat, bot)
+ data["boost"] = de_json_optional(data.get("boost"), ChatBoost, bot)
return super().de_json(data=data, bot=bot)
@@ -372,7 +370,7 @@ def __init__(
self,
chat: Chat,
boost_id: str,
- remove_date: datetime,
+ remove_date: dtm.datetime,
source: ChatBoostSource,
*,
api_kwargs: Optional[JSONDict] = None,
@@ -381,26 +379,21 @@ def __init__(
self.chat: Chat = chat
self.boost_id: str = boost_id
- self.remove_date: datetime = remove_date
+ self.remove_date: dtm.datetime = remove_date
self.source: ChatBoostSource = source
self._id_attrs = (self.chat, self.boost_id, self.remove_date, self.source)
self._freeze()
@classmethod
- def de_json(
- cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
- ) -> Optional["ChatBoostRemoved"]:
+ def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "ChatBoostRemoved":
"""See :meth:`telegram.TelegramObject.de_json`."""
data = cls._parse_data(data)
- if not data:
- return None
-
- data["chat"] = Chat.de_json(data.get("chat"), bot)
- data["source"] = ChatBoostSource.de_json(data.get("source"), bot)
+ data["chat"] = de_json_optional(data.get("chat"), Chat, bot)
+ data["source"] = de_json_optional(data.get("source"), ChatBoostSource, bot)
loc_tzinfo = extract_tzinfo_from_defaults(bot)
- data["remove_date"] = from_timestamp(data["remove_date"], tzinfo=loc_tzinfo)
+ data["remove_date"] = from_timestamp(data.get("remove_date"), tzinfo=loc_tzinfo)
return super().de_json(data=data, bot=bot)
@@ -418,7 +411,7 @@ class UserChatBoosts(TelegramObject):
user.
Attributes:
- boosts (Tuple[:class:`telegram.ChatBoost`]): List of boosts added to the chat by the user.
+ boosts (tuple[:class:`telegram.ChatBoost`]): List of boosts added to the chat by the user.
"""
__slots__ = ("boosts",)
@@ -431,21 +424,16 @@ def __init__(
):
super().__init__(api_kwargs=api_kwargs)
- self.boosts: Tuple[ChatBoost, ...] = parse_sequence_arg(boosts)
+ self.boosts: tuple[ChatBoost, ...] = parse_sequence_arg(boosts)
self._id_attrs = (self.boosts,)
self._freeze()
@classmethod
- def de_json(
- cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
- ) -> Optional["UserChatBoosts"]:
+ def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "UserChatBoosts":
"""See :meth:`telegram.TelegramObject.de_json`."""
data = cls._parse_data(data)
- if not data:
- return None
-
- data["boosts"] = ChatBoost.de_list(data.get("boosts"), bot)
+ data["boosts"] = de_list_optional(data.get("boosts"), ChatBoost, bot)
return super().de_json(data=data, bot=bot)
diff --git a/telegram/_chatfullinfo.py b/telegram/_chatfullinfo.py
index 04898659c3c..1ce640638e1 100644
--- a/telegram/_chatfullinfo.py
+++ b/telegram/_chatfullinfo.py
@@ -2,7 +2,7 @@
# pylint: disable=redefined-builtin
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2024
+# Copyright (C) 2015-2025
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -18,8 +18,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 an object that represents a Telegram ChatFullInfo."""
-from datetime import datetime
-from typing import TYPE_CHECKING, Optional, Sequence, Tuple
+import datetime as dtm
+from collections.abc import Sequence
+from typing import TYPE_CHECKING, Optional
from telegram._birthdate import Birthdate
from telegram._chat import Chat, _ChatBase
@@ -27,7 +28,7 @@
from telegram._chatpermissions import ChatPermissions
from telegram._files.chatphoto import ChatPhoto
from telegram._reaction import ReactionType
-from telegram._utils.argumentparsing import parse_sequence_arg
+from telegram._utils.argumentparsing import de_json_optional, de_list_optional, parse_sequence_arg
from telegram._utils.datetime import extract_tzinfo_from_defaults, from_timestamp
from telegram._utils.types import JSONDict
@@ -78,7 +79,7 @@ class ChatFullInfo(_ChatBase):
#collectible-usernames>`_; for private chats, supergroups and channels.
.. versionadded:: 20.0
- birthdate (:obj:`telegram.Birthdate`, optional): For private chats,
+ birthdate (:class:`telegram.Birthdate`, optional): For private chats,
the date of birth of the user.
.. versionadded:: 21.1
@@ -94,8 +95,8 @@ class ChatFullInfo(_ChatBase):
chats with business accounts, the opening hours of the business.
.. versionadded:: 21.1
- personal_chat (:obj:`telegram.Chat`, optional): For private chats, the personal channel of
- the user.
+ personal_chat (:class:`telegram.Chat`, optional): For private chats, the personal channel
+ of the user.
.. versionadded:: 21.1
available_reactions (Sequence[:class:`telegram.ReactionType`], optional): List of available
@@ -199,6 +200,9 @@ 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.
@@ -224,7 +228,7 @@ class ChatFullInfo(_ChatBase):
.. versionadded:: 20.0
photo (:class:`telegram.ChatPhoto`): Optional. Chat photo.
- active_usernames (Tuple[:obj:`str`]): Optional. If set, the list of all `active chat
+ active_usernames (tuple[:obj:`str`]): Optional. If set, the list of all `active chat
usernames `_; for private chats, supergroups and channels.
@@ -232,7 +236,7 @@ class ChatFullInfo(_ChatBase):
obtained via :meth:`~telegram.Bot.get_chat`.
.. versionadded:: 20.0
- birthdate (:obj:`telegram.Birthdate`): Optional. For private chats,
+ birthdate (:class:`telegram.Birthdate`): Optional. For private chats,
the date of birth of the user.
.. versionadded:: 21.1
@@ -248,11 +252,11 @@ class ChatFullInfo(_ChatBase):
chats with business accounts, the opening hours of the business.
.. versionadded:: 21.1
- personal_chat (:obj:`telegram.Chat`): Optional. For private chats, the personal channel of
- the user.
+ personal_chat (:class:`telegram.Chat`): Optional. For private chats, the personal channel
+ of the user.
.. versionadded:: 21.1
- available_reactions (Tuple[:class:`telegram.ReactionType`]): Optional. List of available
+ available_reactions (tuple[:class:`telegram.ReactionType`]): Optional. List of available
reactions allowed in the chat. If omitted, then all of
:const:`telegram.constants.ReactionEmoji` are allowed.
@@ -353,6 +357,9 @@ 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
@@ -368,6 +375,7 @@ 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",
@@ -421,7 +429,7 @@ def __init__(
profile_accent_color_id: Optional[int] = None,
profile_background_custom_emoji_id: Optional[str] = None,
emoji_status_custom_emoji_id: Optional[str] = None,
- emoji_status_expiration_date: Optional[datetime] = None,
+ emoji_status_expiration_date: Optional[dtm.datetime] = None,
bio: Optional[str] = None,
has_private_forwards: Optional[bool] = None,
has_restricted_voice_and_video_messages: Optional[bool] = None,
@@ -444,6 +452,7 @@ def __init__(
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,
):
@@ -483,14 +492,16 @@ def __init__(
self.has_restricted_voice_and_video_messages: Optional[bool] = (
has_restricted_voice_and_video_messages
)
- self.active_usernames: Tuple[str, ...] = parse_sequence_arg(active_usernames)
+ self.active_usernames: tuple[str, ...] = parse_sequence_arg(active_usernames)
self.emoji_status_custom_emoji_id: Optional[str] = emoji_status_custom_emoji_id
- self.emoji_status_expiration_date: Optional[datetime] = emoji_status_expiration_date
+ self.emoji_status_expiration_date: Optional[dtm.datetime] = (
+ emoji_status_expiration_date
+ )
self.has_aggressive_anti_spam_enabled: Optional[bool] = (
has_aggressive_anti_spam_enabled
)
self.has_hidden_members: Optional[bool] = has_hidden_members
- self.available_reactions: Optional[Tuple[ReactionType, ...]] = parse_sequence_arg(
+ self.available_reactions: Optional[tuple[ReactionType, ...]] = parse_sequence_arg(
available_reactions
)
self.accent_color_id: Optional[int] = accent_color_id
@@ -507,17 +518,13 @@ def __init__(
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: Optional[JSONDict], bot: Optional["Bot"] = None
- ) -> Optional["ChatFullInfo"]:
+ def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "ChatFullInfo":
"""See :meth:`telegram.TelegramObject.de_json`."""
data = cls._parse_data(data)
- if not data:
- return None
-
# Get the local timezone from the bot if it has defaults
loc_tzinfo = extract_tzinfo_from_defaults(bot)
@@ -525,7 +532,7 @@ def de_json(
data.get("emoji_status_expiration_date"), tzinfo=loc_tzinfo
)
- data["photo"] = ChatPhoto.de_json(data.get("photo"), bot)
+ data["photo"] = de_json_optional(data.get("photo"), ChatPhoto, bot)
from telegram import ( # pylint: disable=import-outside-toplevel
BusinessIntro,
@@ -534,16 +541,20 @@ def de_json(
Message,
)
- data["pinned_message"] = Message.de_json(data.get("pinned_message"), bot)
- data["permissions"] = ChatPermissions.de_json(data.get("permissions"), bot)
- data["location"] = ChatLocation.de_json(data.get("location"), bot)
- data["available_reactions"] = ReactionType.de_list(data.get("available_reactions"), bot)
- data["birthdate"] = Birthdate.de_json(data.get("birthdate"), bot)
- data["personal_chat"] = Chat.de_json(data.get("personal_chat"), bot)
- data["business_intro"] = BusinessIntro.de_json(data.get("business_intro"), bot)
- data["business_location"] = BusinessLocation.de_json(data.get("business_location"), bot)
- data["business_opening_hours"] = BusinessOpeningHours.de_json(
- data.get("business_opening_hours"), bot
+ data["pinned_message"] = de_json_optional(data.get("pinned_message"), Message, bot)
+ data["permissions"] = de_json_optional(data.get("permissions"), ChatPermissions, bot)
+ data["location"] = de_json_optional(data.get("location"), ChatLocation, bot)
+ data["available_reactions"] = de_list_optional(
+ data.get("available_reactions"), ReactionType, bot
+ )
+ data["birthdate"] = de_json_optional(data.get("birthdate"), Birthdate, bot)
+ data["personal_chat"] = de_json_optional(data.get("personal_chat"), Chat, bot)
+ data["business_intro"] = de_json_optional(data.get("business_intro"), BusinessIntro, bot)
+ data["business_location"] = de_json_optional(
+ data.get("business_location"), BusinessLocation, bot
+ )
+ data["business_opening_hours"] = de_json_optional(
+ data.get("business_opening_hours"), BusinessOpeningHours, bot
)
return super().de_json(data=data, bot=bot)
diff --git a/telegram/_chatinvitelink.py b/telegram/_chatinvitelink.py
index 43e7e8ab62d..289ee48bdba 100644
--- a/telegram/_chatinvitelink.py
+++ b/telegram/_chatinvitelink.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2024
+# Copyright (C) 2015-2025
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -17,11 +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 an invite link for a chat."""
-import datetime
+import datetime as dtm
from typing import TYPE_CHECKING, Optional
from telegram._telegramobject import TelegramObject
from telegram._user import User
+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
@@ -69,6 +70,16 @@ class ChatInviteLink(TelegramObject):
created using this link.
.. versionadded:: 13.8
+ subscription_period (:obj:`int`, optional): The number of seconds the subscription will be
+ active for before the next payment.
+
+ .. versionadded:: 21.5
+ subscription_price (:obj:`int`, optional): The amount of Telegram Stars a user must pay
+ initially and after each subsequent subscription period to be a member of the chat
+ using the link.
+
+ .. versionadded:: 21.5
+
Attributes:
invite_link (:obj:`str`): The invite link. If the link was created by another chat
administrator, then the second part of the link will be replaced with ``'…'``.
@@ -96,6 +107,15 @@ class ChatInviteLink(TelegramObject):
created using this link.
.. versionadded:: 13.8
+ subscription_period (:obj:`int`): Optional. The number of seconds the subscription will be
+ active for before the next payment.
+
+ .. versionadded:: 21.5
+ subscription_price (:obj:`int`): Optional. The amount of Telegram Stars a user must pay
+ initially and after each subsequent subscription period to be a member of the chat
+ using the link.
+
+ .. versionadded:: 21.5
"""
@@ -109,6 +129,8 @@ class ChatInviteLink(TelegramObject):
"member_limit",
"name",
"pending_join_request_count",
+ "subscription_period",
+ "subscription_price",
)
def __init__(
@@ -118,10 +140,12 @@ def __init__(
creates_join_request: bool,
is_primary: bool,
is_revoked: bool,
- expire_date: Optional[datetime.datetime] = None,
+ expire_date: Optional[dtm.datetime] = None,
member_limit: Optional[int] = None,
name: Optional[str] = None,
pending_join_request_count: Optional[int] = None,
+ subscription_period: Optional[int] = None,
+ subscription_price: Optional[int] = None,
*,
api_kwargs: Optional[JSONDict] = None,
):
@@ -134,12 +158,15 @@ def __init__(
self.is_revoked: bool = is_revoked
# Optionals
- self.expire_date: Optional[datetime.datetime] = expire_date
+ self.expire_date: Optional[dtm.datetime] = expire_date
self.member_limit: Optional[int] = member_limit
self.name: Optional[str] = name
self.pending_join_request_count: Optional[int] = (
int(pending_join_request_count) if pending_join_request_count is not None else None
)
+ self.subscription_period: Optional[int] = subscription_period
+ self.subscription_price: Optional[int] = subscription_price
+
self._id_attrs = (
self.invite_link,
self.creates_join_request,
@@ -151,19 +178,14 @@ def __init__(
self._freeze()
@classmethod
- def de_json(
- cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
- ) -> Optional["ChatInviteLink"]:
+ def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "ChatInviteLink":
"""See :meth:`telegram.TelegramObject.de_json`."""
data = cls._parse_data(data)
- if not data:
- return None
-
# Get the local timezone from the bot if it has defaults
loc_tzinfo = extract_tzinfo_from_defaults(bot)
- data["creator"] = User.de_json(data.get("creator"), bot)
+ data["creator"] = de_json_optional(data.get("creator"), User, bot)
data["expire_date"] = from_timestamp(data.get("expire_date", None), tzinfo=loc_tzinfo)
return super().de_json(data=data, bot=bot)
diff --git a/telegram/_chatjoinrequest.py b/telegram/_chatjoinrequest.py
index 9c444d97b4d..048b6a80b5d 100644
--- a/telegram/_chatjoinrequest.py
+++ b/telegram/_chatjoinrequest.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2024
+# Copyright (C) 2015-2025
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -17,13 +17,14 @@
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains an object that represents a Telegram ChatJoinRequest."""
-import datetime
+import datetime as dtm
from typing import TYPE_CHECKING, Optional
from telegram._chat import Chat
from telegram._chatinvitelink import ChatInviteLink
from telegram._telegramobject import TelegramObject
from telegram._user import User
+from telegram._utils.argumentparsing import de_json_optional
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
@@ -106,7 +107,7 @@ def __init__(
self,
chat: Chat,
from_user: User,
- date: datetime.datetime,
+ date: dtm.datetime,
user_chat_id: int,
bio: Optional[str] = None,
invite_link: Optional[ChatInviteLink] = None,
@@ -117,7 +118,7 @@ def __init__(
# Required
self.chat: Chat = chat
self.from_user: User = from_user
- self.date: datetime.datetime = date
+ self.date: dtm.datetime = date
self.user_chat_id: int = user_chat_id
# Optionals
@@ -129,22 +130,17 @@ def __init__(
self._freeze()
@classmethod
- def de_json(
- cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
- ) -> Optional["ChatJoinRequest"]:
+ def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "ChatJoinRequest":
"""See :meth:`telegram.TelegramObject.de_json`."""
data = cls._parse_data(data)
- if not data:
- return None
-
# Get the local timezone from the bot if it has defaults
loc_tzinfo = extract_tzinfo_from_defaults(bot)
- data["chat"] = Chat.de_json(data.get("chat"), bot)
- data["from_user"] = User.de_json(data.pop("from", None), bot)
+ data["chat"] = de_json_optional(data.get("chat"), Chat, bot)
+ data["from_user"] = de_json_optional(data.pop("from", None), User, bot)
data["date"] = from_timestamp(data.get("date", None), tzinfo=loc_tzinfo)
- data["invite_link"] = ChatInviteLink.de_json(data.get("invite_link"), bot)
+ data["invite_link"] = de_json_optional(data.get("invite_link"), ChatInviteLink, bot)
return super().de_json(data=data, bot=bot)
diff --git a/telegram/_chatlocation.py b/telegram/_chatlocation.py
index 04f9854a23a..4514b2566db 100644
--- a/telegram/_chatlocation.py
+++ b/telegram/_chatlocation.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2024
+# Copyright (C) 2015-2025
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -23,6 +23,7 @@
from telegram import constants
from telegram._files.location import Location
from telegram._telegramobject import TelegramObject
+from telegram._utils.argumentparsing import de_json_optional
from telegram._utils.types import JSONDict
if TYPE_CHECKING:
@@ -68,16 +69,11 @@ def __init__(
self._freeze()
@classmethod
- def de_json(
- cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
- ) -> Optional["ChatLocation"]:
+ def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "ChatLocation":
"""See :meth:`telegram.TelegramObject.de_json`."""
data = cls._parse_data(data)
- if not data:
- return None
-
- data["location"] = Location.de_json(data.get("location"), bot)
+ data["location"] = de_json_optional(data.get("location"), Location, bot)
return super().de_json(data=data, bot=bot)
diff --git a/telegram/_chatmember.py b/telegram/_chatmember.py
index 0cc06bf5804..647c089edde 100644
--- a/telegram/_chatmember.py
+++ b/telegram/_chatmember.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2024
+# Copyright (C) 2015-2025
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -17,12 +17,15 @@
# 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 ChatMember."""
-import datetime
-from typing import TYPE_CHECKING, Dict, Final, Optional, Type
+
+import datetime as dtm
+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
@@ -97,23 +100,18 @@ def __init__(
super().__init__(api_kwargs=api_kwargs)
# Required by all subclasses
self.user: User = user
- self.status: str = status
+ self.status: str = enum.get_member(constants.ChatMemberStatus, status, status)
self._id_attrs = (self.user, self.status)
self._freeze()
@classmethod
- def de_json(
- cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
- ) -> Optional["ChatMember"]:
+ def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "ChatMember":
"""See :meth:`telegram.TelegramObject.de_json`."""
data = cls._parse_data(data)
- if not data:
- return None
-
- _class_mapping: Dict[str, Type[ChatMember]] = {
+ _class_mapping: dict[str, type[ChatMember]] = {
cls.OWNER: ChatMemberOwner,
cls.ADMINISTRATOR: ChatMemberAdministrator,
cls.MEMBER: ChatMemberMember,
@@ -125,12 +123,12 @@ def de_json(
if cls is ChatMember and data.get("status") in _class_mapping:
return _class_mapping[data.pop("status")].de_json(data=data, bot=bot)
- data["user"] = User.de_json(data.get("user"), bot)
+ data["user"] = de_json_optional(data.get("user"), User, bot)
if "until_date" in data:
# Get the local timezone from the bot if it has defaults
loc_tzinfo = extract_tzinfo_from_defaults(bot)
- data["until_date"] = from_timestamp(data["until_date"], tzinfo=loc_tzinfo)
+ data["until_date"] = from_timestamp(data.get("until_date"), tzinfo=loc_tzinfo)
# This is a deprecated field that TG still returns for backwards compatibility
# Let's filter it out to speed up the de-json process
@@ -391,24 +389,34 @@ class ChatMemberMember(ChatMember):
Args:
user (:class:`telegram.User`): Information about the user.
+ until_date (:class:`datetime.datetime`, optional): Date when the user's subscription will
+ expire.
+
+ .. versionadded:: 21.5
Attributes:
status (:obj:`str`): The member's status in the chat,
always :tg-const:`telegram.ChatMember.MEMBER`.
user (:class:`telegram.User`): Information about the user.
+ until_date (:class:`datetime.datetime`): Optional. Date when the user's subscription will
+ expire.
+
+ .. versionadded:: 21.5
"""
- __slots__ = ()
+ __slots__ = ("until_date",)
def __init__(
self,
user: User,
+ until_date: Optional[dtm.datetime] = None,
*,
api_kwargs: Optional[JSONDict] = None,
):
super().__init__(status=ChatMember.MEMBER, user=user, api_kwargs=api_kwargs)
- self._freeze()
+ with self._unfrozen():
+ self.until_date: Optional[dtm.datetime] = until_date
class ChatMemberRestricted(ChatMember):
@@ -555,7 +563,7 @@ def __init__(
can_send_other_messages: bool,
can_add_web_page_previews: bool,
can_manage_topics: bool,
- until_date: datetime.datetime,
+ until_date: dtm.datetime,
can_send_audios: bool,
can_send_documents: bool,
can_send_photos: bool,
@@ -576,7 +584,7 @@ def __init__(
self.can_send_other_messages: bool = can_send_other_messages
self.can_add_web_page_previews: bool = can_add_web_page_previews
self.can_manage_topics: bool = can_manage_topics
- self.until_date: datetime.datetime = until_date
+ self.until_date: dtm.datetime = until_date
self.can_send_audios: bool = can_send_audios
self.can_send_documents: bool = can_send_documents
self.can_send_photos: bool = can_send_photos
@@ -645,10 +653,10 @@ class ChatMemberBanned(ChatMember):
def __init__(
self,
user: User,
- until_date: datetime.datetime,
+ until_date: dtm.datetime,
*,
api_kwargs: Optional[JSONDict] = None,
):
super().__init__(status=ChatMember.BANNED, user=user, api_kwargs=api_kwargs)
with self._unfrozen():
- self.until_date: datetime.datetime = until_date
+ self.until_date: dtm.datetime = until_date
diff --git a/telegram/_chatmemberupdated.py b/telegram/_chatmemberupdated.py
index 1aacb218533..5aeab80a1fa 100644
--- a/telegram/_chatmemberupdated.py
+++ b/telegram/_chatmemberupdated.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2024
+# Copyright (C) 2015-2025
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -17,14 +17,15 @@
# 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 ChatMemberUpdated."""
-import datetime
-from typing import TYPE_CHECKING, Dict, Optional, Tuple, Union
+import datetime as dtm
+from typing import TYPE_CHECKING, Optional, Union
from telegram._chat import Chat
from telegram._chatinvitelink import ChatInviteLink
from telegram._chatmember import ChatMember
from telegram._telegramobject import TelegramObject
from telegram._user import User
+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
@@ -108,7 +109,7 @@ def __init__(
self,
chat: Chat,
from_user: User,
- date: datetime.datetime,
+ date: dtm.datetime,
old_chat_member: ChatMember,
new_chat_member: ChatMember,
invite_link: Optional[ChatInviteLink] = None,
@@ -121,7 +122,7 @@ def __init__(
# Required
self.chat: Chat = chat
self.from_user: User = from_user
- self.date: datetime.datetime = date
+ self.date: dtm.datetime = date
self.old_chat_member: ChatMember = old_chat_member
self.new_chat_member: ChatMember = new_chat_member
self.via_chat_folder_invite_link: Optional[bool] = via_chat_folder_invite_link
@@ -141,28 +142,23 @@ def __init__(
self._freeze()
@classmethod
- def de_json(
- cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
- ) -> Optional["ChatMemberUpdated"]:
+ def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "ChatMemberUpdated":
"""See :meth:`telegram.TelegramObject.de_json`."""
data = cls._parse_data(data)
- if not data:
- return None
-
# Get the local timezone from the bot if it has defaults
loc_tzinfo = extract_tzinfo_from_defaults(bot)
- data["chat"] = Chat.de_json(data.get("chat"), bot)
- data["from_user"] = User.de_json(data.pop("from", None), bot)
+ data["chat"] = de_json_optional(data.get("chat"), Chat, bot)
+ data["from_user"] = de_json_optional(data.pop("from", None), User, bot)
data["date"] = from_timestamp(data.get("date"), tzinfo=loc_tzinfo)
- data["old_chat_member"] = ChatMember.de_json(data.get("old_chat_member"), bot)
- data["new_chat_member"] = ChatMember.de_json(data.get("new_chat_member"), bot)
- data["invite_link"] = ChatInviteLink.de_json(data.get("invite_link"), bot)
+ data["old_chat_member"] = de_json_optional(data.get("old_chat_member"), ChatMember, bot)
+ data["new_chat_member"] = de_json_optional(data.get("new_chat_member"), ChatMember, bot)
+ data["invite_link"] = de_json_optional(data.get("invite_link"), ChatInviteLink, bot)
return super().de_json(data=data, bot=bot)
- def _get_attribute_difference(self, attribute: str) -> Tuple[object, object]:
+ def _get_attribute_difference(self, attribute: str) -> tuple[object, object]:
try:
old = self.old_chat_member[attribute]
except KeyError:
@@ -177,11 +173,9 @@ def _get_attribute_difference(self, attribute: str) -> Tuple[object, object]:
def difference(
self,
- ) -> Dict[
+ ) -> dict[
str,
- Tuple[
- Union[str, bool, datetime.datetime, User], Union[str, bool, datetime.datetime, User]
- ],
+ tuple[Union[str, bool, dtm.datetime, User], Union[str, bool, dtm.datetime, User]],
]:
"""Computes the difference between :attr:`old_chat_member` and :attr:`new_chat_member`.
@@ -198,7 +192,7 @@ def difference(
.. versionadded:: 13.5
Returns:
- Dict[:obj:`str`, Tuple[:class:`object`, :class:`object`]]: A dictionary mapping
+ dict[:obj:`str`, tuple[:class:`object`, :class:`object`]]: A dictionary mapping
attribute names to tuples of the form ``(old_value, new_value)``
"""
# we first get the names of the attributes that have changed
diff --git a/telegram/_chatpermissions.py b/telegram/_chatpermissions.py
index c4e9e94b7a9..e70e858f291 100644
--- a/telegram/_chatpermissions.py
+++ b/telegram/_chatpermissions.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2024
+# Copyright (C) 2015-2025
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -231,15 +231,10 @@ def no_permissions(cls) -> "ChatPermissions":
return cls(*(14 * (False,)))
@classmethod
- def de_json(
- cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
- ) -> Optional["ChatPermissions"]:
+ def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "ChatPermissions":
"""See :meth:`telegram.TelegramObject.de_json`."""
data = cls._parse_data(data)
- if not data:
- return None
-
api_kwargs = {}
# This is a deprecated field that TG still returns for backwards compatibility
# Let's filter it out to speed up the de-json process
diff --git a/telegram/_choseninlineresult.py b/telegram/_choseninlineresult.py
index 76380e95839..e3754039230 100644
--- a/telegram/_choseninlineresult.py
+++ b/telegram/_choseninlineresult.py
@@ -2,7 +2,7 @@
# pylint: disable=too-many-arguments
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2024
+# Copyright (C) 2015-2025
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -24,6 +24,7 @@
from telegram._files.location import Location
from telegram._telegramobject import TelegramObject
from telegram._user import User
+from telegram._utils.argumentparsing import de_json_optional
from telegram._utils.types import JSONDict
if TYPE_CHECKING:
@@ -92,18 +93,13 @@ def __init__(
self._freeze()
@classmethod
- def de_json(
- cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
- ) -> Optional["ChosenInlineResult"]:
+ def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "ChosenInlineResult":
"""See :meth:`telegram.TelegramObject.de_json`."""
data = cls._parse_data(data)
- if not data:
- return None
-
# Required
- data["from_user"] = User.de_json(data.pop("from", None), bot)
+ data["from_user"] = de_json_optional(data.pop("from", None), User, bot)
# Optionals
- data["location"] = Location.de_json(data.get("location"), bot)
+ data["location"] = de_json_optional(data.get("location"), Location, bot)
return super().de_json(data=data, bot=bot)
diff --git a/telegram/_copytextbutton.py b/telegram/_copytextbutton.py
new file mode 100644
index 00000000000..4a3cdb90590
--- /dev/null
+++ b/telegram/_copytextbutton.py
@@ -0,0 +1,55 @@
+#!/usr/bin/env python
+#
+# A library that provides a Python interface to the Telegram Bot API
+# Copyright (C) 2015-2025
+# Leandro Toledo de Souza
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Lesser Public License for more details.
+#
+# You should have received a copy of the GNU Lesser Public License
+# along with this program. If not, see [http://www.gnu.org/licenses/].
+"""This module contains an object that represents a Telegram CopyTextButton."""
+from typing import Optional
+
+from telegram._telegramobject import TelegramObject
+from telegram._utils.types import JSONDict
+
+
+class CopyTextButton(TelegramObject):
+ """
+ This object represents an inline keyboard button that copies specified text to the clipboard.
+
+ Objects of this class are comparable in terms of equality. Two objects of this class are
+ considered equal, if their :attr:`text` is equal.
+
+ .. versionadded:: 21.7
+
+ Args:
+ text (:obj:`str`): The text to be copied to the clipboard;
+ :tg-const:`telegram.constants.InlineKeyboardButtonLimit.MIN_COPY_TEXT`-
+ :tg-const:`telegram.constants.InlineKeyboardButtonLimit.MAX_COPY_TEXT` characters
+
+ Attributes:
+ text (:obj:`str`): The text to be copied to the clipboard;
+ :tg-const:`telegram.constants.InlineKeyboardButtonLimit.MIN_COPY_TEXT`-
+ :tg-const:`telegram.constants.InlineKeyboardButtonLimit.MAX_COPY_TEXT` characters
+
+ """
+
+ __slots__ = ("text",)
+
+ def __init__(self, text: str, *, api_kwargs: Optional[JSONDict] = None):
+ super().__init__(api_kwargs=api_kwargs)
+ self.text: str = text
+
+ self._id_attrs = (self.text,)
+
+ self._freeze()
diff --git a/telegram/_dice.py b/telegram/_dice.py
index 621e4b13f98..a549aefb09d 100644
--- a/telegram/_dice.py
+++ b/telegram/_dice.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2024
+# Copyright (C) 2015-2025
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -17,7 +17,7 @@
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains an object that represents a Telegram Dice."""
-from typing import Final, List, Optional
+from typing import Final, Optional
from telegram import constants
from telegram._telegramobject import TelegramObject
@@ -114,8 +114,8 @@ def __init__(self, value: int, emoji: str, *, api_kwargs: Optional[JSONDict] = N
.. versionadded:: 13.4
"""
- ALL_EMOJI: Final[List[str]] = list(constants.DiceEmoji)
- """List[:obj:`str`]: A list of all available dice emoji."""
+ ALL_EMOJI: Final[list[str]] = list(constants.DiceEmoji)
+ """list[:obj:`str`]: A list of all available dice emoji."""
MIN_VALUE: Final[int] = constants.DiceLimit.MIN_VALUE
""":const:`telegram.constants.DiceLimit.MIN_VALUE`
diff --git a/telegram/_files/_basemedium.py b/telegram/_files/_basemedium.py
index 4decb041206..4dd76b10e4b 100644
--- a/telegram/_files/_basemedium.py
+++ b/telegram/_files/_basemedium.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2024
+# Copyright (C) 2015-2025
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
diff --git a/telegram/_files/_basethumbedmedium.py b/telegram/_files/_basethumbedmedium.py
index 20ff82eab5e..2008475c2f2 100644
--- a/telegram/_files/_basethumbedmedium.py
+++ b/telegram/_files/_basethumbedmedium.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2024
+# Copyright (C) 2015-2025
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -17,10 +17,11 @@
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""Common base class for media objects with thumbnails"""
-from typing import TYPE_CHECKING, Optional, Type, TypeVar
+from typing import TYPE_CHECKING, Optional, TypeVar
from telegram._files._basemedium import _BaseMedium
from telegram._files.photosize import PhotoSize
+from telegram._utils.argumentparsing import de_json_optional
from telegram._utils.types import JSONDict
if TYPE_CHECKING:
@@ -82,17 +83,14 @@ def __init__(
@classmethod
def de_json(
- cls: Type[ThumbedMT_co], data: Optional[JSONDict], bot: Optional["Bot"] = None
- ) -> Optional[ThumbedMT_co]:
+ cls: type[ThumbedMT_co], data: JSONDict, bot: Optional["Bot"] = None
+ ) -> ThumbedMT_co:
"""See :meth:`telegram.TelegramObject.de_json`."""
data = cls._parse_data(data)
- if not data:
- return None
-
# In case this wasn't already done by the subclass
if not isinstance(data.get("thumbnail"), PhotoSize):
- data["thumbnail"] = PhotoSize.de_json(data.get("thumbnail"), bot)
+ data["thumbnail"] = de_json_optional(data.get("thumbnail"), PhotoSize, bot)
api_kwargs = {}
# This is a deprecated field that TG still returns for backwards compatibility
diff --git a/telegram/_files/animation.py b/telegram/_files/animation.py
index 5191ce83d89..537ffc0a0db 100644
--- a/telegram/_files/animation.py
+++ b/telegram/_files/animation.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2024
+# Copyright (C) 2015-2025
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
diff --git a/telegram/_files/audio.py b/telegram/_files/audio.py
index fb7bc2ce7d1..af5e420e1b2 100644
--- a/telegram/_files/audio.py
+++ b/telegram/_files/audio.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2024
+# Copyright (C) 2015-2025
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
diff --git a/telegram/_files/chatphoto.py b/telegram/_files/chatphoto.py
index ace7f9666f2..5d6e91471d7 100644
--- a/telegram/_files/chatphoto.py
+++ b/telegram/_files/chatphoto.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2024
+# Copyright (C) 2015-2025
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
diff --git a/telegram/_files/contact.py b/telegram/_files/contact.py
index 113b11dc8d0..1ff05b36dc0 100644
--- a/telegram/_files/contact.py
+++ b/telegram/_files/contact.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2024
+# Copyright (C) 2015-2025
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
diff --git a/telegram/_files/document.py b/telegram/_files/document.py
index e278dc43e3b..7ddaeaf592e 100644
--- a/telegram/_files/document.py
+++ b/telegram/_files/document.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2024
+# Copyright (C) 2015-2025
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
diff --git a/telegram/_files/file.py b/telegram/_files/file.py
index c9b8d22d49a..38fdac7fd66 100644
--- a/telegram/_files/file.py
+++ b/telegram/_files/file.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2024
+# Copyright (C) 2015-2025
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -128,9 +128,8 @@ async def download_to_drive(
) -> Path:
"""
Download this file. By default, the file is saved in the current working directory with
- :attr:`file_path` as file name. If the file has no filename, the file ID will be used as
- filename. If :paramref:`custom_path` is supplied as a :obj:`str` or :obj:`pathlib.Path`,
- it will be saved to that path.
+ :attr:`file_path` as file name. If :paramref:`custom_path` is supplied as a :obj:`str` or
+ :obj:`pathlib.Path`, it will be saved to that path.
Note:
If :paramref:`custom_path` isn't provided and :attr:`file_path` is the path of a
@@ -152,6 +151,11 @@ async def download_to_drive(
* This method was previously called ``download``. It was split into
:meth:`download_to_drive` and :meth:`download_to_memory`.
+ .. versionchanged:: 21.7
+ Raises :exc:`RuntimeError` if :attr:`file_path` is not set. Note that files without
+ a :attr:`file_path` could never be downloaded, as this attribute is mandatory for that
+ operation.
+
Args:
custom_path (:class:`pathlib.Path` | :obj:`str` , optional): The path where the file
will be saved to. If not specified, will be saved in the current working directory
@@ -175,7 +179,13 @@ async def download_to_drive(
Returns:
:class:`pathlib.Path`: Returns the Path object the file was downloaded to.
+ Raises:
+ RuntimeError: If :attr:`file_path` is not set.
+
"""
+ if not self.file_path:
+ raise RuntimeError("No `file_path` available for this file. Can not download.")
+
local_file = is_local_file(self.file_path)
url = None if local_file else self._get_encoded_url()
@@ -198,10 +208,8 @@ async def download_to_drive(
filename = Path(custom_path)
elif local_file:
return Path(self.file_path)
- elif self.file_path:
- filename = Path(Path(self.file_path).name)
else:
- filename = Path.cwd() / self.file_id
+ filename = Path(Path(self.file_path).name)
buf = await self.get_bot().request.retrieve(
url,
@@ -237,6 +245,11 @@ async def download_to_memory(
.. versionadded:: 20.0
+ .. versionchanged:: 21.7
+ Raises :exc:`RuntimeError` if :attr:`file_path` is not set. Note that files without
+ a :attr:`file_path` could never be downloaded, as this attribute is mandatory for that
+ operation.
+
Args:
out (:obj:`io.BufferedIOBase`): A file-like object. Must be opened for writing in
binary mode.
@@ -254,7 +267,13 @@ async def download_to_memory(
pool_timeout (:obj:`float` | :obj:`None`, optional): Value to pass to
:paramref:`telegram.request.BaseRequest.post.pool_timeout`. Defaults to
:attr:`~telegram.request.BaseRequest.DEFAULT_NONE`.
+
+ Raises:
+ RuntimeError: If :attr:`file_path` is not set.
"""
+ if not self.file_path:
+ raise RuntimeError("No `file_path` available for this file. Can not download.")
+
local_file = is_local_file(self.file_path)
url = None if local_file else self._get_encoded_url()
path = Path(self.file_path) if local_file else None
@@ -283,6 +302,11 @@ async def download_as_bytearray(
) -> bytearray:
"""Download this file and return it as a bytearray.
+ .. versionchanged:: 21.7
+ Raises :exc:`RuntimeError` if :attr:`file_path` is not set. Note that files without
+ a :attr:`file_path` could never be downloaded, as this attribute is mandatory for that
+ operation.
+
Args:
buf (:obj:`bytearray`, optional): Extend the given bytearray with the downloaded data.
@@ -312,7 +336,13 @@ async def download_as_bytearray(
:obj:`bytearray`: The same object as :paramref:`buf` if it was specified. Otherwise a
newly allocated :obj:`bytearray`.
+ Raises:
+ RuntimeError: If :attr:`file_path` is not set.
+
"""
+ if not self.file_path:
+ raise RuntimeError("No `file_path` available for this file. Can not download.")
+
if buf is None:
buf = bytearray()
diff --git a/telegram/_files/inputfile.py b/telegram/_files/inputfile.py
index 994135bb5dd..8c88a9dece2 100644
--- a/telegram/_files/inputfile.py
+++ b/telegram/_files/inputfile.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2024
+# Copyright (C) 2015-2025
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -22,7 +22,8 @@
from typing import IO, Optional, Union
from uuid import uuid4
-from telegram._utils.files import load_file
+from telegram._utils.files import guess_file_name, load_file
+from telegram._utils.strings import TextEncoding
from telegram._utils.types import FieldTuple
_DEFAULT_MIME_TYPE = "application/octet-stream"
@@ -52,9 +53,36 @@ class InputFile:
attach (:obj:`bool`, optional): Pass :obj:`True` if the parameter this file belongs to in
the request to Telegram should point to the multipart data via an ``attach://`` URI.
Defaults to `False`.
+ read_file_handle (:obj:`bool`, optional): If :obj:`True` and :paramref:`obj` is a file
+ handle, the data will be read from the file handle on initialization of this object.
+ If :obj:`False`, the file handle will be passed on to the
+ :attr:`networking backend ` which will have
+ to handle the reading. Defaults to :obj:`True`.
+
+ Tip:
+ If you upload extremely large files, you may want to set this to :obj:`False` to
+ avoid reading the complete file into memory. Additionally, this may be supported
+ better by the networking backend (in particular it is handled better by
+ the default :class:`~telegram.request.HTTPXRequest`).
+
+ Important:
+ If you set this to :obj:`False`, you have to ensure that the file handle is still
+ open when the request is made. In particular, the following snippet can *not* work
+ as expected.
+
+ .. code-block:: python
+
+ with open('file.txt', 'rb') as file:
+ input_file = InputFile(file, read_file_handle=False)
+
+ # here the file handle is already closed and the upload will fail
+ await bot.send_document(chat_id, input_file)
+
+ .. versionadded:: 21.5
+
Attributes:
- input_file_content (:obj:`bytes`): The binary content of the file to send.
+ input_file_content (:obj:`bytes` | :class:`IO`): The binary content of the file to send.
attach_name (:obj:`str`): Optional. If present, the parameter this file belongs to in
the request to Telegram should point to the multipart data via a an URI of the form
``attach://`` URI.
@@ -70,14 +98,18 @@ def __init__(
obj: Union[IO[bytes], bytes, str],
filename: Optional[str] = None,
attach: bool = False,
+ read_file_handle: bool = True,
):
if isinstance(obj, bytes):
- self.input_file_content: bytes = obj
+ self.input_file_content: Union[bytes, IO[bytes]] = obj
elif isinstance(obj, str):
- self.input_file_content = obj.encode("utf-8")
- else:
+ self.input_file_content = obj.encode(TextEncoding.UTF_8)
+ elif read_file_handle:
reported_filename, self.input_file_content = load_file(obj)
filename = filename or reported_filename
+ else:
+ self.input_file_content = obj
+ filename = filename or guess_file_name(obj)
self.attach_name: Optional[str] = "attached" + uuid4().hex if attach else None
@@ -94,8 +126,11 @@ def __init__(
def field_tuple(self) -> FieldTuple:
"""Field tuple representing the contents of the file for upload to the Telegram servers.
+ .. versionchanged:: 21.5
+ Content may now be a file handle.
+
Returns:
- Tuple[:obj:`str`, :obj:`bytes`, :obj:`str`]:
+ tuple[:obj:`str`, :obj:`bytes` | :class:`IO`, :obj:`str`]:
"""
return self.filename, self.input_file_content, self.mimetype
diff --git a/telegram/_files/inputmedia.py b/telegram/_files/inputmedia.py
index 692369130a4..017e1b423fe 100644
--- a/telegram/_files/inputmedia.py
+++ b/telegram/_files/inputmedia.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2024
+# Copyright (C) 2015-2025
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -17,7 +17,8 @@
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""Base class for Telegram InputMedia Objects."""
-from typing import Final, Optional, Sequence, Tuple, Union
+from collections.abc import Sequence
+from typing import Final, Optional, Union
from telegram import constants
from telegram._files.animation import Animation
@@ -50,13 +51,8 @@ class InputMedia(TelegramObject):
Args:
media_type (:obj:`str`): Type of media that the instance represents.
- media (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | \
- :class:`telegram.Animation` | :class:`telegram.Audio` | \
- :class:`telegram.Document` | :class:`telegram.PhotoSize` | \
- :class:`telegram.Video`): File to send.
+ media (:obj:`str` | :class:`~telegram.InputFile`): File to send.
|fileinputnopath|
- Lastly you can pass an existing telegram media object of the corresponding type
- to send.
caption (:obj:`str`, optional): Caption of the media to be sent,
0-:tg-const:`telegram.constants.MessageLimit.CAPTION_LENGTH` characters after entities
parsing.
@@ -74,7 +70,7 @@ class InputMedia(TelegramObject):
0-:tg-const:`telegram.constants.MessageLimit.CAPTION_LENGTH` characters after entities
parsing.
parse_mode (:obj:`str`): Optional. |parse_mode|
- caption_entities (Tuple[:class:`telegram.MessageEntity`]): Optional. |captionentitiesattr|
+ caption_entities (tuple[:class:`telegram.MessageEntity`]): Optional. |captionentitiesattr|
.. versionchanged:: 20.0
@@ -88,7 +84,7 @@ class InputMedia(TelegramObject):
def __init__(
self,
media_type: str,
- media: Union[str, InputFile, MediaType],
+ media: Union[str, InputFile],
caption: Optional[str] = None,
caption_entities: Optional[Sequence[MessageEntity]] = None,
parse_mode: ODVInput[str] = DEFAULT_NONE,
@@ -97,9 +93,9 @@ def __init__(
):
super().__init__(api_kwargs=api_kwargs)
self.type: str = enum.get_member(constants.InputMediaType, media_type, media_type)
- self.media: Union[str, InputFile, Animation, Audio, Document, PhotoSize, Video] = media
+ self.media: Union[str, InputFile] = media
self.caption: Optional[str] = caption
- self.caption_entities: Tuple[MessageEntity, ...] = parse_sequence_arg(caption_entities)
+ self.caption_entities: tuple[MessageEntity, ...] = parse_sequence_arg(caption_entities)
self.parse_mode: ODVInput[str] = parse_mode
self._freeze()
@@ -128,10 +124,8 @@ class InputPaidMedia(TelegramObject):
Args:
type (:obj:`str`): Type of media that the instance represents.
- media (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | \
- :class:`telegram.PhotoSize` | :class:`telegram.Video`): File to send. |fileinputnopath|
- Lastly you can pass an existing telegram media object of the corresponding type
- to send.
+ media (:obj:`str` | :class:`~telegram.InputFile`): File
+ to send. |fileinputnopath|
Attributes:
type (:obj:`str`): Type of the input media.
@@ -148,13 +142,13 @@ class InputPaidMedia(TelegramObject):
def __init__(
self,
type: str, # pylint: disable=redefined-builtin
- media: Union[str, InputFile, PhotoSize, Video],
+ media: Union[str, InputFile],
*,
api_kwargs: Optional[JSONDict] = None,
):
super().__init__(api_kwargs=api_kwargs)
self.type: str = enum.get_member(constants.InputPaidMediaType, type, type)
- self.media: Union[str, InputFile, PhotoSize, Video] = media
+ self.media: Union[str, InputFile] = media
self._freeze()
@@ -167,8 +161,8 @@ class InputPaidMediaPhoto(InputPaidMedia):
.. versionadded:: 21.4
Args:
- media (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | \
- :class:`telegram.PhotoSize`): File to send. |fileinputnopath|
+ media (:obj:`str` | :term:`file object` | :class:`~telegram.InputFile` | :obj:`bytes` | \
+ :class:`pathlib.Path` | :class:`telegram.PhotoSize`): File to send. |fileinputnopath|
Lastly you can pass an existing :class:`telegram.PhotoSize` object to send.
Attributes:
@@ -207,11 +201,18 @@ class InputPaidMediaVideo(InputPaidMedia):
changed by Telegram.
Args:
- media (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | \
- :class:`telegram.Video`): File to send. |fileinputnopath|
+ media (:obj:`str` | :term:`file object` | :class:`~telegram.InputFile` | :obj:`bytes` | \
+ :class:`pathlib.Path` | :class:`telegram.Video`): File to send. |fileinputnopath|
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.
@@ -223,6 +224,13 @@ 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.
@@ -230,7 +238,15 @@ class InputPaidMediaVideo(InputPaidMedia):
suitable for streaming.
"""
- __slots__ = ("duration", "height", "supports_streaming", "thumbnail", "width")
+ __slots__ = (
+ "cover",
+ "duration",
+ "height",
+ "start_timestamp",
+ "supports_streaming",
+ "thumbnail",
+ "width",
+ )
def __init__(
self,
@@ -240,6 +256,8 @@ def __init__(
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,
):
@@ -262,6 +280,10 @@ def __init__(
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):
@@ -278,8 +300,8 @@ class InputMediaAnimation(InputMedia):
|removed_thumb_note|
Args:
- media (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | \
- :class:`telegram.Animation`): File to send. |fileinputnopath|
+ media (:obj:`str` | :term:`file object` | :class:`~telegram.InputFile` | :obj:`bytes` | \
+ :class:`pathlib.Path` | :class:`telegram.Animation`): File to send. |fileinputnopath|
Lastly you can pass an existing :class:`telegram.Animation` object to send.
.. versionchanged:: 13.2
@@ -320,7 +342,7 @@ class InputMediaAnimation(InputMedia):
0-:tg-const:`telegram.constants.MessageLimit.CAPTION_LENGTH` characters
after entities parsing.
parse_mode (:obj:`str`): Optional. The parse mode to use for text formatting.
- caption_entities (Tuple[:class:`telegram.MessageEntity`]): Optional. |captionentitiesattr|
+ caption_entities (tuple[:class:`telegram.MessageEntity`]): Optional. |captionentitiesattr|
.. versionchanged:: 20.0
@@ -401,8 +423,8 @@ class InputMediaPhoto(InputMedia):
.. seealso:: :wiki:`Working with Files and Media `
Args:
- media (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | \
- :class:`telegram.PhotoSize`): File to send. |fileinputnopath|
+ media (:obj:`str` | :term:`file object` | :class:`~telegram.InputFile` | :obj:`bytes` | \
+ :class:`pathlib.Path` | :class:`telegram.PhotoSize`): File to send. |fileinputnopath|
Lastly you can pass an existing :class:`telegram.PhotoSize` object to send.
.. versionchanged:: 13.2
@@ -435,7 +457,7 @@ class InputMediaPhoto(InputMedia):
0-:tg-const:`telegram.constants.MessageLimit.CAPTION_LENGTH` characters
after entities parsing.
parse_mode (:obj:`str`): Optional. |parse_mode|
- caption_entities (Tuple[:class:`telegram.MessageEntity`]): Optional. |captionentitiesattr|
+ caption_entities (tuple[:class:`telegram.MessageEntity`]): Optional. |captionentitiesattr|
.. versionchanged:: 20.0
@@ -501,8 +523,8 @@ class InputMediaVideo(InputMedia):
|removed_thumb_note|
Args:
- media (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | \
- :class:`telegram.Video`): File to send. |fileinputnopath|
+ media (:obj:`str` | :term:`file object` | :class:`~telegram.InputFile` | :obj:`bytes` | \
+ :class:`pathlib.Path` | :class:`telegram.Video`): File to send. |fileinputnopath|
Lastly you can pass an existing :class:`telegram.Video` object to send.
.. versionchanged:: 13.2
@@ -534,6 +556,13 @@ 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
@@ -545,7 +574,7 @@ class InputMediaVideo(InputMedia):
0-:tg-const:`telegram.constants.MessageLimit.CAPTION_LENGTH` characters
after entities parsing.
parse_mode (:obj:`str`): Optional. |parse_mode|
- caption_entities (Tuple[:class:`telegram.MessageEntity`]): Optional. |captionentitiesattr|
+ caption_entities (tuple[:class:`telegram.MessageEntity`]): Optional. |captionentitiesattr|
.. versionchanged:: 20.0
@@ -566,13 +595,22 @@ 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",
@@ -592,6 +630,8 @@ def __init__(
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,
):
@@ -623,6 +663,10 @@ def __init__(
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):
@@ -639,8 +683,8 @@ class InputMediaAudio(InputMedia):
|removed_thumb_note|
Args:
- media (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | \
- :class:`telegram.Audio`): File to send. |fileinputnopath|
+ media (:obj:`str` | :term:`file object` | :class:`~telegram.InputFile` | :obj:`bytes` | \
+ :class:`pathlib.Path` | :class:`telegram.Audio`): File to send. |fileinputnopath|
Lastly you can pass an existing :class:`telegram.Audio` object to send.
.. versionchanged:: 13.2
@@ -675,7 +719,7 @@ class InputMediaAudio(InputMedia):
0-:tg-const:`telegram.constants.MessageLimit.CAPTION_LENGTH` characters
after entities parsing.
parse_mode (:obj:`str`): Optional. |parse_mode|
- caption_entities (Tuple[:class:`telegram.MessageEntity`]): Optional. |captionentitiesattr|
+ caption_entities (tuple[:class:`telegram.MessageEntity`]): Optional. |captionentitiesattr|
.. versionchanged:: 20.0
@@ -743,8 +787,8 @@ class InputMediaDocument(InputMedia):
|removed_thumb_note|
Args:
- media (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | \
- :class:`telegram.Document`): File to send. |fileinputnopath|
+ media (:obj:`str` | :term:`file object` | :class:`~telegram.InputFile` | :obj:`bytes` \
+ | :class:`pathlib.Path` | :class:`telegram.Document`): File to send. |fileinputnopath|
Lastly you can pass an existing :class:`telegram.Document` object to send.
.. versionchanged:: 13.2
@@ -778,7 +822,7 @@ class InputMediaDocument(InputMedia):
0-:tg-const:`telegram.constants.MessageLimit.CAPTION_LENGTH` characters
after entities parsing.
parse_mode (:obj:`str`): Optional. |parse_mode|
- caption_entities (Tuple[:class:`telegram.MessageEntity`]): Optional. |captionentitiesattr|
+ caption_entities (tuple[:class:`telegram.MessageEntity`]): Optional. |captionentitiesattr|
.. versionchanged:: 20.0
diff --git a/telegram/_files/inputsticker.py b/telegram/_files/inputsticker.py
index 5539d610d83..00434639778 100644
--- a/telegram/_files/inputsticker.py
+++ b/telegram/_files/inputsticker.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2024
+# Copyright (C) 2015-2025
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -18,7 +18,8 @@
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains an object that represents a Telegram InputSticker."""
-from typing import TYPE_CHECKING, Optional, Sequence, Tuple, Union
+from collections.abc import Sequence
+from typing import TYPE_CHECKING, Optional, Union
from telegram._files.sticker import MaskPosition
from telegram._telegramobject import TelegramObject
@@ -41,14 +42,15 @@ class InputSticker(TelegramObject):
order of the arguments has changed.
Args:
- sticker (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path`): The
+ sticker (:obj:`str` | :term:`file object` | :class:`~telegram.InputFile` | :obj:`bytes` \
+ | :class:`pathlib.Path`): The
added sticker. |uploadinputnopath| Animated and video stickers can't be uploaded via
HTTP URL.
emoji_list (Sequence[:obj:`str`]): Sequence of
:tg-const:`telegram.constants.StickerLimit.MIN_STICKER_EMOJI` -
:tg-const:`telegram.constants.StickerLimit.MAX_STICKER_EMOJI` emoji associated with the
sticker.
- mask_position (:obj:`telegram.MaskPosition`, optional): Position where the mask should be
+ mask_position (:class:`telegram.MaskPosition`, optional): Position where the mask should be
placed on faces. For ":tg-const:`telegram.constants.StickerType.MASK`" stickers only.
keywords (Sequence[:obj:`str`], optional): Sequence of
0-:tg-const:`telegram.constants.StickerLimit.MAX_SEARCH_KEYWORDS` search keywords
@@ -59,20 +61,20 @@ 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
Attributes:
sticker (:obj:`str` | :class:`telegram.InputFile`): The added sticker.
- emoji_list (Tuple[:obj:`str`]): Tuple of
+ emoji_list (tuple[:obj:`str`]): Tuple of
:tg-const:`telegram.constants.StickerLimit.MIN_STICKER_EMOJI` -
:tg-const:`telegram.constants.StickerLimit.MAX_STICKER_EMOJI` emoji associated with the
sticker.
- mask_position (:obj:`telegram.MaskPosition`): Optional. Position where the mask should be
+ mask_position (:class:`telegram.MaskPosition`): Optional. Position where the mask should be
placed on faces. For ":tg-const:`telegram.constants.StickerType.MASK`" stickers only.
- keywords (Tuple[:obj:`str`]): Optional. Tuple of
+ keywords (tuple[:obj:`str`]): Optional. Tuple of
0-:tg-const:`telegram.constants.StickerLimit.MAX_SEARCH_KEYWORDS` search keywords
for the sticker with the total length of up to
:tg-const:`telegram.constants.StickerLimit.MAX_KEYWORD_LENGTH` characters. For
@@ -82,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
"""
@@ -109,9 +111,9 @@ def __init__(
local_mode=True,
attach=True,
)
- self.emoji_list: Tuple[str, ...] = parse_sequence_arg(emoji_list)
+ self.emoji_list: tuple[str, ...] = parse_sequence_arg(emoji_list)
self.format: str = format
self.mask_position: Optional[MaskPosition] = mask_position
- self.keywords: Tuple[str, ...] = parse_sequence_arg(keywords)
+ self.keywords: tuple[str, ...] = parse_sequence_arg(keywords)
self._freeze()
diff --git a/telegram/_files/location.py b/telegram/_files/location.py
index b2e1458d17f..87c895b711a 100644
--- a/telegram/_files/location.py
+++ b/telegram/_files/location.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2024
+# Copyright (C) 2015-2025
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
diff --git a/telegram/_files/photosize.py b/telegram/_files/photosize.py
index e8c8b699ac3..e06dc3bb772 100644
--- a/telegram/_files/photosize.py
+++ b/telegram/_files/photosize.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2024
+# Copyright (C) 2015-2025
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
diff --git a/telegram/_files/sticker.py b/telegram/_files/sticker.py
index 3c3c1cd7e72..0bf63d4b073 100644
--- a/telegram/_files/sticker.py
+++ b/telegram/_files/sticker.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2024
+# Copyright (C) 2015-2025
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -17,7 +17,8 @@
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains objects that represent stickers."""
-from typing import TYPE_CHECKING, Final, Optional, Sequence, Tuple
+from collections.abc import Sequence
+from typing import TYPE_CHECKING, Final, Optional
from telegram import constants
from telegram._files._basethumbedmedium import _BaseThumbedMedium
@@ -25,7 +26,7 @@
from telegram._files.photosize import PhotoSize
from telegram._telegramobject import TelegramObject
from telegram._utils import enum
-from telegram._utils.argumentparsing import parse_sequence_arg
+from telegram._utils.argumentparsing import de_json_optional, de_list_optional, parse_sequence_arg
from telegram._utils.types import JSONDict
if TYPE_CHECKING:
@@ -193,16 +194,13 @@ def __init__(
""":const:`telegram.constants.StickerType.CUSTOM_EMOJI`"""
@classmethod
- def de_json(cls, data: Optional[JSONDict], bot: Optional["Bot"] = None) -> Optional["Sticker"]:
+ def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "Sticker":
"""See :meth:`telegram.TelegramObject.de_json`."""
data = cls._parse_data(data)
- if not data:
- return None
-
- data["thumbnail"] = PhotoSize.de_json(data.get("thumbnail"), bot)
- data["mask_position"] = MaskPosition.de_json(data.get("mask_position"), bot)
- data["premium_animation"] = File.de_json(data.get("premium_animation"), bot)
+ data["thumbnail"] = de_json_optional(data.get("thumbnail"), PhotoSize, bot)
+ data["mask_position"] = de_json_optional(data.get("mask_position"), MaskPosition, bot)
+ data["premium_animation"] = de_json_optional(data.get("premium_animation"), File, bot)
api_kwargs = {}
# This is a deprecated field that TG still returns for backwards compatibility
@@ -259,7 +257,7 @@ class StickerSet(TelegramObject):
Attributes:
name (:obj:`str`): Sticker set name.
title (:obj:`str`): Sticker set title.
- stickers (Tuple[:class:`telegram.Sticker`]): List of all set stickers.
+ stickers (tuple[:class:`telegram.Sticker`]): List of all set stickers.
.. versionchanged:: 20.0
|tupleclassattrs|
@@ -296,7 +294,7 @@ def __init__(
super().__init__(api_kwargs=api_kwargs)
self.name: str = name
self.title: str = title
- self.stickers: Tuple[Sticker, ...] = parse_sequence_arg(stickers)
+ self.stickers: tuple[Sticker, ...] = parse_sequence_arg(stickers)
self.sticker_type: str = sticker_type
# Optional
self.thumbnail: Optional[PhotoSize] = thumbnail
@@ -305,15 +303,12 @@ def __init__(
self._freeze()
@classmethod
- def de_json(
- cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
- ) -> Optional["StickerSet"]:
+ def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "StickerSet":
"""See :meth:`telegram.TelegramObject.de_json`."""
- if not data:
- return None
+ data = cls._parse_data(data)
- data["thumbnail"] = PhotoSize.de_json(data.get("thumbnail"), bot)
- data["stickers"] = Sticker.de_list(data.get("stickers"), bot)
+ data["thumbnail"] = de_json_optional(data.get("thumbnail"), PhotoSize, bot)
+ data["stickers"] = de_list_optional(data.get("stickers"), Sticker, bot)
api_kwargs = {}
# These are deprecated fields that TG still returns for backwards compatibility
diff --git a/telegram/_files/venue.py b/telegram/_files/venue.py
index 443bd009c17..fd9cbdf69f0 100644
--- a/telegram/_files/venue.py
+++ b/telegram/_files/venue.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2024
+# Copyright (C) 2015-2025
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -22,6 +22,7 @@
from telegram._files.location import Location
from telegram._telegramobject import TelegramObject
+from telegram._utils.argumentparsing import de_json_optional
from telegram._utils.types import JSONDict
if TYPE_CHECKING:
@@ -103,13 +104,10 @@ def __init__(
self._freeze()
@classmethod
- def de_json(cls, data: Optional[JSONDict], bot: Optional["Bot"] = None) -> Optional["Venue"]:
+ def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "Venue":
"""See :meth:`telegram.TelegramObject.de_json`."""
data = cls._parse_data(data)
- if not data:
- return None
-
- data["location"] = Location.de_json(data.get("location"), bot)
+ data["location"] = de_json_optional(data.get("location"), Location, bot)
return super().de_json(data=data, bot=bot)
diff --git a/telegram/_files/video.py b/telegram/_files/video.py
index 7a1201c431e..36381ebbf6b 100644
--- a/telegram/_files/video.py
+++ b/telegram/_files/video.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2024
+# Copyright (C) 2015-2025
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -17,12 +17,17 @@
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains an object that represents a Telegram Video."""
-from typing import Optional
+from collections.abc import Sequence
+from typing import TYPE_CHECKING, 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.
@@ -48,6 +53,13 @@ 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
@@ -64,9 +76,24 @@ 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__ = ("duration", "file_name", "height", "mime_type", "width")
+ __slots__ = (
+ "cover",
+ "duration",
+ "file_name",
+ "height",
+ "mime_type",
+ "start_timestamp",
+ "width",
+ )
def __init__(
self,
@@ -79,6 +106,8 @@ def __init__(
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,
):
@@ -97,3 +126,14 @@ def __init__(
# 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)
diff --git a/telegram/_files/videonote.py b/telegram/_files/videonote.py
index 15b23a69bf2..edb9e555372 100644
--- a/telegram/_files/videonote.py
+++ b/telegram/_files/videonote.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2024
+# Copyright (C) 2015-2025
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
diff --git a/telegram/_files/voice.py b/telegram/_files/voice.py
index ae4fa1d6195..19c0e856d14 100644
--- a/telegram/_files/voice.py
+++ b/telegram/_files/voice.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2024
+# Copyright (C) 2015-2025
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
diff --git a/telegram/_forcereply.py b/telegram/_forcereply.py
index cce00996bbd..b24b2719af9 100644
--- a/telegram/_forcereply.py
+++ b/telegram/_forcereply.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2024
+# Copyright (C) 2015-2025
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
diff --git a/telegram/_forumtopic.py b/telegram/_forumtopic.py
index bd66e40d053..81b64e28c8e 100644
--- a/telegram/_forumtopic.py
+++ b/telegram/_forumtopic.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2024
+# Copyright (C) 2015-2025
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
diff --git a/telegram/_games/callbackgame.py b/telegram/_games/callbackgame.py
index 878816b0194..0917a116b7f 100644
--- a/telegram/_games/callbackgame.py
+++ b/telegram/_games/callbackgame.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2024
+# Copyright (C) 2015-2025
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
diff --git a/telegram/_games/game.py b/telegram/_games/game.py
index 93b3f0161cc..bd8cf19caea 100644
--- a/telegram/_games/game.py
+++ b/telegram/_games/game.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2024
+# Copyright (C) 2015-2025
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -17,13 +17,15 @@
# 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 Game."""
-from typing import TYPE_CHECKING, Dict, List, Optional, Sequence, Tuple
+from collections.abc import Sequence
+from typing import TYPE_CHECKING, Optional
from telegram._files.animation import Animation
from telegram._files.photosize import PhotoSize
from telegram._messageentity import MessageEntity
from telegram._telegramobject import TelegramObject
-from telegram._utils.argumentparsing import parse_sequence_arg
+from telegram._utils.argumentparsing import de_json_optional, de_list_optional, parse_sequence_arg
+from telegram._utils.strings import TextEncoding
from telegram._utils.types import JSONDict
if TYPE_CHECKING:
@@ -64,7 +66,7 @@ class Game(TelegramObject):
Attributes:
title (:obj:`str`): Title of the game.
description (:obj:`str`): Description of the game.
- photo (Tuple[:class:`telegram.PhotoSize`]): Photo that will be displayed in the game
+ photo (tuple[:class:`telegram.PhotoSize`]): Photo that will be displayed in the game
message in chats.
.. versionchanged:: 20.0
@@ -75,7 +77,7 @@ class Game(TelegramObject):
when the bot calls :meth:`telegram.Bot.set_game_score`, or manually edited
using :meth:`telegram.Bot.edit_message_text`.
0-:tg-const:`telegram.constants.MessageLimit.MAX_TEXT_LENGTH` characters.
- text_entities (Tuple[:class:`telegram.MessageEntity`]): Optional. Special entities that
+ text_entities (tuple[:class:`telegram.MessageEntity`]): Optional. Special entities that
appear in text, such as usernames, URLs, bot commands, etc.
This tuple is empty if the message does not contain text entities.
@@ -111,10 +113,10 @@ def __init__(
# Required
self.title: str = title
self.description: str = description
- self.photo: Tuple[PhotoSize, ...] = parse_sequence_arg(photo)
+ self.photo: tuple[PhotoSize, ...] = parse_sequence_arg(photo)
# Optionals
self.text: Optional[str] = text
- self.text_entities: Tuple[MessageEntity, ...] = parse_sequence_arg(text_entities)
+ self.text_entities: tuple[MessageEntity, ...] = parse_sequence_arg(text_entities)
self.animation: Optional[Animation] = animation
self._id_attrs = (self.title, self.description, self.photo)
@@ -122,16 +124,13 @@ def __init__(
self._freeze()
@classmethod
- def de_json(cls, data: Optional[JSONDict], bot: Optional["Bot"] = None) -> Optional["Game"]:
+ def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "Game":
"""See :meth:`telegram.TelegramObject.de_json`."""
data = cls._parse_data(data)
- if not data:
- return None
-
- data["photo"] = PhotoSize.de_list(data.get("photo"), bot)
- data["text_entities"] = MessageEntity.de_list(data.get("text_entities"), bot)
- data["animation"] = Animation.de_json(data.get("animation"), bot)
+ data["photo"] = de_list_optional(data.get("photo"), PhotoSize, bot)
+ data["text_entities"] = de_list_optional(data.get("text_entities"), MessageEntity, bot)
+ data["animation"] = de_json_optional(data.get("animation"), Animation, bot)
return super().de_json(data=data, bot=bot)
@@ -157,12 +156,12 @@ def parse_text_entity(self, entity: MessageEntity) -> str:
if not self.text:
raise RuntimeError("This Game has no 'text'.")
- entity_text = self.text.encode("utf-16-le")
+ entity_text = self.text.encode(TextEncoding.UTF_16_LE)
entity_text = entity_text[entity.offset * 2 : (entity.offset + entity.length) * 2]
- return entity_text.decode("utf-16-le")
+ return entity_text.decode(TextEncoding.UTF_16_LE)
- def parse_text_entities(self, types: Optional[List[str]] = None) -> Dict[MessageEntity, str]:
+ def parse_text_entities(self, types: Optional[list[str]] = None) -> dict[MessageEntity, str]:
"""
Returns a :obj:`dict` that maps :class:`telegram.MessageEntity` to :obj:`str`.
It contains entities from this message filtered by their
@@ -175,13 +174,13 @@ def parse_text_entities(self, types: Optional[List[str]] = None) -> Dict[Message
See :attr:`parse_text_entity` for more info.
Args:
- types (List[:obj:`str`], optional): List of :class:`telegram.MessageEntity` types as
+ types (list[:obj:`str`], optional): List of :class:`telegram.MessageEntity` types as
strings. If the :attr:`~telegram.MessageEntity.type` attribute of an entity is
contained in this list, it will be returned. Defaults to
:attr:`telegram.MessageEntity.ALL_TYPES`.
Returns:
- Dict[:class:`telegram.MessageEntity`, :obj:`str`]: A dictionary of entities mapped to
+ dict[:class:`telegram.MessageEntity`, :obj:`str`]: A dictionary of entities mapped to
the text that belongs to them, calculated based on UTF-16 codepoints.
"""
diff --git a/telegram/_games/gamehighscore.py b/telegram/_games/gamehighscore.py
index 40f93fadd49..2866b59fb99 100644
--- a/telegram/_games/gamehighscore.py
+++ b/telegram/_games/gamehighscore.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2024
+# Copyright (C) 2015-2025
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -22,6 +22,7 @@
from telegram._telegramobject import TelegramObject
from telegram._user import User
+from telegram._utils.argumentparsing import de_json_optional
from telegram._utils.types import JSONDict
if TYPE_CHECKING:
@@ -61,15 +62,10 @@ def __init__(
self._freeze()
@classmethod
- def de_json(
- cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
- ) -> Optional["GameHighScore"]:
+ def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "GameHighScore":
"""See :meth:`telegram.TelegramObject.de_json`."""
data = cls._parse_data(data)
- if not data:
- return None
-
- data["user"] = User.de_json(data.get("user"), bot)
+ data["user"] = de_json_optional(data.get("user"), User, bot)
return super().de_json(data=data, bot=bot)
diff --git a/telegram/_gifts.py b/telegram/_gifts.py
new file mode 100644
index 00000000000..d068923c6df
--- /dev/null
+++ b/telegram/_gifts.py
@@ -0,0 +1,147 @@
+#!/usr/bin/env python
+# pylint: disable=redefined-builtin
+#
+# A library that provides a Python interface to the Telegram Bot API
+# Copyright (C) 2015-2025
+# Leandro Toledo de Souza
+#
+# 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 classes related to gifs sent by bots."""
+from collections.abc import Sequence
+from typing import TYPE_CHECKING, Optional
+
+from telegram._files.sticker import Sticker
+from telegram._telegramobject import TelegramObject
+from telegram._utils.argumentparsing import de_json_optional, de_list_optional, parse_sequence_arg
+from telegram._utils.types import JSONDict
+
+if TYPE_CHECKING:
+ from telegram import Bot
+
+
+class Gift(TelegramObject):
+ """This object represents a gift that can be sent by the bot.
+
+ Objects of this class are comparable in terms of equality. Two objects of this class are
+ considered equal if their :attr:`id` is equal.
+
+ .. versionadded:: 21.8
+
+ Args:
+ id (:obj:`str`): Unique identifier of the gift
+ sticker (:class:`~telegram.Sticker`): The sticker that represents the gift
+ star_count (:obj:`int`): The number of Telegram Stars that must be paid to send the sticker
+ total_count (:obj:`int`, optional): The total number of the gifts of this type that can be
+ sent; for limited gifts only
+ remaining_count (:obj:`int`, optional): The number of remaining gifts of this type that can
+ be sent; for limited gifts only
+ upgrade_star_count (:obj:`int`, optional): The number of Telegram Stars that must be paid
+ to upgrade the gift to a unique one
+
+ .. versionadded:: 21.10
+
+ Attributes:
+ id (:obj:`str`): Unique identifier of the gift
+ sticker (:class:`~telegram.Sticker`): The sticker that represents the gift
+ star_count (:obj:`int`): The number of Telegram Stars that must be paid to send the sticker
+ total_count (:obj:`int`): Optional. The total number of the gifts of this type that can be
+ sent; for limited gifts only
+ remaining_count (:obj:`int`): Optional. The number of remaining gifts of this type that can
+ be sent; for limited gifts only
+ upgrade_star_count (:obj:`int`): Optional. The number of Telegram Stars that must be paid
+ to upgrade the gift to a unique one
+
+ .. versionadded:: 21.10
+
+ """
+
+ __slots__ = (
+ "id",
+ "remaining_count",
+ "star_count",
+ "sticker",
+ "total_count",
+ "upgrade_star_count",
+ )
+
+ def __init__(
+ self,
+ id: str,
+ sticker: Sticker,
+ star_count: int,
+ total_count: Optional[int] = None,
+ remaining_count: Optional[int] = None,
+ upgrade_star_count: Optional[int] = None,
+ *,
+ api_kwargs: Optional[JSONDict] = None,
+ ):
+ super().__init__(api_kwargs=api_kwargs)
+ self.id: str = id
+ self.sticker: Sticker = sticker
+ self.star_count: int = star_count
+ self.total_count: Optional[int] = total_count
+ self.remaining_count: Optional[int] = remaining_count
+ self.upgrade_star_count: Optional[int] = upgrade_star_count
+
+ self._id_attrs = (self.id,)
+
+ self._freeze()
+
+ @classmethod
+ def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "Gift":
+ """See :meth:`telegram.TelegramObject.de_json`."""
+ data = cls._parse_data(data)
+
+ data["sticker"] = de_json_optional(data.get("sticker"), Sticker, bot)
+ return super().de_json(data=data, bot=bot)
+
+
+class Gifts(TelegramObject):
+ """This object represent a list of gifts.
+
+ Objects of this class are comparable in terms of equality. Two objects of this class are
+ considered equal if their :attr:`gifts` are equal.
+
+ .. versionadded:: 21.8
+
+ Args:
+ gifts (Sequence[:class:`Gift`]): The sequence of gifts
+
+ Attributes:
+ gifts (tuple[:class:`Gift`]): The sequence of gifts
+
+ """
+
+ __slots__ = ("gifts",)
+
+ def __init__(
+ self,
+ gifts: Sequence[Gift],
+ *,
+ api_kwargs: Optional[JSONDict] = None,
+ ):
+ super().__init__(api_kwargs=api_kwargs)
+ self.gifts: tuple[Gift, ...] = parse_sequence_arg(gifts)
+
+ self._id_attrs = (self.gifts,)
+
+ self._freeze()
+
+ @classmethod
+ def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "Gifts":
+ """See :meth:`telegram.TelegramObject.de_json`."""
+ data = cls._parse_data(data)
+
+ data["gifts"] = de_list_optional(data.get("gifts"), Gift, bot)
+ return super().de_json(data=data, bot=bot)
diff --git a/telegram/_giveaway.py b/telegram/_giveaway.py
index 0008dc9dd4c..d7d086e6548 100644
--- a/telegram/_giveaway.py
+++ b/telegram/_giveaway.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2024
+# Copyright (C) 2015-2025
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -17,13 +17,14 @@
# 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 objects that are related to Telegram giveaways."""
-import datetime
-from typing import TYPE_CHECKING, Optional, Sequence, Tuple
+import datetime as dtm
+from collections.abc import Sequence
+from typing import TYPE_CHECKING, Optional
from telegram._chat import Chat
from telegram._telegramobject import TelegramObject
from telegram._user import User
-from telegram._utils.argumentparsing import parse_sequence_arg
+from telegram._utils.argumentparsing import de_json_optional, de_list_optional, parse_sequence_arg
from telegram._utils.datetime import extract_tzinfo_from_defaults, from_timestamp
from telegram._utils.types import JSONDict
@@ -41,7 +42,7 @@ class Giveaway(TelegramObject):
.. versionadded:: 20.8
Args:
- chats (Tuple[:class:`telegram.Chat`]): The list of chats which the user must join to
+ chats (tuple[:class:`telegram.Chat`]): The list of chats which the user must join to
participate in the giveaway.
winners_selection_date (:class:`datetime.datetime`): The date when the giveaway winner will
be selected. |datetime_localization|
@@ -56,8 +57,13 @@ class Giveaway(TelegramObject):
country codes indicating the countries from which eligible users for the giveaway must
come. If empty, then all users can participate in the giveaway. Users with a phone
number that was bought on Fragment can always participate in giveaways.
+ prize_star_count (:obj:`int`, optional): The number of Telegram Stars to be split between
+ giveaway winners; for Telegram Star giveaways only.
+
+ .. versionadded:: 21.6
premium_subscription_month_count (:obj:`int`, optional): The number of months the Telegram
- Premium subscription won from the giveaway will be active for.
+ Premium subscription won from the giveaway will be active for; for Telegram Premium
+ giveaways only.
Attributes:
chats (Sequence[:class:`telegram.Chat`]): The list of chats which the user must join to
@@ -71,12 +77,17 @@ class Giveaway(TelegramObject):
has_public_winners (:obj:`True`): Optional. :obj:`True`, if the list of giveaway winners
will be visible to everyone
prize_description (:obj:`str`): Optional. Description of additional giveaway prize
- country_codes (Tuple[:obj:`str`]): Optional. A tuple of two-letter ISO 3166-1 alpha-2
+ country_codes (tuple[:obj:`str`]): Optional. A tuple of two-letter ISO 3166-1 alpha-2
country codes indicating the countries from which eligible users for the giveaway must
come. If empty, then all users can participate in the giveaway. Users with a phone
number that was bought on Fragment can always participate in giveaways.
+ prize_star_count (:obj:`int`): Optional. The number of Telegram Stars to be split between
+ giveaway winners; for Telegram Star giveaways only.
+
+ .. versionadded:: 21.6
premium_subscription_month_count (:obj:`int`): Optional. The number of months the Telegram
- Premium subscription won from the giveaway will be active for.
+ Premium subscription won from the giveaway will be active for; for Telegram Premium
+ giveaways only.
"""
__slots__ = (
@@ -86,6 +97,7 @@ class Giveaway(TelegramObject):
"only_new_members",
"premium_subscription_month_count",
"prize_description",
+ "prize_star_count",
"winner_count",
"winners_selection_date",
)
@@ -93,26 +105,28 @@ class Giveaway(TelegramObject):
def __init__(
self,
chats: Sequence[Chat],
- winners_selection_date: datetime.datetime,
+ winners_selection_date: dtm.datetime,
winner_count: int,
only_new_members: Optional[bool] = None,
has_public_winners: Optional[bool] = None,
prize_description: Optional[str] = None,
country_codes: Optional[Sequence[str]] = None,
premium_subscription_month_count: Optional[int] = None,
+ prize_star_count: Optional[int] = None,
*,
api_kwargs: Optional[JSONDict] = None,
):
super().__init__(api_kwargs=api_kwargs)
- self.chats: Tuple[Chat, ...] = tuple(chats)
- self.winners_selection_date: datetime.datetime = winners_selection_date
+ self.chats: tuple[Chat, ...] = tuple(chats)
+ self.winners_selection_date: dtm.datetime = winners_selection_date
self.winner_count: int = winner_count
self.only_new_members: Optional[bool] = only_new_members
self.has_public_winners: Optional[bool] = has_public_winners
self.prize_description: Optional[str] = prize_description
- self.country_codes: Tuple[str, ...] = parse_sequence_arg(country_codes)
+ self.country_codes: tuple[str, ...] = parse_sequence_arg(country_codes)
self.premium_subscription_month_count: Optional[int] = premium_subscription_month_count
+ self.prize_star_count: Optional[int] = prize_star_count
self._id_attrs = (
self.chats,
@@ -123,19 +137,14 @@ def __init__(
self._freeze()
@classmethod
- def de_json(
- cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
- ) -> Optional["Giveaway"]:
- """See :obj:`telegram.TelegramObject.de_json`."""
+ def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "Giveaway":
+ """See :meth:`telegram.TelegramObject.de_json`."""
data = cls._parse_data(data)
- if data is None:
- return None
-
# Get the local timezone from the bot if it has defaults
loc_tzinfo = extract_tzinfo_from_defaults(bot)
- data["chats"] = tuple(Chat.de_list(data.get("chats"), bot))
+ data["chats"] = de_list_optional(data.get("chats"), Chat, bot)
data["winners_selection_date"] = from_timestamp(
data.get("winners_selection_date"), tzinfo=loc_tzinfo
)
@@ -145,13 +154,28 @@ def de_json(
class GiveawayCreated(TelegramObject):
"""This object represents a service message about the creation of a scheduled giveaway.
- Currently holds no information.
+
+ Args:
+ prize_star_count (:obj:`int`, optional): The number of Telegram Stars to be
+ split between giveaway winners; for Telegram Star giveaways only.
+
+ .. versionadded:: 21.6
+
+ Attributes:
+ prize_star_count (:obj:`int`): Optional. The number of Telegram Stars to be
+ split between giveaway winners; for Telegram Star giveaways only.
+
+ .. versionadded:: 21.6
+
"""
- __slots__ = ()
+ __slots__ = ("prize_star_count",)
- def __init__(self, *, api_kwargs: Optional[JSONDict] = None):
+ def __init__(
+ self, prize_star_count: Optional[int] = None, *, api_kwargs: Optional[JSONDict] = None
+ ):
super().__init__(api_kwargs=api_kwargs)
+ self.prize_star_count: Optional[int] = prize_star_count
self._freeze()
@@ -173,6 +197,10 @@ class GiveawayWinners(TelegramObject):
winner_count (:obj:`int`): Total number of winners in the giveaway
winners (Sequence[:class:`telegram.User`]): List of up to
:tg-const:`telegram.constants.GiveawayLimit.MAX_WINNERS` winners of the giveaway
+ prize_star_count (:obj:`int`, optional): The number of Telegram Stars to be split between
+ giveaway winners; for Telegram Star giveaways only.
+
+ .. versionadded:: 21.6
additional_chat_count (:obj:`int`, optional): The number of other chats the user had to
join in order to be eligible for the giveaway
premium_subscription_month_count (:obj:`int`, optional): The number of months the Telegram
@@ -190,10 +218,14 @@ class GiveawayWinners(TelegramObject):
winners_selection_date (:class:`datetime.datetime`): Point in time when winners of the
giveaway were selected. |datetime_localization|
winner_count (:obj:`int`): Total number of winners in the giveaway
- winners (Tuple[:class:`telegram.User`]): tuple of up to
+ winners (tuple[:class:`telegram.User`]): tuple of up to
:tg-const:`telegram.constants.GiveawayLimit.MAX_WINNERS` winners of the giveaway
additional_chat_count (:obj:`int`): Optional. The number of other chats the user had to
join in order to be eligible for the giveaway
+ prize_star_count (:obj:`int`): Optional. The number of Telegram Stars to be split between
+ giveaway winners; for Telegram Star giveaways only.
+
+ .. versionadded:: 21.6
premium_subscription_month_count (:obj:`int`): Optional. The number of months the Telegram
Premium subscription won from the giveaway will be active for
unclaimed_prize_count (:obj:`int`): Optional. Number of undistributed prizes
@@ -211,6 +243,7 @@ class GiveawayWinners(TelegramObject):
"only_new_members",
"premium_subscription_month_count",
"prize_description",
+ "prize_star_count",
"unclaimed_prize_count",
"was_refunded",
"winner_count",
@@ -222,7 +255,7 @@ def __init__(
self,
chat: Chat,
giveaway_message_id: int,
- winners_selection_date: datetime.datetime,
+ winners_selection_date: dtm.datetime,
winner_count: int,
winners: Sequence[User],
additional_chat_count: Optional[int] = None,
@@ -231,6 +264,7 @@ def __init__(
only_new_members: Optional[bool] = None,
was_refunded: Optional[bool] = None,
prize_description: Optional[str] = None,
+ prize_star_count: Optional[int] = None,
*,
api_kwargs: Optional[JSONDict] = None,
):
@@ -238,15 +272,16 @@ def __init__(
self.chat: Chat = chat
self.giveaway_message_id: int = giveaway_message_id
- self.winners_selection_date: datetime.datetime = winners_selection_date
+ self.winners_selection_date: dtm.datetime = winners_selection_date
self.winner_count: int = winner_count
- self.winners: Tuple[User, ...] = tuple(winners)
+ self.winners: tuple[User, ...] = tuple(winners)
self.additional_chat_count: Optional[int] = additional_chat_count
self.premium_subscription_month_count: Optional[int] = premium_subscription_month_count
self.unclaimed_prize_count: Optional[int] = unclaimed_prize_count
self.only_new_members: Optional[bool] = only_new_members
self.was_refunded: Optional[bool] = was_refunded
self.prize_description: Optional[str] = prize_description
+ self.prize_star_count: Optional[int] = prize_star_count
self._id_attrs = (
self.chat,
@@ -259,20 +294,15 @@ def __init__(
self._freeze()
@classmethod
- def de_json(
- cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
- ) -> Optional["GiveawayWinners"]:
- """See :obj:`telegram.TelegramObject.de_json`."""
+ def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "GiveawayWinners":
+ """See :meth:`telegram.TelegramObject.de_json`."""
data = cls._parse_data(data)
- if data is None:
- return None
-
# Get the local timezone from the bot if it has defaults
loc_tzinfo = extract_tzinfo_from_defaults(bot)
- data["chat"] = Chat.de_json(data.get("chat"), bot)
- data["winners"] = tuple(User.de_list(data.get("winners"), bot))
+ data["chat"] = de_json_optional(data.get("chat"), Chat, bot)
+ data["winners"] = de_list_optional(data.get("winners"), User, bot)
data["winners_selection_date"] = from_timestamp(
data.get("winners_selection_date"), tzinfo=loc_tzinfo
)
@@ -295,21 +325,29 @@ class GiveawayCompleted(TelegramObject):
unclaimed_prize_count (:obj:`int`, optional): Number of undistributed prizes
giveaway_message (:class:`telegram.Message`, optional): Message with the giveaway that was
completed, if it wasn't deleted
+ is_star_giveaway (:obj:`bool`, optional): :obj:`True`, if the giveaway is a Telegram Star
+ giveaway. Otherwise, currently, the giveaway is a Telegram Premium giveaway.
+ .. versionadded:: 21.6
Attributes:
winner_count (:obj:`int`): Number of winners in the giveaway
unclaimed_prize_count (:obj:`int`): Optional. Number of undistributed prizes
giveaway_message (:class:`telegram.Message`): Optional. Message with the giveaway that was
completed, if it wasn't deleted
+ is_star_giveaway (:obj:`bool`): Optional. :obj:`True`, if the giveaway is a Telegram Star
+ giveaway. Otherwise, currently, the giveaway is a Telegram Premium giveaway.
+
+ .. versionadded:: 21.6
"""
- __slots__ = ("giveaway_message", "unclaimed_prize_count", "winner_count")
+ __slots__ = ("giveaway_message", "is_star_giveaway", "unclaimed_prize_count", "winner_count")
def __init__(
self,
winner_count: int,
unclaimed_prize_count: Optional[int] = None,
giveaway_message: Optional["Message"] = None,
+ is_star_giveaway: Optional[bool] = None,
*,
api_kwargs: Optional[JSONDict] = None,
):
@@ -318,6 +356,7 @@ def __init__(
self.winner_count: int = winner_count
self.unclaimed_prize_count: Optional[int] = unclaimed_prize_count
self.giveaway_message: Optional[Message] = giveaway_message
+ self.is_star_giveaway: Optional[bool] = is_star_giveaway
self._id_attrs = (
self.winner_count,
@@ -327,18 +366,13 @@ def __init__(
self._freeze()
@classmethod
- def de_json(
- cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
- ) -> Optional["GiveawayCompleted"]:
- """See :obj:`telegram.TelegramObject.de_json`."""
+ def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "GiveawayCompleted":
+ """See :meth:`telegram.TelegramObject.de_json`."""
data = cls._parse_data(data)
- if data is None:
- return None
-
# Unfortunately, this needs to be here due to cyclic imports
from telegram._message import Message # pylint: disable=import-outside-toplevel
- data["giveaway_message"] = Message.de_json(data.get("giveaway_message"), bot)
+ data["giveaway_message"] = de_json_optional(data.get("giveaway_message"), Message, bot)
return super().de_json(data=data, bot=bot)
diff --git a/telegram/_inline/inlinekeyboardbutton.py b/telegram/_inline/inlinekeyboardbutton.py
index 6ceca1a311b..07d0eed3b2d 100644
--- a/telegram/_inline/inlinekeyboardbutton.py
+++ b/telegram/_inline/inlinekeyboardbutton.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2024
+# Copyright (C) 2015-2025
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -21,10 +21,12 @@
from typing import TYPE_CHECKING, Final, Optional, Union
from telegram import constants
+from telegram._copytextbutton import CopyTextButton
from telegram._games.callbackgame import CallbackGame
from telegram._loginurl import LoginUrl
from telegram._switchinlinequerychosenchat import SwitchInlineQueryChosenChat
from telegram._telegramobject import TelegramObject
+from telegram._utils.argumentparsing import de_json_optional
from telegram._utils.types import JSONDict
from telegram._webappinfo import WebAppInfo
@@ -99,7 +101,7 @@ class InlineKeyboardButton(TelegramObject):
.. seealso:: :wiki:`Arbitrary callback_data `
- web_app (:obj:`telegram.WebAppInfo`, optional): Description of the `Web App
+ web_app (:class:`telegram.WebAppInfo`, optional): Description of the `Web App
`_ that will be launched when the user presses
the button. The Web App will be able to send an arbitrary message on behalf of the user
using the method :meth:`~telegram.Bot.answer_web_app_query`. Available only in
@@ -107,16 +109,14 @@ class InlineKeyboardButton(TelegramObject):
a Telegram Business account.
.. versionadded:: 20.0
- switch_inline_query (:obj:`str`, optional): If set, pressing the button will insert the
- bot's username and the specified inline query in the current chat's input field. May be
- empty, in which case only the bot's username will be inserted.
-
- This offers a quick way for the user to open your bot in inline mode in the same chat -
- good for selecting something from multiple options. Not supported in channels and for
- messages sent on behalf of a Telegram Business account.
+ switch_inline_query (:obj:`str`, optional): If set, pressing the button will prompt the
+ user to select one of their chats, open that chat and insert the bot's username and the
+ specified inline query in the input field. May be empty, in which case just the bot's
+ username will be inserted. Not supported for messages sent on behalf of a Telegram
+ Business account.
Tip:
- This is similar to the new parameter :paramref:`switch_inline_query_chosen_chat`,
+ This is similar to the parameter :paramref:`switch_inline_query_chosen_chat`,
but gives no control over which chats can be selected.
switch_inline_query_current_chat (:obj:`str`, optional): If set, pressing the button will
insert the bot's username and the specified inline query in the current chat's input
@@ -125,6 +125,10 @@ class InlineKeyboardButton(TelegramObject):
This offers a quick way for the user to open your bot in inline mode in the same chat
- good for selecting something from multiple options. Not supported in channels and for
messages sent on behalf of a Telegram Business account.
+ copy_text (:class:`telegram.CopyTextButton`, optional): Description of the button that
+ copies the specified text to the clipboard.
+
+ .. versionadded:: 21.7
callback_game (:class:`telegram.CallbackGame`, optional): Description of the game that will
be launched when the user presses the button
@@ -137,7 +141,7 @@ class InlineKeyboardButton(TelegramObject):
Note:
This type of button **must** always be the first button in the first row and can
only be used in invoice messages.
- switch_inline_query_chosen_chat (:obj:`telegram.SwitchInlineQueryChosenChat`, optional):
+ switch_inline_query_chosen_chat (:class:`telegram.SwitchInlineQueryChosenChat`, optional):
If set, pressing the button will prompt the user to select one of their chats of the
specified type, open that chat and insert the bot's username and the specified inline
query in the input field. Not supported for messages sent on behalf of a Telegram
@@ -170,7 +174,7 @@ class InlineKeyboardButton(TelegramObject):
to the bot when the button is pressed, UTF-8
:tg-const:`telegram.InlineKeyboardButton.MIN_CALLBACK_DATA`-
:tg-const:`telegram.InlineKeyboardButton.MAX_CALLBACK_DATA` bytes.
- web_app (:obj:`telegram.WebAppInfo`): Optional. Description of the `Web App
+ web_app (:class:`telegram.WebAppInfo`): Optional. Description of the `Web App
`_ that will be launched when the user presses
the button. The Web App will be able to send an arbitrary message on behalf of the user
using the method :meth:`~telegram.Bot.answer_web_app_query`. Available only in
@@ -178,16 +182,14 @@ class InlineKeyboardButton(TelegramObject):
a Telegram Business account.
.. versionadded:: 20.0
- switch_inline_query (:obj:`str`): Optional. If set, pressing the button will insert the
- bot's username and the specified inline query in the current chat's input field. May be
- empty, in which case only the bot's username will be inserted.
-
- This offers a quick way for the user to open your bot in inline mode in the same chat -
- good for selecting something from multiple options. Not supported in channels and for
- messages sent on behalf of a Telegram Business account.
+ switch_inline_query (:obj:`str`): Optional. If set, pressing the button will prompt the
+ user to select one of their chats, open that chat and insert the bot's username and the
+ specified inline query in the input field. May be empty, in which case just the bot's
+ username will be inserted. Not supported for messages sent on behalf of a Telegram
+ Business account.
Tip:
- This is similar to the new parameter :paramref:`switch_inline_query_chosen_chat`,
+ This is similar to the parameter :paramref:`switch_inline_query_chosen_chat`,
but gives no control over which chats can be selected.
switch_inline_query_current_chat (:obj:`str`): Optional. If set, pressing the button will
insert the bot's username and the specified inline query in the current chat's input
@@ -196,6 +198,10 @@ class InlineKeyboardButton(TelegramObject):
This offers a quick way for the user to open your bot in inline mode in the same chat
- good for selecting something from multiple options. Not supported in channels and for
messages sent on behalf of a Telegram Business account.
+ copy_text (:class:`telegram.CopyTextButton`): Optional. Description of the button that
+ copies the specified text to the clipboard.
+
+ .. versionadded:: 21.7
callback_game (:class:`telegram.CallbackGame`): Optional. Description of the game that will
be launched when the user presses the button.
@@ -208,7 +214,7 @@ class InlineKeyboardButton(TelegramObject):
Note:
This type of button **must** always be the first button in the first row and can
only be used in invoice messages.
- switch_inline_query_chosen_chat (:obj:`telegram.SwitchInlineQueryChosenChat`): Optional.
+ switch_inline_query_chosen_chat (:class:`telegram.SwitchInlineQueryChosenChat`): Optional.
If set, pressing the button will prompt the user to select one of their chats of the
specified type, open that chat and insert the bot's username and the specified inline
query in the input field. Not supported for messages sent on behalf of a Telegram
@@ -228,6 +234,7 @@ class InlineKeyboardButton(TelegramObject):
__slots__ = (
"callback_data",
"callback_game",
+ "copy_text",
"login_url",
"pay",
"switch_inline_query",
@@ -250,6 +257,7 @@ def __init__(
login_url: Optional[LoginUrl] = None,
web_app: Optional[WebAppInfo] = None,
switch_inline_query_chosen_chat: Optional[SwitchInlineQueryChosenChat] = None,
+ copy_text: Optional[CopyTextButton] = None,
*,
api_kwargs: Optional[JSONDict] = None,
):
@@ -269,6 +277,7 @@ def __init__(
self.switch_inline_query_chosen_chat: Optional[SwitchInlineQueryChosenChat] = (
switch_inline_query_chosen_chat
)
+ self.copy_text: Optional[CopyTextButton] = copy_text
self._id_attrs = ()
self._set_id_attrs()
@@ -288,21 +297,17 @@ def _set_id_attrs(self) -> None:
)
@classmethod
- def de_json(
- cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
- ) -> Optional["InlineKeyboardButton"]:
+ def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "InlineKeyboardButton":
"""See :meth:`telegram.TelegramObject.de_json`."""
data = cls._parse_data(data)
- if not data:
- return None
-
- data["login_url"] = LoginUrl.de_json(data.get("login_url"), bot)
- data["web_app"] = WebAppInfo.de_json(data.get("web_app"), bot)
- data["callback_game"] = CallbackGame.de_json(data.get("callback_game"), bot)
- data["switch_inline_query_chosen_chat"] = SwitchInlineQueryChosenChat.de_json(
- data.get("switch_inline_query_chosen_chat"), bot
+ data["login_url"] = de_json_optional(data.get("login_url"), LoginUrl, bot)
+ data["web_app"] = de_json_optional(data.get("web_app"), WebAppInfo, bot)
+ data["callback_game"] = de_json_optional(data.get("callback_game"), CallbackGame, bot)
+ data["switch_inline_query_chosen_chat"] = de_json_optional(
+ data.get("switch_inline_query_chosen_chat"), SwitchInlineQueryChosenChat, bot
)
+ data["copy_text"] = de_json_optional(data.get("copy_text"), CopyTextButton, bot)
return super().de_json(data=data, bot=bot)
diff --git a/telegram/_inline/inlinekeyboardmarkup.py b/telegram/_inline/inlinekeyboardmarkup.py
index 6857e4d8e3a..64fd8b49124 100644
--- a/telegram/_inline/inlinekeyboardmarkup.py
+++ b/telegram/_inline/inlinekeyboardmarkup.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2024
+# Copyright (C) 2015-2025
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -17,7 +17,8 @@
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains an object that represents a Telegram InlineKeyboardMarkup."""
-from typing import TYPE_CHECKING, Optional, Sequence, Tuple
+from collections.abc import Sequence
+from typing import TYPE_CHECKING, Optional
from telegram._inline.inlinekeyboardbutton import InlineKeyboardButton
from telegram._telegramobject import TelegramObject
@@ -57,7 +58,7 @@ class InlineKeyboardMarkup(TelegramObject):
|sequenceclassargs|
Attributes:
- inline_keyboard (Tuple[Tuple[:class:`telegram.InlineKeyboardButton`]]): Tuple of
+ inline_keyboard (tuple[tuple[:class:`telegram.InlineKeyboardButton`]]): Tuple of
button rows, each represented by a tuple of :class:`~telegram.InlineKeyboardButton`
objects.
@@ -81,7 +82,7 @@ def __init__(
"InlineKeyboardButtons"
)
# Required
- self.inline_keyboard: Tuple[Tuple[InlineKeyboardButton, ...], ...] = tuple(
+ self.inline_keyboard: tuple[tuple[InlineKeyboardButton, ...], ...] = tuple(
tuple(row) for row in inline_keyboard
)
@@ -90,12 +91,8 @@ def __init__(
self._freeze()
@classmethod
- def de_json(
- cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
- ) -> Optional["InlineKeyboardMarkup"]:
+ def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "InlineKeyboardMarkup":
"""See :meth:`telegram.TelegramObject.de_json`."""
- if not data:
- return None
keyboard = []
for row in data["inline_keyboard"]:
diff --git a/telegram/_inline/inlinequery.py b/telegram/_inline/inlinequery.py
index ba29a8646fe..73bb3b43b4d 100644
--- a/telegram/_inline/inlinequery.py
+++ b/telegram/_inline/inlinequery.py
@@ -2,7 +2,7 @@
# pylint: disable=too-many-arguments
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2024
+# Copyright (C) 2015-2025
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -19,15 +19,17 @@
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains an object that represents a Telegram InlineQuery."""
-from typing import TYPE_CHECKING, Callable, Final, Optional, Sequence, Union
+from collections.abc import Sequence
+from typing import TYPE_CHECKING, Callable, Final, Optional, Union
from telegram import constants
from telegram._files.location import Location
from telegram._inline.inlinequeryresultsbutton import InlineQueryResultsButton
from telegram._telegramobject import TelegramObject
from telegram._user import User
+from telegram._utils.argumentparsing import de_json_optional
from telegram._utils.defaultvalue import DEFAULT_NONE
-from telegram._utils.types import JSONDict, ODVInput
+from telegram._utils.types import JSONDict, ODVInput, TimePeriod
if TYPE_CHECKING:
from telegram import Bot, InlineQueryResult
@@ -60,6 +62,12 @@ 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.
@@ -125,17 +133,12 @@ def __init__(
self._freeze()
@classmethod
- def de_json(
- cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
- ) -> Optional["InlineQuery"]:
+ def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "InlineQuery":
"""See :meth:`telegram.TelegramObject.de_json`."""
data = cls._parse_data(data)
- if not data:
- return None
-
- data["from_user"] = User.de_json(data.pop("from", None), bot)
- data["location"] = Location.de_json(data.get("location"), bot)
+ data["from_user"] = de_json_optional(data.pop("from", None), User, bot)
+ data["location"] = de_json_optional(data.get("location"), Location, bot)
return super().de_json(data=data, bot=bot)
@@ -144,7 +147,7 @@ async def answer(
results: Union[
Sequence["InlineQueryResult"], Callable[[int], Optional[Sequence["InlineQueryResult"]]]
],
- cache_time: Optional[int] = None,
+ cache_time: Optional[TimePeriod] = None,
is_personal: Optional[bool] = None,
next_offset: Optional[str] = None,
button: Optional[InlineQueryResultsButton] = None,
@@ -205,16 +208,6 @@ async def answer(
.. 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`
diff --git a/telegram/_inline/inlinequeryresult.py b/telegram/_inline/inlinequeryresult.py
index 534d255c305..67ce6e421f3 100644
--- a/telegram/_inline/inlinequeryresult.py
+++ b/telegram/_inline/inlinequeryresult.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2024
+# Copyright (C) 2015-2025
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
diff --git a/telegram/_inline/inlinequeryresultarticle.py b/telegram/_inline/inlinequeryresultarticle.py
index 92c358e77ef..784fc8fac78 100644
--- a/telegram/_inline/inlinequeryresultarticle.py
+++ b/telegram/_inline/inlinequeryresultarticle.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2024
+# Copyright (C) 2015-2025
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -38,6 +38,9 @@ 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`-
@@ -48,8 +51,9 @@ 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.
description (:obj:`str`, optional): Short description of the result.
thumbnail_url (:obj:`str`, optional): Url of the thumbnail for the result.
@@ -72,8 +76,6 @@ 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.
description (:obj:`str`): Optional. Short description of the result.
thumbnail_url (:obj:`str`): Optional. Url of the thumbnail for the result.
@@ -89,7 +91,6 @@ class InlineQueryResultArticle(InlineQueryResult):
__slots__ = (
"description",
- "hide_url",
"input_message_content",
"reply_markup",
"thumbnail_height",
@@ -106,7 +107,6 @@ def __init__(
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,7 +123,6 @@ def __init__(
# Optional
self.reply_markup: Optional[InlineKeyboardMarkup] = reply_markup
self.url: Optional[str] = url
- 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
diff --git a/telegram/_inline/inlinequeryresultaudio.py b/telegram/_inline/inlinequeryresultaudio.py
index 69353967adc..8e3376a458f 100644
--- a/telegram/_inline/inlinequeryresultaudio.py
+++ b/telegram/_inline/inlinequeryresultaudio.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2024
+# Copyright (C) 2015-2025
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -17,7 +17,8 @@
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains the classes that represent Telegram InlineQueryResultAudio."""
-from typing import TYPE_CHECKING, Optional, Sequence, Tuple
+from collections.abc import Sequence
+from typing import TYPE_CHECKING, Optional
from telegram._inline.inlinekeyboardmarkup import InlineKeyboardMarkup
from telegram._inline.inlinequeryresult import InlineQueryResult
@@ -73,7 +74,7 @@ class InlineQueryResultAudio(InlineQueryResult):
0-:tg-const:`telegram.constants.MessageLimit.CAPTION_LENGTH` characters after entities
parsing.
parse_mode (:obj:`str`): Optional. |parse_mode|
- caption_entities (Tuple[:class:`telegram.MessageEntity`]): Optional. |captionentitiesattr|
+ caption_entities (tuple[:class:`telegram.MessageEntity`]): Optional. |captionentitiesattr|
.. versionchanged:: 20.0
@@ -124,6 +125,6 @@ def __init__(
self.audio_duration: Optional[int] = audio_duration
self.caption: Optional[str] = caption
self.parse_mode: ODVInput[str] = parse_mode
- self.caption_entities: Tuple[MessageEntity, ...] = parse_sequence_arg(caption_entities)
+ self.caption_entities: tuple[MessageEntity, ...] = parse_sequence_arg(caption_entities)
self.reply_markup: Optional[InlineKeyboardMarkup] = reply_markup
self.input_message_content: Optional[InputMessageContent] = input_message_content
diff --git a/telegram/_inline/inlinequeryresultcachedaudio.py b/telegram/_inline/inlinequeryresultcachedaudio.py
index 2fb7cdbb54d..f1f75a12a6e 100644
--- a/telegram/_inline/inlinequeryresultcachedaudio.py
+++ b/telegram/_inline/inlinequeryresultcachedaudio.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2024
+# Copyright (C) 2015-2025
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -17,7 +17,8 @@
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains the classes that represent Telegram InlineQueryResultCachedAudio."""
-from typing import TYPE_CHECKING, Optional, Sequence, Tuple
+from collections.abc import Sequence
+from typing import TYPE_CHECKING, Optional
from telegram._inline.inlinekeyboardmarkup import InlineKeyboardMarkup
from telegram._inline.inlinequeryresult import InlineQueryResult
@@ -68,7 +69,7 @@ class InlineQueryResultCachedAudio(InlineQueryResult):
0-:tg-const:`telegram.constants.MessageLimit.CAPTION_LENGTH` characters after entities
parsing.
parse_mode (:obj:`str`): Optional. |parse_mode|
- caption_entities (Tuple[:class:`telegram.MessageEntity`]): Optional. |captionentitiesattr|
+ caption_entities (tuple[:class:`telegram.MessageEntity`]): Optional. |captionentitiesattr|
.. versionchanged:: 20.0
@@ -110,6 +111,6 @@ def __init__(
# Optionals
self.caption: Optional[str] = caption
self.parse_mode: ODVInput[str] = parse_mode
- self.caption_entities: Tuple[MessageEntity, ...] = parse_sequence_arg(caption_entities)
+ self.caption_entities: tuple[MessageEntity, ...] = parse_sequence_arg(caption_entities)
self.reply_markup: Optional[InlineKeyboardMarkup] = reply_markup
self.input_message_content: Optional[InputMessageContent] = input_message_content
diff --git a/telegram/_inline/inlinequeryresultcacheddocument.py b/telegram/_inline/inlinequeryresultcacheddocument.py
index b5416c2748c..af2e6ef7989 100644
--- a/telegram/_inline/inlinequeryresultcacheddocument.py
+++ b/telegram/_inline/inlinequeryresultcacheddocument.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2024
+# Copyright (C) 2015-2025
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -17,7 +17,8 @@
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains the classes that represent Telegram InlineQueryResultCachedDocument."""
-from typing import TYPE_CHECKING, Optional, Sequence, Tuple
+from collections.abc import Sequence
+from typing import TYPE_CHECKING, Optional
from telegram._inline.inlinekeyboardmarkup import InlineKeyboardMarkup
from telegram._inline.inlinequeryresult import InlineQueryResult
@@ -72,7 +73,7 @@ class InlineQueryResultCachedDocument(InlineQueryResult):
0-:tg-const:`telegram.constants.MessageLimit.CAPTION_LENGTH` characters
after entities parsing.
parse_mode (:obj:`str`): Optional. |parse_mode|
- caption_entities (Tuple[:class:`telegram.MessageEntity`]): Optional. |captionentitiesattr|
+ caption_entities (tuple[:class:`telegram.MessageEntity`]): Optional. |captionentitiesattr|
.. versionchanged:: 20.0
@@ -120,6 +121,6 @@ def __init__(
self.description: Optional[str] = description
self.caption: Optional[str] = caption
self.parse_mode: ODVInput[str] = parse_mode
- self.caption_entities: Tuple[MessageEntity, ...] = parse_sequence_arg(caption_entities)
+ self.caption_entities: tuple[MessageEntity, ...] = parse_sequence_arg(caption_entities)
self.reply_markup: Optional[InlineKeyboardMarkup] = reply_markup
self.input_message_content: Optional[InputMessageContent] = input_message_content
diff --git a/telegram/_inline/inlinequeryresultcachedgif.py b/telegram/_inline/inlinequeryresultcachedgif.py
index 9f52347a05c..f682ec0c7d4 100644
--- a/telegram/_inline/inlinequeryresultcachedgif.py
+++ b/telegram/_inline/inlinequeryresultcachedgif.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2024
+# Copyright (C) 2015-2025
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -17,7 +17,8 @@
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains the classes that represent Telegram InlineQueryResultCachedGif."""
-from typing import TYPE_CHECKING, Optional, Sequence, Tuple
+from collections.abc import Sequence
+from typing import TYPE_CHECKING, Optional
from telegram._inline.inlinekeyboardmarkup import InlineKeyboardMarkup
from telegram._inline.inlinequeryresult import InlineQueryResult
@@ -74,7 +75,7 @@ class InlineQueryResultCachedGif(InlineQueryResult):
0-:tg-const:`telegram.constants.MessageLimit.CAPTION_LENGTH` characters
after entities parsing.
parse_mode (:obj:`str`): Optional. |parse_mode|
- caption_entities (Tuple[:class:`telegram.MessageEntity`]): Optional. |captionentitiesattr|
+ caption_entities (tuple[:class:`telegram.MessageEntity`]): Optional. |captionentitiesattr|
.. versionchanged:: 20.0
@@ -124,7 +125,7 @@ def __init__(
self.title: Optional[str] = title
self.caption: Optional[str] = caption
self.parse_mode: ODVInput[str] = parse_mode
- self.caption_entities: Tuple[MessageEntity, ...] = parse_sequence_arg(caption_entities)
+ self.caption_entities: tuple[MessageEntity, ...] = parse_sequence_arg(caption_entities)
self.reply_markup: Optional[InlineKeyboardMarkup] = reply_markup
self.input_message_content: Optional[InputMessageContent] = input_message_content
self.show_caption_above_media: Optional[bool] = show_caption_above_media
diff --git a/telegram/_inline/inlinequeryresultcachedmpeg4gif.py b/telegram/_inline/inlinequeryresultcachedmpeg4gif.py
index f750f4df8fd..6dc7e557e92 100644
--- a/telegram/_inline/inlinequeryresultcachedmpeg4gif.py
+++ b/telegram/_inline/inlinequeryresultcachedmpeg4gif.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2024
+# Copyright (C) 2015-2025
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -17,7 +17,8 @@
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains the classes that represent Telegram InlineQueryResultMpeg4Gif."""
-from typing import TYPE_CHECKING, Optional, Sequence, Tuple
+from collections.abc import Sequence
+from typing import TYPE_CHECKING, Optional
from telegram._inline.inlinekeyboardmarkup import InlineKeyboardMarkup
from telegram._inline.inlinequeryresult import InlineQueryResult
@@ -74,7 +75,7 @@ class InlineQueryResultCachedMpeg4Gif(InlineQueryResult):
0-:tg-const:`telegram.constants.MessageLimit.CAPTION_LENGTH` characters
after entities parsing.
parse_mode (:obj:`str`): Optional. |parse_mode|
- caption_entities (Tuple[:class:`telegram.MessageEntity`]): Optional. |captionentitiesattr|
+ caption_entities (tuple[:class:`telegram.MessageEntity`]): Optional. |captionentitiesattr|
.. versionchanged:: 20.0
@@ -124,7 +125,7 @@ def __init__(
self.title: Optional[str] = title
self.caption: Optional[str] = caption
self.parse_mode: ODVInput[str] = parse_mode
- self.caption_entities: Tuple[MessageEntity, ...] = parse_sequence_arg(caption_entities)
+ self.caption_entities: tuple[MessageEntity, ...] = parse_sequence_arg(caption_entities)
self.reply_markup: Optional[InlineKeyboardMarkup] = reply_markup
self.input_message_content: Optional[InputMessageContent] = input_message_content
self.show_caption_above_media: Optional[bool] = show_caption_above_media
diff --git a/telegram/_inline/inlinequeryresultcachedphoto.py b/telegram/_inline/inlinequeryresultcachedphoto.py
index 75f292d2e32..adf8ea6b6b4 100644
--- a/telegram/_inline/inlinequeryresultcachedphoto.py
+++ b/telegram/_inline/inlinequeryresultcachedphoto.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2024
+# Copyright (C) 2015-2025
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -17,7 +17,8 @@
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains the classes that represent Telegram InlineQueryResultPhoto"""
-from typing import TYPE_CHECKING, Optional, Sequence, Tuple
+from collections.abc import Sequence
+from typing import TYPE_CHECKING, Optional
from telegram._inline.inlinekeyboardmarkup import InlineKeyboardMarkup
from telegram._inline.inlinequeryresult import InlineQueryResult
@@ -76,7 +77,7 @@ class InlineQueryResultCachedPhoto(InlineQueryResult):
0-:tg-const:`telegram.constants.MessageLimit.CAPTION_LENGTH` characters after
entities parsing.
parse_mode (:obj:`str`): Optional. |parse_mode|
- caption_entities (Tuple[:class:`telegram.MessageEntity`]): Optional. |captionentitiesattr|
+ caption_entities (tuple[:class:`telegram.MessageEntity`]): Optional. |captionentitiesattr|
.. versionchanged:: 20.0
@@ -129,7 +130,7 @@ def __init__(
self.description: Optional[str] = description
self.caption: Optional[str] = caption
self.parse_mode: ODVInput[str] = parse_mode
- self.caption_entities: Tuple[MessageEntity, ...] = parse_sequence_arg(caption_entities)
+ self.caption_entities: tuple[MessageEntity, ...] = parse_sequence_arg(caption_entities)
self.reply_markup: Optional[InlineKeyboardMarkup] = reply_markup
self.input_message_content: Optional[InputMessageContent] = input_message_content
self.show_caption_above_media: Optional[bool] = show_caption_above_media
diff --git a/telegram/_inline/inlinequeryresultcachedsticker.py b/telegram/_inline/inlinequeryresultcachedsticker.py
index 8e8d22544ca..0dd8c55ad26 100644
--- a/telegram/_inline/inlinequeryresultcachedsticker.py
+++ b/telegram/_inline/inlinequeryresultcachedsticker.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2024
+# Copyright (C) 2015-2025
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
diff --git a/telegram/_inline/inlinequeryresultcachedvideo.py b/telegram/_inline/inlinequeryresultcachedvideo.py
index 99a58eebbe5..3595330361a 100644
--- a/telegram/_inline/inlinequeryresultcachedvideo.py
+++ b/telegram/_inline/inlinequeryresultcachedvideo.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2024
+# Copyright (C) 2015-2025
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -17,7 +17,8 @@
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains the classes that represent Telegram InlineQueryResultCachedVideo."""
-from typing import TYPE_CHECKING, Optional, Sequence, Tuple
+from collections.abc import Sequence
+from typing import TYPE_CHECKING, Optional
from telegram._inline.inlinekeyboardmarkup import InlineKeyboardMarkup
from telegram._inline.inlinequeryresult import InlineQueryResult
@@ -72,7 +73,7 @@ class InlineQueryResultCachedVideo(InlineQueryResult):
0-:tg-const:`telegram.constants.MessageLimit.CAPTION_LENGTH` characters after
entities parsing.
parse_mode (:obj:`str`): Optional. |parse_mode|
- caption_entities (Tuple[:class:`telegram.MessageEntity`]): Optional. |captionentitiesattr|
+ caption_entities (tuple[:class:`telegram.MessageEntity`]): Optional. |captionentitiesattr|
.. versionchanged:: 20.0
@@ -125,7 +126,7 @@ def __init__(
self.description: Optional[str] = description
self.caption: Optional[str] = caption
self.parse_mode: ODVInput[str] = parse_mode
- self.caption_entities: Tuple[MessageEntity, ...] = parse_sequence_arg(caption_entities)
+ self.caption_entities: tuple[MessageEntity, ...] = parse_sequence_arg(caption_entities)
self.reply_markup: Optional[InlineKeyboardMarkup] = reply_markup
self.input_message_content: Optional[InputMessageContent] = input_message_content
self.show_caption_above_media: Optional[bool] = show_caption_above_media
diff --git a/telegram/_inline/inlinequeryresultcachedvoice.py b/telegram/_inline/inlinequeryresultcachedvoice.py
index dc8bd2ad3a6..139fdabff18 100644
--- a/telegram/_inline/inlinequeryresultcachedvoice.py
+++ b/telegram/_inline/inlinequeryresultcachedvoice.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2024
+# Copyright (C) 2015-2025
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -17,7 +17,8 @@
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains the classes that represent Telegram InlineQueryResultCachedVoice."""
-from typing import TYPE_CHECKING, Optional, Sequence, Tuple
+from collections.abc import Sequence
+from typing import TYPE_CHECKING, Optional
from telegram._inline.inlinekeyboardmarkup import InlineKeyboardMarkup
from telegram._inline.inlinequeryresult import InlineQueryResult
@@ -70,7 +71,7 @@ class InlineQueryResultCachedVoice(InlineQueryResult):
0-:tg-const:`telegram.constants.MessageLimit.CAPTION_LENGTH` characters after entities
parsing.
parse_mode (:obj:`str`): Optional. |parse_mode|
- caption_entities (Tuple[:class:`telegram.MessageEntity`]): Optional. |caption_entities|
+ caption_entities (tuple[:class:`telegram.MessageEntity`]): Optional. |caption_entities|
.. versionchanged:: 20.0
@@ -115,6 +116,6 @@ def __init__(
# Optionals
self.caption: Optional[str] = caption
self.parse_mode: ODVInput[str] = parse_mode
- self.caption_entities: Tuple[MessageEntity, ...] = parse_sequence_arg(caption_entities)
+ self.caption_entities: tuple[MessageEntity, ...] = parse_sequence_arg(caption_entities)
self.reply_markup: Optional[InlineKeyboardMarkup] = reply_markup
self.input_message_content: Optional[InputMessageContent] = input_message_content
diff --git a/telegram/_inline/inlinequeryresultcontact.py b/telegram/_inline/inlinequeryresultcontact.py
index faff47454d3..7ededbbd0b9 100644
--- a/telegram/_inline/inlinequeryresultcontact.py
+++ b/telegram/_inline/inlinequeryresultcontact.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2024
+# Copyright (C) 2015-2025
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
diff --git a/telegram/_inline/inlinequeryresultdocument.py b/telegram/_inline/inlinequeryresultdocument.py
index e0380440b20..e7114ef60aa 100644
--- a/telegram/_inline/inlinequeryresultdocument.py
+++ b/telegram/_inline/inlinequeryresultdocument.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2024
+# Copyright (C) 2015-2025
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -17,7 +17,8 @@
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains the classes that represent Telegram InlineQueryResultDocument"""
-from typing import TYPE_CHECKING, Optional, Sequence, Tuple
+from collections.abc import Sequence
+from typing import TYPE_CHECKING, Optional
from telegram._inline.inlinekeyboardmarkup import InlineKeyboardMarkup
from telegram._inline.inlinequeryresult import InlineQueryResult
@@ -86,7 +87,7 @@ class InlineQueryResultDocument(InlineQueryResult):
0-:tg-const:`telegram.constants.MessageLimit.CAPTION_LENGTH` characters
after entities parsing.
parse_mode (:obj:`str`): Optional. |parse_mode|
- caption_entities (Tuple[:class:`telegram.MessageEntity`]): Optional. |captionentitiesattr|
+ caption_entities (tuple[:class:`telegram.MessageEntity`]): Optional. |captionentitiesattr|
.. versionchanged:: 20.0
@@ -155,7 +156,7 @@ def __init__(
# Optionals
self.caption: Optional[str] = caption
self.parse_mode: ODVInput[str] = parse_mode
- self.caption_entities: Tuple[MessageEntity, ...] = parse_sequence_arg(caption_entities)
+ self.caption_entities: tuple[MessageEntity, ...] = parse_sequence_arg(caption_entities)
self.description: Optional[str] = description
self.reply_markup: Optional[InlineKeyboardMarkup] = reply_markup
self.input_message_content: Optional[InputMessageContent] = input_message_content
diff --git a/telegram/_inline/inlinequeryresultgame.py b/telegram/_inline/inlinequeryresultgame.py
index aeb78c0f1b4..27b12c87915 100644
--- a/telegram/_inline/inlinequeryresultgame.py
+++ b/telegram/_inline/inlinequeryresultgame.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2024
+# Copyright (C) 2015-2025
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
diff --git a/telegram/_inline/inlinequeryresultgif.py b/telegram/_inline/inlinequeryresultgif.py
index 13e1f253b99..398d61cc79a 100644
--- a/telegram/_inline/inlinequeryresultgif.py
+++ b/telegram/_inline/inlinequeryresultgif.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2024
+# Copyright (C) 2015-2025
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -17,7 +17,8 @@
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains the classes that represent Telegram InlineQueryResultGif."""
-from typing import TYPE_CHECKING, Optional, Sequence, Tuple
+from collections.abc import Sequence
+from typing import TYPE_CHECKING, Optional
from telegram._inline.inlinekeyboardmarkup import InlineKeyboardMarkup
from telegram._inline.inlinequeryresult import InlineQueryResult
@@ -46,20 +47,18 @@ 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. File size must not exceed 1MB.
+ gif_url (:obj:`str`): A valid URL for the GIF file.
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.
- thumbnail_url (:obj:`str`, optional): URL of the static (JPEG or GIF) or animated (MPEG4)
+ thumbnail_url (:obj:`str`): URL of the static (JPEG or GIF) or animated (MPEG4)
thumbnail for the result.
- Warning:
- The Bot API does **not** define this as an optional argument. It is formally
- optional for backwards compatibility with the deprecated :paramref:`thumb_url`.
- If you pass neither :paramref:`thumbnail_url` nor :paramref:`thumb_url`,
- :class:`ValueError` will be raised.
-
.. versionadded:: 20.2
+
+ ..versionchanged:: 20.5
+ |thumbnail_url_mandatory|
+
thumbnail_mime_type (:obj:`str`, optional): MIME type of the thumbnail, must be one of
``'image/jpeg'``, ``'image/gif'``, or ``'video/mp4'``. Defaults to ``'image/jpeg'``.
@@ -82,16 +81,12 @@ class InlineQueryResultGif(InlineQueryResult):
.. versionadded:: 21.3
- Raises:
- :class:`ValueError`: If neither :paramref:`thumbnail_url` nor :paramref:`thumb_url` is
- supplied or if both are supplied and are not equal.
-
Attributes:
type (:obj:`str`): :tg-const:`telegram.constants.InlineQueryResultType.GIF`.
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. File size must not exceed 1MB.
+ gif_url (:obj:`str`): A valid URL for the GIF file.
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.
@@ -108,7 +103,7 @@ class InlineQueryResultGif(InlineQueryResult):
0-:tg-const:`telegram.constants.MessageLimit.CAPTION_LENGTH` characters
after entities parsing.
parse_mode (:obj:`str`): Optional. |parse_mode|
- caption_entities (Tuple[:class:`telegram.MessageEntity`]): Optional. |captionentitiesattr|
+ caption_entities (tuple[:class:`telegram.MessageEntity`]): Optional. |captionentitiesattr|
.. versionchanged:: 20.0
@@ -172,7 +167,7 @@ def __init__(
self.title: Optional[str] = title
self.caption: Optional[str] = caption
self.parse_mode: ODVInput[str] = parse_mode
- self.caption_entities: Tuple[MessageEntity, ...] = parse_sequence_arg(caption_entities)
+ self.caption_entities: tuple[MessageEntity, ...] = parse_sequence_arg(caption_entities)
self.reply_markup: Optional[InlineKeyboardMarkup] = reply_markup
self.input_message_content: Optional[InputMessageContent] = input_message_content
self.thumbnail_mime_type: Optional[str] = thumbnail_mime_type
diff --git a/telegram/_inline/inlinequeryresultlocation.py b/telegram/_inline/inlinequeryresultlocation.py
index dff2b29a48b..01035537840 100644
--- a/telegram/_inline/inlinequeryresultlocation.py
+++ b/telegram/_inline/inlinequeryresultlocation.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2024
+# Copyright (C) 2015-2025
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
diff --git a/telegram/_inline/inlinequeryresultmpeg4gif.py b/telegram/_inline/inlinequeryresultmpeg4gif.py
index 1fff848418d..b47faa0186a 100644
--- a/telegram/_inline/inlinequeryresultmpeg4gif.py
+++ b/telegram/_inline/inlinequeryresultmpeg4gif.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2024
+# Copyright (C) 2015-2025
# Leandro Toledo de Souza
#
# This program is free software: you can redistribute it and/or modify
@@ -17,7 +17,8 @@
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains the classes that represent Telegram InlineQueryResultMpeg4Gif."""
-from typing import TYPE_CHECKING, Optional, Sequence, Tuple
+from collections.abc import Sequence
+from typing import TYPE_CHECKING, Optional
from telegram._inline.inlinekeyboardmarkup import InlineKeyboardMarkup
from telegram._inline.inlinequeryresult import InlineQueryResult
@@ -47,20 +48,18 @@ 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. File size must not exceed 1MB.
+ mpeg4_url (:obj:`str`): A valid URL for the MP4 file.
mpeg4_width (:obj:`int`, optional): Video width.
mpeg4_height (:obj:`int`, optional): Video height.
mpeg4_duration (:obj:`int`, optional): Video duration in seconds.
- thumbnail_url (:obj:`str`, optional): URL of the static (JPEG or GIF) or animated (MPEG4)
+ thumbnail_url (:obj:`str`): URL of the static (JPEG or GIF) or animated (MPEG4)
thumbnail for the result.
- Warning:
- The Bot API does **not** define this as an optional argument. It is formally
- optional for backwards compatibility with the deprecated :paramref:`thumb_url`.
- If you pass neither :paramref:`thumbnail_url` nor :paramref:`thumb_url`,
- :class:`ValueError` will be raised.
-
.. versionadded:: 20.2
+
+ ..versionchanged:: 20.5
+ |thumbnail_url_mandatory|
+
thumbnail_mime_type (:obj:`str`, optional): MIME type of the thumbnail, must be one of
``'image/jpeg'``, ``'image/gif'``, or ``'video/mp4'``. Defaults to ``'image/jpeg'``.
@@ -83,16 +82,13 @@ class InlineQueryResultMpeg4Gif(InlineQueryResult):
show_caption_above_media (:obj:`bool`, optional): Pass |show_cap_above_med|
.. versionadded:: 21.3
- Raises:
- :class:`ValueError`: If neither :paramref:`thumbnail_url` nor :paramref:`thumb_url` is
- supplied or if both are supplied and are not equal.
Attributes:
type (:obj:`str`): :tg-const:`telegram.constants.InlineQueryResultType.MPEG4GIF`.
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. File size must not exceed 1MB.
+ mpeg4_url (:obj:`str`): A valid URL for the MP4 file.
mpeg4_width (:obj:`int`): Optional. Video width.
mpeg4_height (:obj:`int`): Optional. Video height.
mpeg4_duration (:obj:`int`): Optional. Video duration in seconds.
@@ -109,7 +105,7 @@ class InlineQueryResultMpeg4Gif(InlineQueryResult):
0-:tg-const:`telegram.constants.MessageLimit.CAPTION_LENGTH` characters
after entities parsing.
parse_mode (:obj:`str`): Optional. |parse_mode|
- caption_entities (Tuple[:class:`telegram.MessageEntity`]): Optional. |caption_entities|
+ caption_entities (tuple[:class:`telegram.MessageEntity`]): Optional. |caption_entities|
.. versionchanged:: 20.0
@@ -173,7 +169,7 @@ def __init__(
self.title: Optional[str] = title
self.caption: Optional[str] = caption
self.parse_mode: ODVInput[str] = parse_mode
- self.caption_entities: Tuple[MessageEntity, ...] = parse_sequence_arg(caption_entities)
+ self.caption_entities: tuple[MessageEntity, ...] = parse_sequence_arg(caption_entities)
self.reply_markup: Optional[InlineKeyboardMarkup] = reply_markup
self.input_message_content: Optional[InputMessageContent] = input_message_content
self.thumbnail_mime_type: Optional[str] = thumbnail_mime_type
diff --git a/telegram/_inline/inlinequeryresultphoto.py b/telegram/_inline/inlinequeryresultphoto.py
index 637e952d4d0..e4556d62d49 100644
--- a/telegram/_inline/inlinequeryresultphoto.py
+++ b/telegram/_inline/inlinequeryresultphoto.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
-# Copyright (C) 2015-2024
+# Copyright (C) 2015-2025
# Leandro Toledo de Souza