diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 0000000000000000000000000000000000000000..8a8399b2db2a78d9b0ffcdc30b0dde386c2ab9c2 --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,7 @@ +# .git-blame-ignore-revs +# Use locally as `git blame file.py --ignore-revs-file .git-blame-ignore-revs` +# or configure git to always use it: `git config blame.ignoreRevsFile .git-blame-ignore-revs` +# First migration to code style Black (#2122) +264b2c9c72691c5937b80e84e061c52dd2d8861a +# Use Black more extensively (#2972) +950d9a0751d79b92d78ea44344ce3e3c5b3948f9 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..470d2a2aac1658624a80b69013e876dda947fbec --- /dev/null +++ b/.gitignore @@ -0,0 +1,97 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg +.env +.pybuild +debian/tmp +debian/python3-telegram +debian/python3-telegram-doc +debian/.debhelper + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +.pytest_cache +.mypy_cache +nosetests.xml +coverage.xml +*,cover +.coveralls.yml +.testmondata +.testmondata-journal + +# Translations +*.mo +*.pot + +# Django stuff: +*.log + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ +.idea/ + +# Sublime Text 2 +*.sublime* + +# VS Code +.vscode + +# unitests files +game.gif +telegram.mp3 +telegram.mp4 +telegram2.mp4 +telegram.ogg +telegram.png +telegram.webp +telegram.jpg + +# original files from merges +*.orig + +# Exclude .exrc file for Vim +.exrc + +# virtual env +venv* + +# environment manager: +.mise.toml \ No newline at end of file diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000000000000000000000000000000000000..0da0cea13810c6aae66f28598330e3ef5911a56c --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,83 @@ +# Make sure that the additional_dependencies here match pyproject.toml + +ci: + autofix_prs: false + autoupdate_schedule: quarterly + autoupdate_commit_msg: 'Bump `pre-commit` Hooks to Latest Versions' + +repos: +- repo: https://github.com/astral-sh/ruff-pre-commit + rev: 'v0.5.6' + hooks: + - id: ruff + name: ruff + additional_dependencies: + - httpx~=0.27 + - tornado~=6.4 + - APScheduler~=3.10.4 + - cachetools>=5.3.3,<5.5.0 + - aiolimiter~=1.1.0 +- repo: https://github.com/psf/black-pre-commit-mirror + rev: 24.4.2 + hooks: + - id: black + args: + - --diff + - --check +- repo: https://github.com/PyCQA/flake8 + rev: 7.1.0 + hooks: + - id: flake8 +- repo: https://github.com/PyCQA/pylint + rev: v3.2.4 + hooks: + - id: pylint + files: ^(?!(tests|docs)).*\.py$ + additional_dependencies: + - httpx~=0.27 + - tornado~=6.4 + - APScheduler~=3.10.4 + - cachetools>=5.3.3,<5.5.0 + - aiolimiter~=1.1.0 + - . # this basically does `pip install -e .` +- repo: https://github.com/pre-commit/mirrors-mypy + rev: v1.10.1 + hooks: + - id: mypy + name: mypy-ptb + files: ^(?!(tests|examples|docs)).*\.py$ + additional_dependencies: + - types-pytz + - types-cryptography + - types-cachetools + - httpx~=0.27 + - tornado~=6.4 + - APScheduler~=3.10.4 + - cachetools>=5.3.3,<5.5.0 + - aiolimiter~=1.1.0 + - . # this basically does `pip install -e .` + - id: mypy + name: mypy-examples + files: ^examples/.*\.py$ + args: + - --no-strict-optional + - --follow-imports=silent + additional_dependencies: + - tornado~=6.4 + - APScheduler~=3.10.4 + - cachetools>=5.3.3,<5.5.0 + - . # this basically does `pip install -e .` +- repo: https://github.com/asottile/pyupgrade + rev: v3.16.0 + hooks: + - id: pyupgrade + args: + - --py38-plus +- repo: https://github.com/pycqa/isort + rev: 5.13.2 + hooks: + - id: isort + name: isort + args: + - --diff + - --check diff --git a/.readthedocs.yml b/.readthedocs.yml new file mode 100644 index 0000000000000000000000000000000000000000..a23c582637d063c9e11237c90063a9cbe5320b49 --- /dev/null +++ b/.readthedocs.yml @@ -0,0 +1,62 @@ +# .readthedocs.yaml +# Read the Docs configuration file +# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details + +# Required +version: 2 + +# Build documentation in the docs/ directory with Sphinx +sphinx: + configuration: docs/source/conf.py + +# Optionally build your docs in additional formats such as PDF +formats: + - pdf + +# Optionally set the version of Python and requirements required to build your docs +python: + install: + - method: pip + path: . + - requirements: docs/requirements-docs.txt + +build: + os: ubuntu-22.04 + tools: + python: "3" # latest stable cpython version + jobs: + post_build: + # Based on https://github.com/readthedocs/readthedocs.org/issues/3242#issuecomment-1410321534 + # This provides a HTML zip file for download, with the same structure as the hosted website + - mkdir --parents $READTHEDOCS_OUTPUT/htmlzip + - cp --recursive $READTHEDOCS_OUTPUT/html $READTHEDOCS_OUTPUT/$READTHEDOCS_PROJECT + # Hide the "other versions" dropdown. This is a workaround for those versions being shown, + # but not being accessible, as they are not built. Also, they hide the actual sidebar menu + # that is relevant only on ReadTheDocs. + - echo "#furo-readthedocs-versions{display:none}" >> $READTHEDOCS_OUTPUT/$READTHEDOCS_PROJECT/_static/styles/furo-extensions.css + - cd $READTHEDOCS_OUTPUT ; zip --recurse-path --symlinks htmlzip/$READTHEDOCS_PROJECT.zip $READTHEDOCS_PROJECT + +search: + ranking: # bump up rank of commonly searched pages: (default: 0, values range from -10 to 10) + telegram.bot.html: 7 + telegram.message.html: 3 + telegram.update.html: 3 + telegram.user.html: 2 + telegram.chat.html: 2 + telegram.ext.application.html: 3 + telegram.ext.filters.html: 3 + telegram.ext.callbackcontext.html: 2 + telegram.ext.inlinekeyboardbutton.html: 1 + + telegram.passport*.html: -7 + + ignore: + - changelog.html + - coc.html + - bot_methods.html# + - bot_methods.html + # Defaults + - search.html + - search/index.html + - 404.html + - 404/index.html' diff --git a/AUTHORS.rst b/AUTHORS.rst new file mode 100644 index 0000000000000000000000000000000000000000..e95a2b7a3f979e91d155de2a00d472c1c39a39b3 --- /dev/null +++ b/AUTHORS.rst @@ -0,0 +1,137 @@ +Credits +======= + +``python-telegram-bot`` was originally created by +`Leandro Toledo `_. +The current development team includes + +- `Hinrich Mahler `_ (maintainer) +- `Poolitzer `_ (community liaison) +- `Shivam `_ +- `Harshil `_ +- `Dmitry Kolomatskiy `_ +- `Aditya `_ + +Emeritus maintainers include +`Jannes Höke `_ (`@jh0ker `_ on Telegram), +`Noam Meltzer `_, `Pieter Schutz `_ and `Jasmin Bom `_. + +Contributors +------------ + +The following wonderful people contributed directly or indirectly to this project: + +- `Abdelrahman `_ +- `Abshar `_ +- `Alateas `_ +- `Ales Dokshanin `_ +- `Alexandre `_ +- `Alizia `_ +- `Ambro17 `_ +- `Andrej Zhilenkov `_ +- `Anton Tagunov `_ +- `Avanatiker `_ +- `Balduro `_ +- `Bibo-Joshi `_ +- `Biruk Alamirew `_ +- `bimmlerd `_ +- `cyc8 `_ +- `d-qoi `_ +- `daimajia `_ +- `Daniel Reed `_ +- `D David Livingston `_ +- `DonalDuck004 `_ +- `Eana Hufwe `_ +- `Ehsan Online `_ +- `Eldad Carin `_ +- `Eli Gao `_ +- `Emilio Molinari `_ +- `ErgoZ Riftbit Vaper `_ +- `Eugene Lisitsky `_ +- `Eugenio Panadero `_ +- `Evan Haberecht `_ +- `Evgeny Denisov `_ +- `evgfilim1 `_ +- `ExalFabu `_ +- `franciscod `_ +- `gamgi `_ +- `Gauthamram Ravichandran `_ +- `Harshil `_ +- `Hugo Damer `_ +- `ihoru `_ +- `Iulian Onofrei `_ +- `Jainam Oswal `_ +- `Jasmin Bom `_ +- `JASON0916 `_ +- `jeffffc `_ +- `Jelle Besseling `_ +- `jh0ker `_ +- `jlmadurga `_ +- `John Yong `_ +- `Joscha Götzer `_ +- `jossalgon `_ +- `JRoot3D `_ +- `kenjitagawa `_ +- `kennethcheo `_ +- `Kirill Vasin `_ +- `Kjwon15 `_ +- `Li-aung Yip `_ +- `Loo Zheng Yuan `_ +- `LRezende `_ +- `Luca Bellanti `_ +- `Lucas Molinari `_ +- `macrojames `_ +- `Matheus Lemos `_ +- `Michael Dix `_ +- `Michael Elovskikh `_ +- `Miguel C. R. `_ +- `miles `_ +- `Mischa Krüger `_ +- `Mohd Yusuf `_ +- `naveenvhegde `_ +- `neurrone `_ +- `NikitaPirate `_ +- `Nikolai Krivenko `_ +- `njittam `_ +- `Noam Meltzer `_ +- `Oleg Shlyazhko `_ +- `Oleg Sushchenko `_ +- `Or Bin `_ +- `overquota `_ +- `Pablo Martinez `_ +- `Paradox `_ +- `Patrick Hofmann `_ +- `Paul Larsen `_ +- `Pawan `_ +- `Pieter Schutz `_ +- `Piraty `_ +- `Poolitzer `_ +- `Pranjalya Tiwari `_ +- `Rahiel Kasim `_ +- `Riko Naka `_ +- `Rizlas `_ +- `Sahil Sharma `_ +- `Sam Mosleh `_ +- `Sascha `_ +- `Shelomentsev D `_ +- `Shivam Saini `_ +- `Simon Schürrle `_ +- `sooyhwang `_ +- `syntx `_ +- `thodnev `_ +- `Timur Kushukov `_ +- `Trainer Jono `_ +- `Valentijn `_ +- `voider1 `_ +- `Vorobjev Simon `_ +- `Wagner Macedo `_ +- `wjt `_ +- `Wonseok Oh `_ +- `Yaw Danso `_ +- `Yao Kuan `_ +- `zeroone2numeral2 `_ +- `zeshuaro `_ +- `zpavloudis `_ + + +Please add yourself here alphabetically when you submit your first pull request. diff --git a/CHANGES.rst b/CHANGES.rst new file mode 100644 index 0000000000000000000000000000000000000000..8e5f302dd034f70307918ab803e9bb0efc14dd66 --- /dev/null +++ b/CHANGES.rst @@ -0,0 +1,2630 @@ +.. _ptb-changelog: + +========= +Changelog +========= + +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 +------------------ + +- 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 +============ + +*Released 2024-07-12* + +This is the technical changelog for version 21.4. More elaborate release notes can be found in the news channel `@pythontelegrambotchannel `_. + +Major Changes +------------- + +- Full Support for Bot API 7.5 (:pr:`4328`, :pr:`4316`, :pr:`4315`, :pr:`4312` closes :issue:`4310`, :pr:`4311`) +- Full Support for Bot API 7.6 (:pr:`4333` closes :issue:`4331`, :pr:`4344`, :pr:`4341`, :pr:`4334`, :pr:`4335`, :pr:`4351`, :pr:`4342`, :pr:`4348`) +- Full Support for Bot API 7.7 (:pr:`4356` closes :issue:`4355`) +- Drop ``python-telegram-bot-raw`` And Switch to ``pyproject.toml`` Based Packaging (:pr:`4288` closes :issue:`4129` and :issue:`4296`) +- Deprecate Inclusion of ``successful_payment`` in ``Message.effective_attachment`` (:pr:`4365` closes :issue:`4350`) + +New Features +------------ + +- Add Support for Python 3.13 Beta (:pr:`4253`) +- Add ``filters.PAID_MEDIA`` (:pr:`4357`) +- Log Received Data on Deserialization Errors (:pr:`4304`) +- Add ``MessageEntity.adjust_message_entities_to_utf_16`` Utility Function (:pr:`4323` by `Antares0982 `_ closes :issue:`4319`) +- Make Argument ``bot`` of ``TelegramObject.de_json`` Optional (:pr:`4320`) + +Documentation Improvements +-------------------------- + +- Documentation Improvements (:pr:`4303` closes :issue:`4301`) +- Restructure Readme (:pr:`4362`) +- Fix Link-Check Workflow (:pr:`4332`) + +Internal Changes +---------------- + +- Automate PyPI Releases (:pr:`4364` closes :issue:`4318`) +- Add ``mise-en-place`` to ``.gitignore`` (:pr:`4300`) +- Use a Composite Action for Testing Type Completeness (:pr:`4367`) +- Stabilize Some Concurrency Usages in Test Suite (:pr:`4360`) +- Add a Test Case for ``MenuButton`` (:pr:`4363`) +- Extend ``SuccessfulPayment`` Test (:pr:`4349`) +- Small Fixes for ``test_stars.py`` (:pr:`4347`) +- Use Python 3.13 Beta 3 in Test Suite (:pr:`4336`) + +Dependency Updates +------------------ + +- Bump ``ruff`` and Add New Rules (:pr:`4329`) +- Bump ``pre-commit`` Hooks to Latest Versions (:pr:`4337`) +- Add Lower Bound for ``flaky`` Dependency (:pr:`4322` by `Palaptin `_) +- Bump ``pytest`` from 8.2.1 to 8.2.2 (:pr:`4294`) + +Version 21.3 +============ +*Released 2024-06-07* + +This is the technical changelog for version 21.3. More elaborate release notes can be found in the news channel `@pythontelegrambotchannel `_. + +Major Changes +------------- + +- Full Support for Bot API 7.4 (:pr:`4286`, :pr:`4276` closes :issue:`4275`, :pr:`4285`, :pr:`4283`, :pr:`4280`, :pr:`4278`, :pr:`4279`) +- Deprecate ``python-telegram-bot-raw`` (:pr:`4270`) +- Remove Functionality Deprecated in Bot API 7.3 (:pr:`4266` closes :issue:`4244`) + +New Features +------------ + +- Add Parameter ``chat_id`` to ``ChatMemberHandler`` (:pr:`4290` by `uniquetrij `_ closes :issue:`4287`) + +Documentation Improvements +-------------------------- + +- Documentation Improvements (:pr:`4264` closes :issue:`4240`) + +Internal Changes +---------------- + +- Add ``setuptools`` to ``requirements-dev.txt`` (:pr:`4282`) +- Update Settings for pre-commit.ci (:pr:`4265`) + +Dependency Updates +------------------ + +- Bump ``pytest`` from 8.2.0 to 8.2.1 (:pr:`4272`) + +Version 21.2 +============ + +*Released 2024-05-20* + +This is the technical changelog for version 21.2. More elaborate release notes can be found in the news channel `@pythontelegrambotchannel `_. + +Major Changes +------------- + +- Full Support for Bot API 7.3 (:pr:`4246`, :pr:`4260`, :pr:`4243`, :pr:`4248`, :pr:`4242` closes :issue:`4236`, :pr:`4247` by `aelkheir `_) +- Remove Functionality Deprecated by Bot API 7.2 (:pr:`4245`) + +New Features +------------ + +- Add Version to ``PTBDeprecationWarning`` (:pr:`4262` closes :issue:`4261`) +- Handle Exceptions in building ``CallbackContext`` (:pr:`4222`) + +Bug Fixes +--------- + +- Call ``Application.post_stop`` Only if ``Application.stop`` was called (:pr:`4211` closes :issue:`4210`) +- Handle ``SystemExit`` raised in Handlers (:pr:`4157` closes :issue:`4155` and :issue:`4156`) +- Make ``Birthdate.to_date`` Return a ``datetime.date`` Object (:pr:`4251`) + +Documentation Improvements +-------------------------- + +- Documentation Improvements (:pr:`4217`) + +Internal Changes +---------------- + +- Add New Rules to ``ruff`` Config (:pr:`4250`) +- Adapt Test Suite to Changes in Error Messages (:pr:`4238`) + +Dependency Updates +------------------ + +- Bump ``furo`` from 2024.4.27 to 2024.5.6 (:pr:`4252`) +- ``pre-commit`` autoupdate (:pr:`4239`) +- Bump ``pytest`` from 8.1.1 to 8.2.0 (:pr:`4231`) +- Bump ``dependabot/fetch-metadata`` from 2.0.0 to 2.1.0 (:pr:`4228`) +- Bump ``pytest-asyncio`` from 0.21.1 to 0.21.2 (:pr:`4232`) +- Bump ``pytest-xdist`` from 3.6.0 to 3.6.1 (:pr:`4233`) +- Bump ``furo`` from 2024.1.29 to 2024.4.27 (:pr:`4230`) +- Bump ``srvaroa/labeler`` from 1.10.0 to 1.10.1 (:pr:`4227`) +- Bump ``pytest`` from 7.4.4 to 8.1.1 (:pr:`4218`) +- Bump ``sphinx`` from 7.2.6 to 7.3.7 (:pr:`4215`) +- Bump ``pytest-xdist`` from 3.5.0 to 3.6.0 (:pr:`4215`) + +Version 21.1.1 +============== + +*Released 2024-04-15* + +This is the technical changelog for version 21.1.1. More elaborate release notes can be found in the news channel `@pythontelegrambotchannel `__. + +Bug Fixes +--------- + +- Fix Bug With Parameter ``message_thread_id`` of ``Message.reply_*`` (:pr:`4207` closes :issue:`4205`) + +Minor Changes +------------- + +- Remove Deprecation Warning in ``JobQueue.run_daily`` (:pr:`4206` by `@Konano `__) +- Fix Annotation of ``EncryptedCredentials.decrypted_secret`` (:pr:`4199` by `@marinelay `__ closes :issue:`4198`) + + +Version 21.1 +============== + +*Released 2024-04-12* + +This is the technical changelog for version 21.1. More elaborate release notes can be found in the news channel `@pythontelegrambotchannel `__. + +Major Changes +------------- + +- API 7.2 (:pr:`4180` closes :issue:`4179` and :issue:`4181`, :issue:`4181`) +- Make ``ChatAdministratorRights/ChatMemberAdministrator.can_*_stories`` Required (API 7.1) (:pr:`4192`) + +Minor Changes +------------- + +- Refactor Debug logging in ``Bot`` to Improve Type Hinting (:pr:`4151` closes :issue:`4010`) + +New Features +------------ + +- Make ``Message.reply_*`` Reply in the Same Topic by Default (:pr:`4170` by `@aelkheir `__ closes :issue:`4139`) +- Accept Socket Objects for Webhooks (:pr:`4161` closes :issue:`4078`) +- Add ``Update.effective_sender`` (:pr:`4168` by `@aelkheir `__ closes :issue:`4085`) + +Documentation Improvements +-------------------------- + +- Documentation Improvements (:pr:`4171`, :pr:`4158` by `@teslaedison `__) + +Internal Changes +---------------- + +- Temporarily Mark Tests with ``get_sticker_set`` as XFAIL due to API 7.2 Update (:pr:`4190`) + +Dependency Updates +------------------ + +- ``pre-commit`` autoupdate (:pr:`4184`) +- Bump ``dependabot/fetch-metadata`` from 1.6.0 to 2.0.0 (:pr:`4185`) + + +Version 21.0.1 +============== + +*Released 2024-03-06* + +This is the technical changelog for version 21.0.1. More elaborate release notes can be found in the news channel `@pythontelegrambotchannel `__. + +Bug Fixes +--------- + +- Remove ``docs`` from Package (:pr:`4150`) + + +Version 21.0 +============ + +*Released 2024-03-06* + +This is the technical changelog for version 21.0. More elaborate release notes can be found in the news channel `@pythontelegrambotchannel `__. + +Major Changes +------------- + +- Remove Functionality Deprecated in API 7.0 (:pr:`4114` closes :issue:`4099`) +- API 7.1 (:pr:`4118`) + +New Features +------------ + +- Add Parameter ``media_write_timeout`` to ``HTTPXRequest`` and Method ``ApplicationBuilder.media_write_timeout`` (:pr:`4120` closes :issue:`3864`) +- Handle Properties in ``TelegramObject.__setstate__`` (:pr:`4134` closes :issue:`4111`) + +Bug Fixes +--------- + +- Add Missing Slot to ``Updater`` (:pr:`4130` closes :issue:`4127`) + +Documentation Improvements +-------------------------- + +- Improve HTML Download of Documentation (:pr:`4146` closes :issue:`4050`) +- Documentation Improvements (:pr:`4109`, :issue:`4116`) +- Update Copyright to 2024 (:pr:`4121` by `@aelkheir `__ closes :issue:`4041`) + +Internal Changes +---------------- + +- Apply ``pre-commit`` Checks More Widely (:pr:`4135`) +- Refactor and Overhaul ``test_official`` (:pr:`4087` closes :issue:`3874`) +- Run Unit Tests in PRs on Requirements Changes (:pr:`4144`) +- Make ``Updater.stop`` Independent of ``CancelledError`` (:pr:`4126`) + +Dependency Updates +------------------ + +- Relax Upper Bound for ``httpx`` Dependency (:pr:`4148`) +- Bump ``test-summary/action`` from 2.2 to 2.3 (:pr:`4142`) +- Update ``cachetools`` requirement from ~=5.3.2 to ~=5.3.3 (:pr:`4141`) +- Update ``httpx`` requirement from ~=0.26.0 to ~=0.27.0 (:pr:`4131`) + + +Version 20.8 +============ + +*Released 2024-02-08* + +This is the technical changelog for version 20.8. More elaborate release notes can be found in the news channel `@pythontelegrambotchannel `__. + +Major Changes +------------- + +- API 7.0 (:pr:`4034` closes :issue:`4033`, :pr:`4038` by `@aelkheir `__) + +Minor Changes +------------- + +- Fix Type Hint for ``filters`` Parameter of ``MessageHandler`` (:pr:`4039` by `@Palaptin `__) +- Deprecate ``filters.CHAT`` (:pr:`4083` closes :issue:`4062`) +- Improve Error Handling in Built-In Webhook Handler (:pr:`3987` closes :issue:`3979`) + +New Features +------------ + +- Add Parameter ``pattern`` to ``PreCheckoutQueryHandler`` and ``filters.SuccessfulPayment`` (:pr:`4005` by `@aelkheir `__ closes :issue:`3752`) +- Add Missing Conversions of ``type`` to Corresponding Enum from ``telegram.constants`` (:pr:`4067`) +- Add Support for Unix Sockets to ``Updater.start_webhook`` (:pr:`3986` closes :issue:`3978`) +- Add ``Bot.do_api_request`` (:pr:`4084` closes :issue:`4053`) +- Add ``AsyncContextManager`` as Parent Class to ``BaseUpdateProcessor`` (:pr:`4001`) + +Documentation Improvements +-------------------------- + +- Documentation Improvements (:pr:`3919`) +- Add Docstring to Dunder Methods (:pr:`3929` closes :issue:`3926`) +- Documentation Improvements (:pr:`4002`, :pr:`4079` by `@kenjitagawa `__, :pr:`4104` by `@xTudoS `__) + +Internal Changes +---------------- + +- Drop Usage of DeepSource (:pr:`4100`) +- Improve Type Completeness & Corresponding Workflow (:pr:`4035`) +- Bump ``ruff`` and Remove ``sort-all`` (:pr:`4075`) +- Move Handler Files to ``_handlers`` Subdirectory (:pr:`4064` by `@lucasmolinari `__ closes :issue:`4060`) +- Introduce ``sort-all`` Hook for ``pre-commit`` (:pr:`4052`) +- Use Recommended ``pre-commit`` Mirror for ``black`` (:pr:`4051`) +- Remove Unused ``DEFAULT_20`` (:pr:`3997`) +- Migrate From ``setup.cfg`` to ``pyproject.toml`` Where Possible (:pr:`4088`) + +Dependency Updates +------------------ + +- Bump ``black`` and ``ruff`` (:pr:`4089`) +- Bump ``srvaroa/labeler`` from 1.8.0 to 1.10.0 (:pr:`4048`) +- Update ``tornado`` requirement from ~=6.3.3 to ~=6.4 (:pr:`3992`) +- Bump ``actions/stale`` from 8 to 9 (:pr:`4046`) +- Bump ``actions/setup-python`` from 4 to 5 (:pr:`4047`) +- ``pre-commit`` autoupdate (:pr:`4101`) +- Bump ``actions/upload-artifact`` from 3 to 4 (:pr:`4045`) +- ``pre-commit`` autoupdate (:pr:`3996`) +- Bump ``furo`` from 2023.9.10 to 2024.1.29 (:pr:`4094`) +- ``pre-commit`` autoupdate (:pr:`4043`) +- Bump ``codecov/codecov-action`` from 3 to 4 (:pr:`4091`) +- Bump ``EndBug/add-and-commit`` from 9.1.3 to 9.1.4 (:pr:`4090`) +- Update ``httpx`` requirement from ~=0.25.2 to ~=0.26.0 (:pr:`4024`) +- Bump ``pytest`` from 7.4.3 to 7.4.4 (:pr:`4056`) +- Bump ``srvaroa/labeler`` from 1.7.0 to 1.8.0 (:pr:`3993`) +- Bump ``test-summary/action`` from 2.1 to 2.2 (:pr:`4044`) +- Bump ``dessant/lock-threads`` from 4.0.1 to 5.0.1 (:pr:`3994`) + + +Version 20.7 +============ + +*Released 2023-11-27* + +This is the technical changelog for version 20.7. More elaborate release notes can be found in the news channel `@pythontelegrambotchannel `__. + +New Features +------------ + +- Add ``JobQueue.scheduler_configuration`` and Corresponding Warnings (:pr:`3913` closes :issue:`3837`) +- Add Parameter ``socket_options`` to ``HTTPXRequest`` (:pr:`3935` closes :issue:`2965`) +- Add ``ApplicationBuilder.(get_updates_)socket_options`` (:pr:`3943`) +- Improve ``write_timeout`` Handling for Media Methods (:pr:`3952`) +- Add ``filters.Mention`` (:pr:`3941` closes :issue:`3799`) +- Rename ``proxy_url`` to ``proxy`` and Allow ``httpx.{Proxy, URL}`` as Input (:pr:`3939` closes :issue:`3844`) + +Bug Fixes & Changes +------------------- + +- Adjust ``read_timeout`` Behavior for ``Bot.get_updates`` (:pr:`3963` closes :issue:`3893`) +- Improve ``BaseHandler.__repr__`` for Callbacks without ``__qualname__`` (:pr:`3934`) +- Fix Persistency Issue with Ended Non-Blocking Conversations (:pr:`3962`) +- Improve Type Hinting for Arguments with Default Values in ``Bot`` (:pr:`3942`) + +Documentation Improvements +-------------------------- + +- Add Documentation for ``__aenter__`` and ``__aexit__`` Methods (:pr:`3907` closes :issue:`3886`) +- Improve Insertion of Kwargs into ``Bot`` Methods (:pr:`3965`) + +Internal Changes +---------------- + +- Adjust Tests to New Error Messages (:pr:`3970`) + +Dependency Updates +------------------ + +- Bump ``pytest-xdist`` from 3.3.1 to 3.4.0 (:pr:`3975`) +- ``pre-commit`` autoupdate (:pr:`3967`) +- Update ``httpx`` requirement from ~=0.25.1 to ~=0.25.2 (:pr:`3983`) +- Bump ``pytest-xdist`` from 3.4.0 to 3.5.0 (:pr:`3982`) +- Update ``httpx`` requirement from ~=0.25.0 to ~=0.25.1 (:pr:`3961`) +- Bump ``srvaroa/labeler`` from 1.6.1 to 1.7.0 (:pr:`3958`) +- Update ``cachetools`` requirement from ~=5.3.1 to ~=5.3.2 (:pr:`3954`) +- Bump ``pytest`` from 7.4.2 to 7.4.3 (:pr:`3953`) + + +Version 20.6 +============ + +*Released 2023-10-03* + +This is the technical changelog for version 20.6. More elaborate release notes can be found in the news channel `@pythontelegrambotchannel `__. + +Major Changes +------------- + +- Drop Backward Compatibility Layer Introduced in :pr:`3853` (API 6.8) (:pr:`3873`) +- Full Support for Bot API 6.9 (:pr:`3898`) + +New Features +------------ + +- Add Rich Equality Comparison to ``WriteAccessAllowed`` (:pr:`3911` closes :issue:`3909`) +- Add ``__repr__`` Methods Added in :pr:`3826` closes :issue:`3770` to Sphinx Documentation (:pr:`3901` closes :issue:`3889`) +- Add String Representation for Selected Classes (:pr:`3826` closes :issue:`3770`) + +Minor Changes +------------- + +- Add Support Python 3.12 (:pr:`3915`) +- Documentation Improvements (:pr:`3910`) + +Internal Changes +---------------- + +- Verify Type Hints for Bot Method & Telegram Class Parameters (:pr:`3868`) +- Move Bot API Tests to Separate Workflow File (:pr:`3912`) +- Fix Failing ``file_size`` Tests (:pr:`3906`) +- Set Threshold for DeepSource’s PY-R1000 to High (:pr:`3888`) +- One-Time Code Formatting Improvement via ``--preview`` Flag of ``black`` (:pr:`3882`) +- Move Dunder Methods to the Top of Class Bodies (:pr:`3883`) +- Remove Superfluous ``Defaults.__ne__`` (:pr:`3884`) + +Dependency Updates +------------------ + +- ``pre-commit`` autoupdate (:pr:`3876`) +- Update ``pre-commit`` Dependencies (:pr:`3916`) +- Bump ``actions/checkout`` from 3 to 4 (:pr:`3914`) +- Update ``httpx`` requirement from ~=0.24.1 to ~=0.25.0 (:pr:`3891`) +- Bump ``furo`` from 2023.8.19 to 2023.9.10 (:pr:`3890`) +- Bump ``sphinx`` from 7.2.5 to 7.2.6 (:pr:`3892`) +- Update ``tornado`` requirement from ~=6.2 to ~=6.3.3 (:pr:`3675`) +- Bump ``pytest`` from 7.4.0 to 7.4.2 (:pr:`3881`) + + +Version 20.5 +============ +*Released 2023-09-03* + +This is the technical changelog for version 20.5. More elaborate release notes can be found in the news channel `@pythontelegrambotchannel `__. + +Major Changes +------------- + +- API 6.8 (:pr:`3853`) +- Remove Functionality Deprecated Since Bot API 6.5, 6.6 or 6.7 (:pr:`3858`) + +New Features +------------ + +- Extend Allowed Values for HTTP Version (:pr:`3823` closes :issue:`3821`) +- Add ``has_args`` Parameter to ``CommandHandler`` (:pr:`3854` by `@thatguylah `__ closes :issue:`3798`) +- Add ``Application.stop_running()`` and Improve Marking Updates as Read on ``Updater.stop()`` (:pr:`3804`) + +Minor Changes +------------- + +- Type Hinting Fixes for ``WebhookInfo`` (:pr:`3871`) +- Test and Document ``Exception.__cause__`` on ``NetworkError`` (:pr:`3792` closes :issue:`3778`) +- Add Support for Python 3.12 RC (:pr:`3847`) + +Documentation Improvements +-------------------------- + +- Remove Version Check from Examples (:pr:`3846`) +- Documentation Improvements (:pr:`3803`, :pr:`3797`, :pr:`3816` by `@trim21 `__, :pr:`3829` by `@aelkheir `__) +- Provide Versions of ``customwebhookbot.py`` with Different Frameworks (:pr:`3820` closes :issue:`3717`) + +Dependency Updates +------------------ + +- ``pre-commit`` autoupdate (:pr:`3824`) +- Bump ``srvaroa/labeler`` from 1.6.0 to 1.6.1 (:pr:`3870`) +- Bump ``sphinx`` from 7.0.1 to 7.1.1 (:pr:`3818`) +- Bump ``sphinx`` from 7.2.3 to 7.2.5 (:pr:`3869`) +- Bump ``furo`` from 2023.5.20 to 2023.7.26 (:pr:`3817`) +- Update ``apscheduler`` requirement from ~=3.10.3 to ~=3.10.4 (:pr:`3862`) +- Bump ``sphinx`` from 7.2.2 to 7.2.3 (:pr:`3861`) +- Bump ``pytest-asyncio`` from 0.21.0 to 0.21.1 (:pr:`3801`) +- Bump ``sphinx-paramlinks`` from 0.5.4 to 0.6.0 (:pr:`3840`) +- Update ``apscheduler`` requirement from ~=3.10.1 to ~=3.10.3 (:pr:`3851`) +- Bump ``furo`` from 2023.7.26 to 2023.8.19 (:pr:`3850`) +- Bump ``sphinx`` from 7.1.2 to 7.2.2 (:pr:`3852`) +- Bump ``sphinx`` from 7.1.1 to 7.1.2 (:pr:`3827`) + + +Version 20.4 +============ + +*Released 2023-07-09* + +This is the technical changelog for version 20.4. More elaborate release notes can be found in the news channel `@pythontelegrambotchannel `__. + +Major Changes +------------- + +- Drop Support for Python 3.7 (:pr:`3728`, :pr:`3742` by `@Trifase `__, :pr:`3749` by `@thefunkycat `__, :pr:`3740` closes :issue:`3732`, :pr:`3754` closes :issue:`3731`, :pr:`3753`, :pr:`3764`, :pr:`3762`, :pr:`3759` closes :issue:`3733`) + +New Features +------------ + +- Make Integration of ``APScheduler`` into ``JobQueue`` More Explicit (:pr:`3695`) +- Introduce ``BaseUpdateProcessor`` for Customized Concurrent Handling of Updates (:pr:`3654` closes :issue:`3509`) + +Minor Changes +------------- + +- Fix Inconsistent Type Hints for ``timeout`` Parameter of ``Bot.get_updates`` (:pr:`3709` by `@revolter `__) +- Use Explicit Optionals (:pr:`3692` by `@MiguelX413 `__) + +Bug Fixes +--------- + +- Fix Wrong Warning Text in ``KeyboardButton.__eq__`` (:pr:`3768`) + +Documentation Improvements +-------------------------- + +- Explicitly set ``allowed_updates`` in Examples (:pr:`3741` by `@Trifase `__ closes :issue:`3726`) +- Bump ``furo`` and ``sphinx`` (:pr:`3719`) +- Documentation Improvements (:pr:`3698`, :pr:`3708` by `@revolter `__, :pr:`3767`) +- Add Quotes for Installation Instructions With Optional Dependencies (:pr:`3780`) +- Exclude Type Hints from Stability Policy (:pr:`3712`) +- Set ``httpx`` Logging Level to Warning in Examples (:pr:`3746` closes :issue:`3743`) + +Internal Changes +---------------- + +- Drop a Legacy ``pre-commit.ci`` Configuration (:pr:`3697`) +- Add Python 3.12 Beta to the Test Matrix (:pr:`3751`) +- Use Temporary Files for Testing File Downloads (:pr:`3777`) +- Auto-Update Changed Version in Other Files After Dependabot PRs (:pr:`3716`) +- Add More ``ruff`` Rules (:pr:`3763`) +- Rename ``_handler.py`` to ``_basehandler.py`` (:pr:`3761`) +- Automatically Label ``pre-commit-ci`` PRs (:pr:`3713`) +- Rework ``pytest`` Integration into GitHub Actions (:pr:`3776`) +- Fix Two Bugs in GitHub Actions Workflows (:pr:`3739`) + +Dependency Updates +------------------ + +- Update ``cachetools`` requirement from ~=5.3.0 to ~=5.3.1 (:pr:`3738`) +- Update ``aiolimiter`` requirement from ~=1.0.0 to ~=1.1.0 (:pr:`3707`) +- ``pre-commit`` autoupdate (:pr:`3791`) +- Bump ``sphinxcontrib-mermaid`` from 0.8.1 to 0.9.2 (:pr:`3737`) +- Bump ``pytest-xdist`` from 3.2.1 to 3.3.0 (:pr:`3705`) +- Bump ``srvaroa/labeler`` from 1.5.0 to 1.6.0 (:pr:`3786`) +- Bump ``dependabot/fetch-metadata`` from 1.5.1 to 1.6.0 (:pr:`3787`) +- Bump ``dessant/lock-threads`` from 4.0.0 to 4.0.1 (:pr:`3785`) +- Bump ``pytest`` from 7.3.2 to 7.4.0 (:pr:`3774`) +- Update ``httpx`` requirement from ~=0.24.0 to ~=0.24.1 (:pr:`3715`) +- Bump ``pytest-xdist`` from 3.3.0 to 3.3.1 (:pr:`3714`) +- Bump ``pytest`` from 7.3.1 to 7.3.2 (:pr:`3758`) +- ``pre-commit`` autoupdate (:pr:`3747`) + + +Version 20.3 +============ +*Released 2023-05-07* + +This is the technical changelog for version 20.3. More elaborate release notes can be found in the news channel `@pythontelegrambotchannel `_. + +Major Changes +------------- + +- Full support for API 6.7 (:pr:`3673`) +- Add a Stability Policy (:pr:`3622`) + +New Features +------------ + +- Add ``Application.mark_data_for_update_persistence`` (:pr:`3607`) +- Make ``Message.link`` Point to Thread View Where Possible (:pr:`3640`) +- Localize Received ``datetime`` Objects According to ``Defaults.tzinfo`` (:pr:`3632`) + +Minor Changes, Documentation Improvements and CI +------------------------------------------------ + +- Empower ``ruff`` (:pr:`3594`) +- Drop Usage of ``sys.maxunicode`` (:pr:`3630`) +- Add String Representation for ``RequestParameter`` (:pr:`3634`) +- Stabilize CI by Rerunning Failed Tests (:pr:`3631`) +- Give Loggers Better Names (:pr:`3623`) +- Add Logging for Invalid JSON Data in ``BasePersistence.parse_json_payload`` (:pr:`3668`) +- Improve Warning Categories & Stacklevels (:pr:`3674`) +- Stabilize ``test_delete_sticker_set`` (:pr:`3685`) +- Shield Update Fetcher Task in ``Application.start`` (:pr:`3657`) +- Recover 100% Type Completeness (:pr:`3676`) +- Documentation Improvements (:pr:`3628`, :pr:`3636`, :pr:`3694`) + +Dependencies +------------ + +- Bump ``actions/stale`` from 7 to 8 (:pr:`3644`) +- Bump ``furo`` from 2023.3.23 to 2023.3.27 (:pr:`3643`) +- ``pre-commit`` autoupdate (:pr:`3646`, :pr:`3688`) +- Remove Deprecated ``codecov`` Package from CI (:pr:`3664`) +- Bump ``sphinx-copybutton`` from 0.5.1 to 0.5.2 (:pr:`3662`) +- Update ``httpx`` requirement from ~=0.23.3 to ~=0.24.0 (:pr:`3660`) +- Bump ``pytest`` from 7.2.2 to 7.3.1 (:pr:`3661`) + +Version 20.2 +============ +*Released 2023-03-25* + +This is the technical changelog for version 20.2. More elaborate release notes can be found in the news channel `@pythontelegrambotchannel `_. + +Major Changes +------------- +- Full Support for API 6.6 (:pr:`3584`) +- Revert to HTTP/1.1 as Default and make HTTP/2 an Optional Dependency (:pr:`3576`) + +Minor Changes, Documentation Improvements and CI +------------------------------------------------ +- Documentation Improvements (:pr:`3565`, :pr:`3600`) +- Handle Symbolic Links in ``was_called_by`` (:pr:`3552`) +- Tidy Up Tests Directory (:pr:`3553`) +- Enhance ``Application.create_task`` (:pr:`3543`) +- Make Type Completeness Workflow Usable for ``PRs`` from Forks (:pr:`3551`) +- Refactor and Overhaul the Test Suite (:pr:`3426`) + +Dependencies +------------ +- Bump ``pytest-asyncio`` from 0.20.3 to 0.21.0 (:pr:`3624`) +- Bump ``furo`` from 2022.12.7 to 2023.3.23 (:pr:`3625`) +- Bump ``pytest-xdist`` from 3.2.0 to 3.2.1 (:pr:`3606`) +- ``pre-commit`` autoupdate (:pr:`3577`) +- Update ``apscheduler`` requirement from ~=3.10.0 to ~=3.10.1 (:pr:`3572`) +- Bump ``pytest`` from 7.2.1 to 7.2.2 (:pr:`3573`) +- Bump ``pytest-xdist`` from 3.1.0 to 3.2.0 (:pr:`3550`) +- Bump ``sphinxcontrib-mermaid`` from 0.7.1 to 0.8 (:pr:`3549`) + +Version 20.1 +============ +*Released 2023-02-09* + +This is the technical changelog for version 20.1. More elaborate release notes can be found in the news channel `@pythontelegrambotchannel `_. + +Major Changes +------------- + +- Full Support for Bot API 6.5 (:pr:`3530`) + +New Features +------------ + +- Add ``Application(Builder).post_stop`` (:pr:`3466`) +- Add ``Chat.effective_name`` Convenience Property (:pr:`3485`) +- Allow to Adjust HTTP Version and Use HTTP/2 by Default (:pr:`3506`) + +Documentation Improvements +-------------------------- + +- Enhance ``chatmemberbot`` Example (:pr:`3500`) +- Automatically Generate Cross-Reference Links (:pr:`3501`, :pr:`3529`, :pr:`3523`) +- Add Some Graphic Elements to Docs (:pr:`3535`) +- Various Smaller Improvements (:pr:`3464`, :pr:`3483`, :pr:`3484`, :pr:`3497`, :pr:`3512`, :pr:`3515`, :pr:`3498`) + +Minor Changes, Documentation Improvements and CI +------------------------------------------------ + +- Update Copyright to 2023 (:pr:`3459`) +- Stabilize Tests on Closing and Hiding the General Forum Topic (:pr:`3460`) +- Fix Dependency Warning Typo (:pr:`3474`) +- Cache Dependencies on ``GitHub`` Actions (:pr:`3469`) +- Store Documentation Builts as ``GitHub`` Actions Artifacts (:pr:`3468`) +- Add ``ruff`` to ``pre-commit`` Hooks (:pr:`3488`) +- Improve Warning for ``days`` Parameter of ``JobQueue.run_daily`` (:pr:`3503`) +- Improve Error Message for ``NetworkError`` (:pr:`3505`) +- Lock Inactive Threads Only Once Each Day (:pr:`3510`) +- Bump ``pytest`` from 7.2.0 to 7.2.1 (:pr:`3513`) +- Check for 3D Arrays in ``check_keyboard_type`` (:pr:`3514`) +- Explicit Type Annotations (:pr:`3508`) +- Increase Verbosity of Type Completeness CI Job (:pr:`3531`) +- Fix CI on Python 3.11 + Windows (:pr:`3547`) + +Dependencies +------------ + +- Bump ``actions/stale`` from 6 to 7 (:pr:`3461`) +- Bump ``dessant/lock-threads`` from 3.0.0 to 4.0.0 (:pr:`3462`) +- ``pre-commit`` autoupdate (:pr:`3470`) +- Update ``httpx`` requirement from ~=0.23.1 to ~=0.23.3 (:pr:`3489`) +- Update ``cachetools`` requirement from ~=5.2.0 to ~=5.2.1 (:pr:`3502`) +- Improve Config for ``ruff`` and Bump to ``v0.0.222`` (:pr:`3507`) +- Update ``cachetools`` requirement from ~=5.2.1 to ~=5.3.0 (:pr:`3520`) +- Bump ``isort`` to 5.12.0 (:pr:`3525`) +- Update ``apscheduler`` requirement from ~=3.9.1 to ~=3.10.0 (:pr:`3532`) +- ``pre-commit`` autoupdate (:pr:`3537`) +- Update ``cryptography`` requirement to >=39.0.1 to address Vulnerability (:pr:`3539`) + +Version 20.0 +============ +*Released 2023-01-01* + +This is the technical changelog for version 20.0. More elaborate release notes can be found in the news channel `@pythontelegrambotchannel `_. + +Major Changes +------------- + +- Full Support For Bot API 6.4 (:pr:`3449`) + +Minor Changes, Documentation Improvements and CI +------------------------------------------------ + +- Documentation Improvements (:pr:`3428`, :pr:`3423`, :pr:`3429`, :pr:`3441`, :pr:`3404`, :pr:`3443`) +- Allow ``Sequence`` Input for Bot Methods (:pr:`3412`) +- Update Link-Check CI and Replace a Dead Link (:pr:`3456`) +- Freeze Classes Without Arguments (:pr:`3453`) +- Add New Constants (:pr:`3444`) +- Override ``Bot.__deepcopy__`` to Raise ``TypeError`` (:pr:`3446`) +- Add Log Decorator to ``Bot.get_webhook_info`` (:pr:`3442`) +- Add Documentation On Verifying Releases (:pr:`3436`) +- Drop Undocumented ``Job.__lt__`` (:pr:`3432`) + +Dependencies +------------ + +- Downgrade ``sphinx`` to 5.3.0 to Fix Search (:pr:`3457`) +- Bump ``sphinx`` from 5.3.0 to 6.0.0 (:pr:`3450`) + +Version 20.0b0 +============== +*Released 2022-12-15* + +This is the technical changelog for version 20.0b0. More elaborate release notes can be found in the news channel `@pythontelegrambotchannel `_. + +Major Changes +------------- + +- Make ``TelegramObject`` Immutable (:pr:`3249`) + +Minor Changes, Documentation Improvements and CI +------------------------------------------------ + +- Reduce Code Duplication in Testing ``Defaults`` (:pr:`3419`) +- Add Notes and Warnings About Optional Dependencies (:pr:`3393`) +- Simplify Internals of ``Bot`` Methods (:pr:`3396`) +- Reduce Code Duplication in Several ``Bot`` Methods (:pr:`3385`) +- Documentation Improvements (:pr:`3386`, :pr:`3395`, :pr:`3398`, :pr:`3403`) + +Dependencies +------------ + +- Bump ``pytest-xdist`` from 3.0.2 to 3.1.0 (:pr:`3415`) +- Bump ``pytest-asyncio`` from 0.20.2 to 0.20.3 (:pr:`3417`) +- ``pre-commit`` autoupdate (:pr:`3409`) + +Version 20.0a6 +============== +*Released 2022-11-24* + +This is the technical changelog for version 20.0a6. More elaborate release notes can be found in the news channel `@pythontelegrambotchannel `_. + +Bug Fixes +--------- + +- Only Persist Arbitrary ``callback_data`` if ``ExtBot.callback_data_cache`` is Present (:pr:`3384`) +- Improve Backwards Compatibility of ``TelegramObjects`` Pickle Behavior (:pr:`3382`) +- Fix Naming and Keyword Arguments of ``File.download_*`` Methods (:pr:`3380`) +- Fix Return Value Annotation of ``Chat.create_forum_topic`` (:pr:`3381`) + +Version 20.0a5 +============== +*Released 2022-11-22* + +This is the technical changelog for version 20.0a5. More elaborate release notes can be found in the news channel `@pythontelegrambotchannel `_. + +Major Changes +------------- + +- API 6.3 (:pr:`3346`, :pr:`3343`, :pr:`3342`, :pr:`3360`) +- Explicit ``local_mode`` Setting (:pr:`3154`) +- Make Almost All 3rd Party Dependencies Optional (:pr:`3267`) +- Split ``File.download`` Into ``File.download_to_drive`` And ``File.download_to_memory`` (:pr:`3223`) + +New Features +------------ + +- Add Properties for API Settings of ``Bot`` (:pr:`3247`) +- Add ``chat_id`` and ``username`` Parameters to ``ChatJoinRequestHandler`` (:pr:`3261`) +- Introduce ``TelegramObject.api_kwargs`` (:pr:`3233`) +- Add Two Constants Related to Local Bot API Servers (:pr:`3296`) +- Add ``recursive`` Parameter to ``TelegramObject.to_dict()`` (:pr:`3276`) +- Overhaul String Representation of ``TelegramObject`` (:pr:`3234`) +- Add Methods ``Chat.mention_{html, markdown, markdown_v2}`` (:pr:`3308`) +- Add ``constants.MessageLimit.DEEP_LINK_LENGTH`` (:pr:`3315`) +- Add Shortcut Parameters ``caption``, ``parse_mode`` and ``caption_entities`` to ``Bot.send_media_group`` (:pr:`3295`) +- Add Several New Enums To Constants (:pr:`3351`) + +Bug Fixes +--------- + +- Fix ``CallbackQueryHandler`` Not Handling Non-String Data Correctly With Regex Patterns (:pr:`3252`) +- Fix Defaults Handling in ``Bot.answer_web_app_query`` (:pr:`3362`) + +Documentation Improvements +-------------------------- + +- Update PR Template (:pr:`3361`) +- Document Dunder Methods of ``TelegramObject`` (:pr:`3319`) +- Add Several References to Wiki pages (:pr:`3306`) +- Overhaul Search bar (:pr:`3218`) +- Unify Documentation of Arguments and Attributes of Telegram Classes (:pr:`3217`, :pr:`3292`, :pr:`3303`, :pr:`3312`, :pr:`3314`) +- Several Smaller Improvements (:pr:`3214`, :pr:`3271`, :pr:`3289`, :pr:`3326`, :pr:`3370`, :pr:`3376`, :pr:`3366`) + +Minor Changes, Documentation Improvements and CI +------------------------------------------------ + +- Improve Warning About Unknown ``ConversationHandler`` States (:pr:`3242`) +- Switch from Stale Bot to ``GitHub`` Actions (:pr:`3243`) +- Bump Python 3.11 to RC2 in Test Matrix (:pr:`3246`) +- Make ``Job.job`` a Property and Make ``Jobs`` Hashable (:pr:`3250`) +- Skip ``JobQueue`` Tests on Windows Again (:pr:`3280`) +- Read-Only ``CallbackDataCache`` (:pr:`3266`) +- Type Hinting Fix for ``Message.effective_attachment`` (:pr:`3294`) +- Run Unit Tests in Parallel (:pr:`3283`) +- Update Test Matrix to Use Stable Python 3.11 (:pr:`3313`) +- Don't Edit Objects In-Place When Inserting ``ext.Defaults`` (:pr:`3311`) +- Add a Test for ``MessageAttachmentType`` (:pr:`3335`) +- Add Three New Test Bots (:pr:`3347`) +- Improve Unit Tests Regarding ``ChatMemberUpdated.difference`` (:pr:`3352`) +- Flaky Unit Tests: Use ``pytest`` Marker (:pr:`3354`) +- Fix ``DeepSource`` Issues (:pr:`3357`) +- Handle Lists and Tuples and Datetimes Directly in ``TelegramObject.to_dict`` (:pr:`3353`) +- Update Meta Config (:pr:`3365`) +- Merge ``ChatDescriptionLimit`` Enum Into ``ChatLimit`` (:pr:`3377`) + +Dependencies +------------ + +- Bump ``pytest`` from 7.1.2 to 7.1.3 (:pr:`3228`) +- ``pre-commit`` Updates (:pr:`3221`) +- Bump ``sphinx`` from 5.1.1 to 5.2.3 (:pr:`3269`) +- Bump ``furo`` from 2022.6.21 to 2022.9.29 (:pr:`3268`) +- Bump ``actions/stale`` from 5 to 6 (:pr:`3277`) +- ``pre-commit`` autoupdate (:pr:`3282`) +- Bump ``sphinx`` from 5.2.3 to 5.3.0 (:pr:`3300`) +- Bump ``pytest-asyncio`` from 0.19.0 to 0.20.1 (:pr:`3299`) +- Bump ``pytest`` from 7.1.3 to 7.2.0 (:pr:`3318`) +- Bump ``pytest-xdist`` from 2.5.0 to 3.0.2 (:pr:`3317`) +- ``pre-commit`` autoupdate (:pr:`3325`) +- Bump ``pytest-asyncio`` from 0.20.1 to 0.20.2 (:pr:`3359`) +- Update ``httpx`` requirement from ~=0.23.0 to ~=0.23.1 (:pr:`3373`) + +Version 20.0a4 +============== +*Released 2022-08-27* + +This is the technical changelog for version 20.0a4. More elaborate release notes can be found in the news channel `@pythontelegrambotchannel `_. + +Hot Fixes +--------- + +* Fix a Bug in ``setup.py`` Regarding Optional Dependencies (:pr:`3209`) + +Version 20.0a3 +============== +*Released 2022-08-27* + +This is the technical changelog for version 20.0a3. More elaborate release notes can be found in the news channel `@pythontelegrambotchannel `_. + +Major Changes +------------- + +- Full Support for API 6.2 (:pr:`3195`) + +New Features +------------ + +- New Rate Limiting Mechanism (:pr:`3148`) +- Make ``chat/user_data`` Available in Error Handler for Errors in Jobs (:pr:`3152`) +- Add ``Application.post_shutdown`` (:pr:`3126`) + +Bug Fixes +--------- + +- Fix ``helpers.mention_markdown`` for Markdown V1 and Improve Related Unit Tests (:pr:`3155`) +- Add ``api_kwargs`` Parameter to ``Bot.log_out`` and Improve Related Unit Tests (:pr:`3147`) +- Make ``Bot.delete_my_commands`` a Coroutine Function (:pr:`3136`) +- Fix ``ConversationHandler.check_update`` not respecting ``per_user`` (:pr:`3128`) + +Minor Changes, Documentation Improvements and CI +------------------------------------------------ + +- Add Python 3.11 to Test Suite & Adapt Enum Behaviour (:pr:`3168`) +- Drop Manual Token Validation (:pr:`3167`) +- Simplify Unit Tests for ``Bot.send_chat_action`` (:pr:`3151`) +- Drop ``pre-commit`` Dependencies from ``requirements-dev.txt`` (:pr:`3120`) +- Change Default Values for ``concurrent_updates`` and ``connection_pool_size`` (:pr:`3127`) +- Documentation Improvements (:pr:`3139`, :pr:`3153`, :pr:`3135`) +- Type Hinting Fixes (:pr:`3202`) + +Dependencies +------------ + +- Bump ``sphinx`` from 5.0.2 to 5.1.1 (:pr:`3177`) +- Update ``pre-commit`` Dependencies (:pr:`3085`) +- Bump ``pytest-asyncio`` from 0.18.3 to 0.19.0 (:pr:`3158`) +- Update ``tornado`` requirement from ~=6.1 to ~=6.2 (:pr:`3149`) +- Bump ``black`` from 22.3.0 to 22.6.0 (:pr:`3132`) +- Bump ``actions/setup-python`` from 3 to 4 (:pr:`3131`) + +Version 20.0a2 +============== +*Released 2022-06-27* + +This is the technical changelog for version 20.0a2. More elaborate release notes can be found in the news channel `@pythontelegrambotchannel `_. + +Major Changes +------------- + +- Full Support for API 6.1 (:pr:`3112`) + +New Features +------------ + +- Add Additional Shortcut Methods to ``Chat`` (:pr:`3115`) +- Mermaid-based Example State Diagrams (:pr:`3090`) + +Minor Changes, Documentation Improvements and CI +------------------------------------------------ + +- Documentation Improvements (:pr:`3103`, :pr:`3121`, :pr:`3098`) +- Stabilize CI (:pr:`3119`) +- Bump ``pyupgrade`` from 2.32.1 to 2.34.0 (:pr:`3096`) +- Bump ``furo`` from 2022.6.4 to 2022.6.4.1 (:pr:`3095`) +- Bump ``mypy`` from 0.960 to 0.961 (:pr:`3093`) + +Version 20.0a1 +============== +*Released 2022-06-09* + +This is the technical changelog for version 20.0a1. More elaborate release notes can be found in the news channel `@pythontelegrambotchannel `_. + +Major Changes: +-------------- + +- Drop Support for ``ujson`` and instead ``BaseRequest.parse_json_payload`` (:pr:`3037`, :pr:`3072`) +- Drop ``InputFile.is_image`` (:pr:`3053`) +- Drop Explicit Type conversions in ``__init__`` s (:pr:`3056`) +- Handle List-Valued Attributes More Consistently (:pr:`3057`) +- Split ``{Command, Prefix}Handler`` And Make Attributes Immutable (:pr:`3045`) +- Align Behavior Of ``JobQueue.run_daily`` With ``cron`` (:pr:`3046`) +- Make PTB Specific Keyword-Only Arguments for PTB Specific in Bot methods (:pr:`3035`) +- Adjust Equality Comparisons to Fit Bot API 6.0 (:pr:`3033`) +- Add Tuple Based Version Info (:pr:`3030`) +- Improve Type Annotations for ``CallbackContext`` and Move Default Type Alias to ``ContextTypes.DEFAULT_TYPE`` (:pr:`3017`, :pr:`3023`) +- Rename ``Job.context`` to ``Job.data`` (:pr:`3028`) +- Rename ``Handler`` to ``BaseHandler`` (:pr:`3019`) + +New Features: +------------- + +- Add ``Application.post_init`` (:pr:`3078`) +- Add Arguments ``chat/user_id`` to ``CallbackContext`` And Example On Custom Webhook Setups (:pr:`3059`) +- Add Convenience Property ``Message.id`` (:pr:`3077`) +- Add Example for ``WebApp`` (:pr:`3052`) +- Rename ``telegram.bot_api_version`` to ``telegram.__bot_api_version__`` (:pr:`3030`) + +Bug Fixes: +---------- + +- Fix Non-Blocking Entry Point in ``ConversationHandler`` (:pr:`3068`) +- Escape Backslashes in ``escape_markdown`` (:pr:`3055`) + +Dependencies: +------------- + +- Update ``httpx`` requirement from ~=0.22.0 to ~=0.23.0 (:pr:`3069`) +- Update ``cachetools`` requirement from ~=5.0.0 to ~=5.2.0 (:pr:`3058`, :pr:`3080`) + +Minor Changes, Documentation Improvements and CI: +------------------------------------------------- + +- Move Examples To Documentation (:pr:`3089`) +- Documentation Improvements and Update Dependencies (:pr:`3010`, :pr:`3007`, :pr:`3012`, :pr:`3067`, :pr:`3081`, :pr:`3082`) +- Improve Some Unit Tests (:pr:`3026`) +- Update Code Quality dependencies (:pr:`3070`, :pr:`3032`,:pr:`2998`, :pr:`2999`) +- Don't Set Signal Handlers On Windows By Default (:pr:`3065`) +- Split ``{Command, Prefix}Handler`` And Make Attributes Immutable (:pr:`3045`) +- Apply ``isort`` and Update ``pre-commit.ci`` Configuration (:pr:`3049`) +- Adjust ``pre-commit`` Settings for ``isort`` (:pr:`3043`) +- Add Version Check to Examples (:pr:`3036`) +- Use ``Collection`` Instead of ``List`` and ``Tuple`` (:pr:`3025`) +- Remove Client-Side Parameter Validation (:pr:`3024`) +- Don't Pass Default Values of Optional Parameters to Telegram (:pr:`2978`) +- Stabilize ``Application.run_*`` on Python 3.7 (:pr:`3009`) +- Ignore Code Style Commits in ``git blame`` (:pr:`3003`) +- Adjust Tests to Changed API Behavior (:pr:`3002`) + +Version 20.0a0 +============== +*Released 2022-05-06* + +This is the technical changelog for version 20.0a0. More elaborate release notes can be found in the news channel `@pythontelegrambotchannel `_. + +Major Changes: +-------------- + +- Refactor Initialization of Persistence Classes + (:pr:`2604`) +- Drop Non-``CallbackContext`` API + (:pr:`2617`) +- Remove ``__dict__`` from ``__slots__`` and drop Python 3.6 + (:pr:`2619`, + :pr:`2636`) +- Move and Rename ``TelegramDecryptionError`` to + ``telegram.error.PassportDecryptionError`` + (:pr:`2621`) +- Make ``BasePersistence`` Methods Abstract + (:pr:`2624`) +- Remove ``day_is_strict`` argument of ``JobQueue.run_monthly`` + (:pr:`2634` + by `iota-008 `__) +- Move ``Defaults`` to ``telegram.ext`` + (:pr:`2648`) +- Remove Deprecated Functionality + (:pr:`2644`, + :pr:`2740`, + :pr:`2745`) +- Overhaul of Filters + (:pr:`2759`, + :pr:`2922`) +- Switch to ``asyncio`` and Refactor PTBs Architecture + (:pr:`2731`) +- Improve ``Job.__getattr__`` + (:pr:`2832`) +- Remove ``telegram.ReplyMarkup`` + (:pr:`2870`) +- Persistence of ``Bots``: Refactor Automatic Replacement and + Integration with ``TelegramObject`` + (:pr:`2893`) + +New Features: +------------- + +- Introduce Builder Pattern + (:pr:`2646`) +- Add ``Filters.update.edited`` + (:pr:`2705` + by `PhilippFr `__) +- Introduce ``Enums`` for ``telegram.constants`` + (:pr:`2708`) +- Accept File Paths for ``private_key`` + (:pr:`2724`) +- Associate ``Jobs`` with ``chat/user_id`` + (:pr:`2731`) +- Convenience Functionality for ``ChatInviteLinks`` + (:pr:`2782`) +- Add ``Dispatcher.add_handlers`` + (:pr:`2823`) +- Improve Error Messages in ``CommandHandler.__init__`` + (:pr:`2837`) +- ``Defaults.protect_content`` + (:pr:`2840`) +- Add ``Dispatcher.migrate_chat_data`` + (:pr:`2848` + by `DonalDuck004 `__) +- Add Method ``drop_chat/user_data`` to ``Dispatcher`` and Persistence + (:pr:`2852`) +- Add methods ``ChatPermissions.{all, no}_permissions`` (:pr:`2948`) +- Full Support for API 6.0 + (:pr:`2956`) +- Add Python 3.10 to Test Suite + (:pr:`2968`) + +Bug Fixes & Minor Changes: +-------------------------- + +- Improve Type Hinting for ``CallbackContext`` + (:pr:`2587` + by `revolter `__) +- Fix Signatures and Improve ``test_official`` + (:pr:`2643`) +- Refine ``Dispatcher.dispatch_error`` + (:pr:`2660`) +- Make ``InlineQuery.answer`` Raise ``ValueError`` + (:pr:`2675`) +- Improve Signature Inspection for Bot Methods + (:pr:`2686`) +- Introduce ``TelegramObject.set/get_bot`` + (:pr:`2712` + by `zpavloudis `__) +- Improve Subscription of ``TelegramObject`` + (:pr:`2719` + by `SimonDamberg `__) +- Use Enums for Dynamic Types & Rename Two Attributes in ``ChatMember`` + (:pr:`2817`) +- Return Plain Dicts from ``BasePersistence.get_*_data`` + (:pr:`2873`) +- Fix a Bug in ``ChatMemberUpdated.difference`` + (:pr:`2947`) +- Update Dependency Policy + (:pr:`2958`) + +Internal Restructurings & Improvements: +--------------------------------------- + +- Add User Friendly Type Check For Init Of + ``{Inline, Reply}KeyboardMarkup`` + (:pr:`2657`) +- Warnings Overhaul + (:pr:`2662`) +- Clear Up Import Policy + (:pr:`2671`) +- Mark Internal Modules As Private + (:pr:`2687` + by `kencx `__) +- Handle Filepaths via the ``pathlib`` Module + (:pr:`2688` + by `eldbud `__) +- Refactor MRO of ``InputMedia*`` and Some File-Like Classes + (:pr:`2717` + by `eldbud `__) +- Update Exceptions for Immutable Attributes + (:pr:`2749`) +- Refactor Warnings in ``ConversationHandler`` + (:pr:`2755`, + :pr:`2784`) +- Use ``__all__`` Consistently + (:pr:`2805`) + +CI, Code Quality & Test Suite Improvements: +------------------------------------------- + +- Add Custom ``pytest`` Marker to Ease Development + (:pr:`2628`) +- Pass Failing Jobs to Error Handlers + (:pr:`2692`) +- Update Notification Workflows + (:pr:`2695`) +- Use Error Messages for ``pylint`` Instead of Codes + (:pr:`2700` + by `Piraty `__) +- Make Tests Agnostic of the CWD + (:pr:`2727` + by `eldbud `__) +- Update Code Quality Dependencies + (:pr:`2748`) +- Improve Code Quality + (:pr:`2783`) +- Update ``pre-commit`` Settings & Improve a Test + (:pr:`2796`) +- Improve Code Quality & Test Suite + (:pr:`2843`) +- Fix failing animation tests + (:pr:`2865`) +- Update and Expand Tests & pre-commit Settings and Improve Code + Quality + (:pr:`2925`) +- Extend Code Formatting With Black + (:pr:`2972`) +- Update Workflow Permissions + (:pr:`2984`) +- Adapt Tests to Changed ``Bot.get_file`` Behavior + (:pr:`2995`) + +Documentation Improvements: +--------------------------- + +- Doc Fixes + (:pr:`2597`) +- Add Code Comment Guidelines to Contribution Guide + (:pr:`2612`) +- Add Cross-References to External Libraries & Other Documentation + Improvements + (:pr:`2693`, + :pr:`2691` + by `joesinghh `__, + :pr:`2739` + by `eldbud `__) +- Use Furo Theme, Make Parameters Referenceable, Add Documentation + Building to CI, Improve Links to Source Code & Other Improvements + (:pr:`2856`, + :pr:`2798`, + :pr:`2854`, + :pr:`2841`) +- Documentation Fixes & Improvements + (:pr:`2822`) +- Replace ``git.io`` Links + (:pr:`2872` + by `murugu-21 `__) +- Overhaul Readmes, Update RTD Startpage & Other Improvements + (:pr:`2969`) + +Version 13.11 +============= +*Released 2022-02-02* + +This is the technical changelog for version 13.11. More elaborate release notes can be found in the news channel `@pythontelegrambotchannel `_. + +**Major Changes:** + +- Full Support for Bot API 5.7 (:pr:`2881`) + +Version 13.10 +============= +*Released 2022-01-03* + +This is the technical changelog for version 13.10. More elaborate release notes can be found in the news channel `@pythontelegrambotchannel `_. + +**Major Changes:** + +- Full Support for API 5.6 (:pr:`2835`) + +**Minor Changes & Doc fixes:** + +- Update Copyright to 2022 (:pr:`2836`) +- Update Documentation of ``BotCommand`` (:pr:`2820`) + +Version 13.9 +============ +*Released 2021-12-11* + +This is the technical changelog for version 13.9. More elaborate release notes can be found in the news channel `@pythontelegrambotchannel `_. + +**Major Changes:** + +- Full Support for Api 5.5 (:pr:`2809`) + +**Minor Changes** + +- Adjust Automated Locking of Inactive Issues (:pr:`2775`) + +Version 13.8.1 +============== +*Released 2021-11-08* + +This is the technical changelog for version 13.8.1. More elaborate release notes can be found in the news channel `@pythontelegrambotchannel `_. + +**Doc fixes:** + +- Add ``ChatJoinRequest(Handler)`` to Docs (:pr:`2771`) + +Version 13.8 +============ +*Released 2021-11-08* + +This is the technical changelog for version 13.8. More elaborate release notes can be found in the news channel `@pythontelegrambotchannel `_. + +**Major Changes:** + +- Full support for API 5.4 (:pr:`2767`) + +**Minor changes, CI improvements, Doc fixes and Type hinting:** + +- Create Issue Template Forms (:pr:`2689`) +- Fix ``camelCase`` Functions in ``ExtBot`` (:pr:`2659`) +- Fix Empty Captions not Being Passed by ``Bot.copy_message`` (:pr:`2651`) +- Fix Setting Thumbs When Uploading A Single File (:pr:`2583`) +- Fix Bug in ``BasePersistence.insert``/``replace_bot`` for Objects with ``__dict__`` not in ``__slots__`` (:pr:`2603`) + +Version 13.7 +============ +*Released 2021-07-01* + +This is the technical changelog for version 13.7. More elaborate release notes can be found in the news channel `@pythontelegrambotchannel `_. + +**Major Changes:** + +- Full support for Bot API 5.3 (:pr:`2572`) + +**Bug Fixes:** + +- Fix Bug in ``BasePersistence.insert/replace_bot`` for Objects with ``__dict__`` in their slots (:pr:`2561`) +- Remove Incorrect Warning About ``Defaults`` and ``ExtBot`` (:pr:`2553`) + +**Minor changes, CI improvements, Doc fixes and Type hinting:** + +- Type Hinting Fixes (:pr:`2552`) +- Doc Fixes (:pr:`2551`) +- Improve Deprecation Warning for ``__slots__`` (:pr:`2574`) +- Stabilize CI (:pr:`2575`) +- Fix Coverage Configuration (:pr:`2571`) +- Better Exception-Handling for ``BasePersistence.replace/insert_bot`` (:pr:`2564`) +- Remove Deprecated ``pass_args`` from Deeplinking Example (:pr:`2550`) + +Version 13.6 +============ +*Released 2021-06-06* + +New Features: + +- Arbitrary ``callback_data`` (:pr:`1844`) +- Add ``ContextTypes`` & ``BasePersistence.refresh_user/chat/bot_data`` (:pr:`2262`) +- Add ``Filters.attachment`` (:pr:`2528`) +- Add ``pattern`` Argument to ``ChosenInlineResultHandler`` (:pr:`2517`) + +Major Changes: + +- Add ``slots`` (:pr:`2345`) + +Minor changes, CI improvements, Doc fixes and Type hinting: + +- Doc Fixes (:pr:`2495`, :pr:`2510`) +- Add ``max_connections`` Parameter to ``Updater.start_webhook`` (:pr:`2547`) +- Fix for ``Promise.done_callback`` (:pr:`2544`) +- Improve Code Quality (:pr:`2536`, :pr:`2454`) +- Increase Test Coverage of ``CallbackQueryHandler`` (:pr:`2520`) +- Stabilize CI (:pr:`2522`, :pr:`2537`, :pr:`2541`) +- Fix ``send_phone_number_to_provider`` argument for ``Bot.send_invoice`` (:pr:`2527`) +- Handle Classes as Input for ``BasePersistence.replace/insert_bot`` (:pr:`2523`) +- Bump Tornado Version and Remove Workaround from :pr:`2067` (:pr:`2494`) + +Version 13.5 +============ +*Released 2021-04-30* + +**Major Changes:** + +- Full support of Bot API 5.2 (:pr:`2489`). + + .. note:: + The ``start_parameter`` argument of ``Bot.send_invoice`` and the corresponding shortcuts is now optional, so the order of + parameters had to be changed. Make sure to update your method calls accordingly. + +- Update ``ChatActions``, Deprecating ``ChatAction.RECORD_AUDIO`` and ``ChatAction.UPLOAD_AUDIO`` (:pr:`2460`) + +**New Features:** + +- Convenience Utilities & Example for Handling ``ChatMemberUpdated`` (:pr:`2490`) +- ``Filters.forwarded_from`` (:pr:`2446`) + +**Minor changes, CI improvements, Doc fixes and Type hinting:** + +- Improve Timeouts in ``ConversationHandler`` (:pr:`2417`) +- Stabilize CI (:pr:`2480`) +- Doc Fixes (:pr:`2437`) +- Improve Type Hints of Data Filters (:pr:`2456`) +- Add Two ``UserWarnings`` (:pr:`2464`) +- Improve Code Quality (:pr:`2450`) +- Update Fallback Test-Bots (:pr:`2451`) +- Improve Examples (:pr:`2441`, :pr:`2448`) + +Version 13.4.1 +============== +*Released 2021-03-14* + +**Hot fix release:** + +- Fixed a bug in ``setup.py`` (:pr:`2431`) + +Version 13.4 +============ +*Released 2021-03-14* + +**Major Changes:** + +- Full support of Bot API 5.1 (:pr:`2424`) + +**Minor changes, CI improvements, doc fixes and type hinting:** + +- Improve ``Updater.set_webhook`` (:pr:`2419`) +- Doc Fixes (:pr:`2404`) +- Type Hinting Fixes (:pr:`2425`) +- Update ``pre-commit`` Settings (:pr:`2415`) +- Fix Logging for Vendored ``urllib3`` (:pr:`2427`) +- Stabilize Tests (:pr:`2409`) + +Version 13.3 +============ +*Released 2021-02-19* + +**Major Changes:** + +- Make ``cryptography`` Dependency Optional & Refactor Some Tests (:pr:`2386`, :pr:`2370`) +- Deprecate ``MessageQueue`` (:pr:`2393`) + +**Bug Fixes:** + +- Refactor ``Defaults`` Integration (:pr:`2363`) +- Add Missing ``telegram.SecureValue`` to init and Docs (:pr:`2398`) + +**Minor changes:** + +- Doc Fixes (:pr:`2359`) + +Version 13.2 +============ +*Released 2021-02-02* + +**Major Changes:** + +- Introduce ``python-telegram-bot-raw`` (:pr:`2324`) +- Explicit Signatures for Shortcuts (:pr:`2240`) + +**New Features:** + +- Add Missing Shortcuts to ``Message`` (:pr:`2330`) +- Rich Comparison for ``Bot`` (:pr:`2320`) +- Add ``run_async`` Parameter to ``ConversationHandler`` (:pr:`2292`) +- Add New Shortcuts to ``Chat`` (:pr:`2291`) +- Add New Constant ``MAX_ANSWER_CALLBACK_QUERY_TEXT_LENGTH`` (:pr:`2282`) +- Allow Passing Custom Filename For All Media (:pr:`2249`) +- Handle Bytes as File Input (:pr:`2233`) + +**Bug Fixes:** + +- Fix Escaping in Nested Entities in ``Message`` Properties (:pr:`2312`) +- Adjust Calling of ``Dispatcher.update_persistence`` (:pr:`2285`) +- Add ``quote`` kwarg to ``Message.reply_copy`` (:pr:`2232`) +- ``ConversationHandler``: Docs & ``edited_channel_post`` behavior (:pr:`2339`) + +**Minor changes, CI improvements, doc fixes and type hinting:** + +- Doc Fixes (:pr:`2253`, :pr:`2225`) +- Reduce Usage of ``typing.Any`` (:pr:`2321`) +- Extend Deeplinking Example (:pr:`2335`) +- Add pyupgrade to pre-commit Hooks (:pr:`2301`) +- Add PR Template (:pr:`2299`) +- Drop Nightly Tests & Update Badges (:pr:`2323`) +- Update Copyright (:pr:`2289`, :pr:`2287`) +- Change Order of Class DocStrings (:pr:`2256`) +- Add macOS to Test Matrix (:pr:`2266`) +- Start Using Versioning Directives in Docs (:pr:`2252`) +- Improve Annotations & Docs of Handlers (:pr:`2243`) + +Version 13.1 +============ +*Released 2020-11-29* + +**Major Changes:** + +- Full support of Bot API 5.0 (:pr:`2181`, :pr:`2186`, :pr:`2190`, :pr:`2189`, :pr:`2183`, :pr:`2184`, :pr:`2188`, :pr:`2185`, :pr:`2192`, :pr:`2196`, :pr:`2193`, :pr:`2223`, :pr:`2199`, :pr:`2187`, :pr:`2147`, :pr:`2205`) + +**New Features:** + +- Add ``Defaults.run_async`` (:pr:`2210`) +- Improve and Expand ``CallbackQuery`` Shortcuts (:pr:`2172`) +- Add XOR Filters and make ``Filters.name`` a Property (:pr:`2179`) +- Add ``Filters.document.file_extension`` (:pr:`2169`) +- Add ``Filters.caption_regex`` (:pr:`2163`) +- Add ``Filters.chat_type`` (:pr:`2128`) +- Handle Non-Binary File Input (:pr:`2202`) + +**Bug Fixes:** + +- Improve Handling of Custom Objects in ``BasePersistence.insert``/``replace_bot`` (:pr:`2151`) +- Fix bugs in ``replace/insert_bot`` (:pr:`2218`) + +**Minor changes, CI improvements, doc fixes and type hinting:** + +- Improve Type hinting (:pr:`2204`, :pr:`2118`, :pr:`2167`, :pr:`2136`) +- Doc Fixes & Extensions (:pr:`2201`, :pr:`2161`) +- Use F-Strings Where Possible (:pr:`2222`) +- Rename kwargs to _kwargs where possible (:pr:`2182`) +- Comply with PEP561 (:pr:`2168`) +- Improve Code Quality (:pr:`2131`) +- Switch Code Formatting to Black (:pr:`2122`, :pr:`2159`, :pr:`2158`) +- Update Wheel Settings (:pr:`2142`) +- Update ``timerbot.py`` to ``v13.0`` (:pr:`2149`) +- Overhaul Constants (:pr:`2137`) +- Add Python 3.9 to Test Matrix (:pr:`2132`) +- Switch Codecov to ``GitHub`` Action (:pr:`2127`) +- Specify Required pytz Version (:pr:`2121`) + +Version 13.0 +============ +*Released 2020-10-07* + +**For a detailed guide on how to migrate from v12 to v13, see this** `wiki page `_. + +**Major Changes:** + +- Deprecate old-style callbacks, i.e. set ``use_context=True`` by default (:pr:`2050`) +- Refactor Handling of Message VS Update Filters (:pr:`2032`) +- Deprecate ``Message.default_quote`` (:pr:`1965`) +- Refactor persistence of Bot instances (:pr:`1994`) +- Refactor ``JobQueue`` (:pr:`1981`) +- Refactor handling of kwargs in Bot methods (:pr:`1924`) +- Refactor ``Dispatcher.run_async``, deprecating the ``@run_async`` decorator (:pr:`2051`) + +**New Features:** + +- Type Hinting (:pr:`1920`) +- Automatic Pagination for ``answer_inline_query`` (:pr:`2072`) +- ``Defaults.tzinfo`` (:pr:`2042`) +- Extend rich comparison of objects (:pr:`1724`) +- Add ``Filters.via_bot`` (:pr:`2009`) +- Add missing shortcuts (:pr:`2043`) +- Allow ``DispatcherHandlerStop`` in ``ConversationHandler`` (:pr:`2059`) +- Make Errors picklable (:pr:`2106`) + +**Minor changes, CI improvements, doc fixes or bug fixes:** + +- Fix Webhook not working on Windows with Python 3.8+ (:pr:`2067`) +- Fix setting thumbs with ``send_media_group`` (:pr:`2093`) +- Make ``MessageHandler`` filter for ``Filters.update`` first (:pr:`2085`) +- Fix ``PicklePersistence.flush()`` with only ``bot_data`` (:pr:`2017`) +- Add test for clean argument of ``Updater.start_polling/webhook`` (:pr:`2002`) +- Doc fixes, refinements and additions (:pr:`2005`, :pr:`2008`, :pr:`2089`, :pr:`2094`, :pr:`2090`) +- CI fixes (:pr:`2018`, :pr:`2061`) +- Refine ``pollbot.py`` example (:pr:`2047`) +- Refine Filters in examples (:pr:`2027`) +- Rename ``echobot`` examples (:pr:`2025`) +- Use Lock-Bot to lock old threads (:pr:`2048`, :pr:`2052`, :pr:`2049`, :pr:`2053`) + +Version 12.8 +============ +*Released 2020-06-22* + +**Major Changes:** + +- Remove Python 2 support (:pr:`1715`) +- Bot API 4.9 support (:pr:`1980`) +- IDs/Usernames of ``Filters.user`` and ``Filters.chat`` can now be updated (:pr:`1757`) + +**Minor changes, CI improvements, doc fixes or bug fixes:** + +- Update contribution guide and stale bot (:pr:`1937`) +- Remove ``NullHandlers`` (:pr:`1913`) +- Improve and expand examples (:pr:`1943`, :pr:`1995`, :pr:`1983`, :pr:`1997`) +- Doc fixes (:pr:`1940`, :pr:`1962`) +- Add ``User.send_poll()`` shortcut (:pr:`1968`) +- Ignore private attributes en ``TelegramObject.to_dict()`` (:pr:`1989`) +- Stabilize CI (:pr:`2000`) + +Version 12.7 +============ +*Released 2020-05-02* + +**Major Changes:** + +- Bot API 4.8 support. **Note:** The ``Dice`` object now has a second positional argument ``emoji``. This is relevant, if you instantiate ``Dice`` objects manually. (:pr:`1917`) +- Added ``tzinfo`` argument to ``helpers.from_timestamp``. It now returns an timezone aware object. This is relevant for ``Message.{date,forward_date,edit_date}``, ``Poll.close_date`` and ``ChatMember.until_date`` (:pr:`1621`) + +**New Features:** + +- New method ``run_monthly`` for the ``JobQueue`` (:pr:`1705`) +- ``Job.next_t`` now gives the datetime of the jobs next execution (:pr:`1685`) + +**Minor changes, CI improvements, doc fixes or bug fixes:** + +- Stabalize CI (:pr:`1919`, :pr:`1931`) +- Use ABCs ``@abstractmethod`` instead of raising ``NotImplementedError`` for ``Handler``, ``BasePersistence`` and ``BaseFilter`` (:pr:`1905`) +- Doc fixes (:pr:`1914`, :pr:`1902`, :pr:`1910`) + +Version 12.6.1 +============== +*Released 2020-04-11* + +**Bug fixes:** + +- Fix serialization of ``reply_markup`` in media messages (:pr:`1889`) + +Version 12.6 +============ +*Released 2020-04-10* + +**Major Changes:** + +- Bot API 4.7 support. **Note:** In ``Bot.create_new_sticker_set`` and ``Bot.add_sticker_to_set``, the order of the parameters had be changed, as the ``png_sticker`` parameter is now optional. (:pr:`1858`) + +**Minor changes, CI improvements or bug fixes:** + +- Add tests for ``swtich_inline_query(_current_chat)`` with empty string (:pr:`1635`) +- Doc fixes (:pr:`1854`, :pr:`1874`, :pr:`1884`) +- Update issue templates (:pr:`1880`) +- Favor concrete types over "Iterable" (:pr:`1882`) +- Pass last valid ``CallbackContext`` to ``TIMEOUT`` handlers of ``ConversationHandler`` (:pr:`1826`) +- Tweak handling of persistence and update persistence after job calls (:pr:`1827`) +- Use checkout@v2 for GitHub actions (:pr:`1887`) + +Version 12.5.1 +============== +*Released 2020-03-30* + +**Minor changes, doc fixes or bug fixes:** + +- Add missing docs for `PollHandler` and `PollAnswerHandler` (:pr:`1853`) +- Fix wording in `Filters` docs (:pr:`1855`) +- Reorder tests to make them more stable (:pr:`1835`) +- Make `ConversationHandler` attributes immutable (:pr:`1756`) +- Make `PrefixHandler` attributes `command` and `prefix` editable (:pr:`1636`) +- Fix UTC as default `tzinfo` for `Job` (:pr:`1696`) + +Version 12.5 +============ +*Released 2020-03-29* + +**New Features:** + +- `Bot.link` gives the `t.me` link of the bot (:pr:`1770`) + +**Major Changes:** + +- Bot API 4.5 and 4.6 support. (:pr:`1508`, :pr:`1723`) + +**Minor changes, CI improvements or bug fixes:** + +- Remove legacy CI files (:pr:`1783`, :pr:`1791`) +- Update pre-commit config file (:pr:`1787`) +- Remove builtin names (:pr:`1792`) +- CI improvements (:pr:`1808`, :pr:`1848`) +- Support Python 3.8 (:pr:`1614`, :pr:`1824`) +- Use stale bot for auto closing stale issues (:pr:`1820`, :pr:`1829`, :pr:`1840`) +- Doc fixes (:pr:`1778`, :pr:`1818`) +- Fix typo in `edit_message_media` (:pr:`1779`) +- In examples, answer CallbackQueries and use `edit_message_text` shortcut (:pr:`1721`) +- Revert accidental change in vendored urllib3 (:pr:`1775`) + +Version 12.4.2 +============== +*Released 2020-02-10* + +**Bug Fixes** + +- Pass correct parse_mode to InlineResults if bot.defaults is None (:pr:`1763`) +- Make sure PP can read files that dont have bot_data (:pr:`1760`) + +Version 12.4.1 +============== +*Released 2020-02-08* + +This is a quick release for :pr:`1744` which was accidently left out of v12.4.0 though mentioned in the +release notes. + +Version 12.4.0 +============== +*Released 2020-02-08* + +**New features:** + +- Set default values for arguments appearing repeatedly. We also have a `wiki page for the new defaults`_. (:pr:`1490`) +- Store data in ``CallbackContext.bot_data`` to access it in every callback. Also persists. (:pr:`1325`) +- ``Filters.poll`` allows only messages containing a poll (:pr:`1673`) + +**Major changes:** + +- ``Filters.text`` now accepts messages that start with a slash, because ``CommandHandler`` checks for ``MessageEntity.BOT_COMMAND`` since v12. This might lead to your MessageHandlers receiving more updates than before (:pr:`1680`). +- ``Filters.command`` new checks for ``MessageEntity.BOT_COMMAND`` instead of just a leading slash. Also by ``Filters.command(False)`` you can now filters for messages containing a command `anywhere` in the text (:pr:`1744`). + +**Minor changes, CI improvements or bug fixes:** + +- Add ``disptacher`` argument to ``Updater`` to allow passing a customized ``Dispatcher`` (:pr:`1484`) +- Add missing names for ``Filters`` (:pr:`1632`) +- Documentation fixes (:pr:`1624`, :pr:`1647`, :pr:`1669`, :pr:`1703`, :pr:`1718`, :pr:`1734`, :pr:`1740`, :pr:`1642`, :pr:`1739`, :pr:`1746`) +- CI improvements (:pr:`1716`, :pr:`1731`, :pr:`1738`, :pr:`1748`, :pr:`1749`, :pr:`1750`, :pr:`1752`) +- Fix spelling issue for ``encode_conversations_to_json`` (:pr:`1661`) +- Remove double assignement of ``Dispatcher.job_queue`` (:pr:`1698`) +- Expose dispatcher as property for ``CallbackContext`` (:pr:`1684`) +- Fix ``None`` check in ``JobQueue._put()`` (:pr:`1707`) +- Log datetimes correctly in ``JobQueue`` (:pr:`1714`) +- Fix false ``Message.link`` creation for private groups (:pr:`1741`) +- Add option ``--with-upstream-urllib3`` to `setup.py` to allow using non-vendored version (:pr:`1725`) +- Fix persistence for nested ``ConversationHandlers`` (:pr:`1679`) +- Improve handling of non-decodable server responses (:pr:`1623`) +- Fix download for files without ``file_path`` (:pr:`1591`) +- test_webhook_invalid_posts is now considered flaky and retried on failure (:pr:`1758`) + +.. _`wiki page for the new defaults`: https://github.com/python-telegram-bot/python-telegram-bot/wiki/Adding-defaults-to-your-bot + +Version 12.3.0 +============== +*Released 2020-01-11* + +**New features:** + +- `Filters.caption` allows only messages with caption (:pr:`1631`). +- Filter for exact messages/captions with new capability of `Filters.text` and `Filters.caption`. Especially useful in combination with ReplyKeyboardMarkup. (:pr:`1631`). + +**Major changes:** + +- Fix inconsistent handling of naive datetimes (:pr:`1506`). + +**Minor changes, CI improvements or bug fixes:** + +- Documentation fixes (:pr:`1558`, :pr:`1569`, :pr:`1579`, :pr:`1572`, :pr:`1566`, :pr:`1577`, :pr:`1656`). +- Add mutex protection on `ConversationHandler` (:pr:`1533`). +- Add `MAX_PHOTOSIZE_UPLOAD` constant (:pr:`1560`). +- Add args and kwargs to `Message.forward()` (:pr:`1574`). +- Transfer to GitHub Actions CI (:pr:`1555`, :pr:`1556`, :pr:`1605`, :pr:`1606`, :pr:`1607`, :pr:`1612`, :pr:`1615`, :pr:`1645`). +- Fix deprecation warning with Py3.8 by vendored urllib3 (:pr:`1618`). +- Simplify assignements for optional arguments (:pr:`1600`) +- Allow private groups for `Message.link` (:pr:`1619`). +- Fix wrong signature call for `ConversationHandler.TIMEOUT` handlers (:pr:`1653`). + +Version 12.2.0 +============== +*Released 2019-10-14* + +**New features:** + +- Nested ConversationHandlers (:pr:`1512`). + +**Minor changes, CI improvments or bug fixes:** + +- Fix CI failures due to non-backward compat attrs depndency (:pr:`1540`). +- travis.yaml: TEST_OFFICIAL removed from allowed_failures. +- Fix typos in examples (:pr:`1537`). +- Fix Bot.to_dict to use proper first_name (:pr:`1525`). +- Refactor ``test_commandhandler.py`` (:pr:`1408`). +- Add Python 3.8 (RC version) to Travis testing matrix (:pr:`1543`). +- test_bot.py: Add to_dict test (:pr:`1544`). +- Flake config moved into setup.cfg (:pr:`1546`). + +Version 12.1.1 +============== +*Released 2019-09-18* + +**Hot fix release** + +Fixed regression in the vendored urllib3 (:pr:`1517`). + +Version 12.1.0 +================ +*Released 2019-09-13* + +**Major changes:** + +- Bot API 4.4 support (:pr:`1464`, :pr:`1510`) +- Add `get_file` method to `Animation` & `ChatPhoto`. Add, `get_small_file` & `get_big_file` + methods to `ChatPhoto` (:pr:`1489`) +- Tools for deep linking (:pr:`1049`) + +**Minor changes and/or bug fixes:** + +- Documentation fixes (:pr:`1500`, :pr:`1499`) +- Improved examples (:pr:`1502`) + +Version 12.0.0 +================ +*Released 2019-08-29* + +Well... This felt like decades. But here we are with a new release. + +Expect minor releases soon (mainly complete Bot API 4.4 support) + +**Major and/or breaking changes:** + +- Context based callbacks +- Persistence +- PrefixHandler added (Handler overhaul) +- Deprecation of RegexHandler and edited_messages, channel_post, etc. arguments (Filter overhaul) +- Various ConversationHandler changes and fixes +- Bot API 4.1, 4.2, 4.3 support +- Python 3.4 is no longer supported +- Error Handler now handles all types of exceptions (:pr:`1485`) +- Return UTC from from_timestamp() (:pr:`1485`) + +**See the wiki page at https://github.com/python-telegram-bot/python-telegram-bot/wiki/Transition-guide-to-Version-12.0 for a detailed guide on how to migrate from version 11 to version 12.** + +Context based callbacks (:pr:`1100`) +------------------------------------ + +- Use of ``pass_`` in handlers is deprecated. +- Instead use ``use_context=True`` on ``Updater`` or ``Dispatcher`` and change callback from (bot, update, others...) to (update, context). +- This also applies to error handlers ``Dispatcher.add_error_handler`` and JobQueue jobs (change (bot, job) to (context) here). +- For users with custom handlers subclassing Handler, this is mostly backwards compatible, but to use the new context based callbacks you need to implement the new collect_additional_context method. +- Passing bot to ``JobQueue.__init__`` is deprecated. Use JobQueue.set_dispatcher with a dispatcher instead. +- Dispatcher makes sure to use a single `CallbackContext` for a entire update. This means that if an update is handled by multiple handlers (by using the group argument), you can add custom arguments to the `CallbackContext` in a lower group handler and use it in higher group handler. NOTE: Never use with @run_async, see docs for more info. (:pr:`1283`) +- If you have custom handlers they will need to be updated to support the changes in this release. +- Update all examples to use context based callbacks. + +Persistence (:pr:`1017`) +------------------------ + +- Added PicklePersistence and DictPersistence for adding persistence to your bots. +- BasePersistence can be subclassed for all your persistence needs. +- Add a new example that shows a persistent ConversationHandler bot + +Handler overhaul (:pr:`1114`) +----------------------------- + +- CommandHandler now only triggers on actual commands as defined by telegram servers (everything that the clients mark as a tabable link). +- PrefixHandler can be used if you need to trigger on prefixes (like all messages starting with a "/" (old CommandHandler behaviour) or even custom prefixes like "#" or "!"). + +Filter overhaul (:pr:`1221`) +---------------------------- + +- RegexHandler is deprecated and should be replaced with a MessageHandler with a regex filter. +- Use update filters to filter update types instead of arguments (message_updates, channel_post_updates and edited_updates) on the handlers. +- Completely remove allow_edited argument - it has been deprecated for a while. +- data_filters now exist which allows filters that return data into the callback function. This is how the regex filter is implemented. +- All this means that it no longer possible to use a list of filters in a handler. Use bitwise operators instead! + +ConversationHandler +------------------- + +- Remove ``run_async_timeout`` and ``timed_out_behavior`` arguments (:pr:`1344`) +- Replace with ``WAITING`` constant and behavior from states (:pr:`1344`) +- Only emit one warning for multiple CallbackQueryHandlers in a ConversationHandler (:pr:`1319`) +- Use warnings.warn for ConversationHandler warnings (:pr:`1343`) +- Fix unresolvable promises (:pr:`1270`) + +Bug fixes & improvements +------------------------ + +- Handlers should be faster due to deduped logic. +- Avoid compiling compiled regex in regex filter. (:pr:`1314`) +- Add missing ``left_chat_member`` to Message.MESSAGE_TYPES (:pr:`1336`) +- Make custom timeouts actually work properly (:pr:`1330`) +- Add convenience classmethods (from_button, from_row and from_column) to InlineKeyboardMarkup +- Small typo fix in setup.py (:pr:`1306`) +- Add Conflict error (HTTP error code 409) (:pr:`1154`) +- Change MAX_CAPTION_LENGTH to 1024 (:pr:`1262`) +- Remove some unnecessary clauses (:pr:`1247`, :pr:`1239`) +- Allow filenames without dots in them when sending files (:pr:`1228`) +- Fix uploading files with unicode filenames (:pr:`1214`) +- Replace http.server with Tornado (:pr:`1191`) +- Allow SOCKSConnection to parse username and password from URL (:pr:`1211`) +- Fix for arguments in passport/data.py (:pr:`1213`) +- Improve message entity parsing by adding text_mention (:pr:`1206`) +- Documentation fixes (:pr:`1348`, :pr:`1397`, :pr:`1436`) +- Merged filters short-circuit (:pr:`1350`) +- Fix webhook listen with tornado (:pr:`1383`) +- Call task_done() on update queue after update processing finished (:pr:`1428`) +- Fix send_location() - latitude may be 0 (:pr:`1437`) +- Make MessageEntity objects comparable (:pr:`1465`) +- Add prefix to thread names (:pr:`1358`) + +Buf fixes since v12.0.0b1 +------------------------- + +- Fix setting bot on ShippingQuery (:pr:`1355`) +- Fix _trigger_timeout() missing 1 required positional argument: 'job' (:pr:`1367`) +- Add missing message.text check in PrefixHandler check_update (:pr:`1375`) +- Make updates persist even on DispatcherHandlerStop (:pr:`1463`) +- Dispatcher force updating persistence object's chat data attribute(:pr:`1462`) + +Internal improvements +--------------------- + +- Finally fix our CI builds mostly (too many commits and PRs to list) +- Use multiple bots for CI to improve testing times significantly. +- Allow pypy to fail in CI. +- Remove the last CamelCase CheckUpdate methods from the handlers we missed earlier. +- test_official is now executed in a different job + +Version 11.1.0 +============== +*Released 2018-09-01* + +Fixes and updates for Telegram Passport: (:pr:`1198`) + +- Fix passport decryption failing at random times +- Added support for middle names. +- Added support for translations for documents +- Add errors for translations for documents +- Added support for requesting names in the language of the user's country of residence +- Replaced the payload parameter with the new parameter nonce +- Add hash to EncryptedPassportElement + +Version 11.0.0 +============== +*Released 2018-08-29* + +Fully support Bot API version 4.0! +(also some bugfixes :)) + +Telegram Passport (:pr:`1174`): + +- Add full support for telegram passport. + - New types: PassportData, PassportFile, EncryptedPassportElement, EncryptedCredentials, PassportElementError, PassportElementErrorDataField, PassportElementErrorFrontSide, PassportElementErrorReverseSide, PassportElementErrorSelfie, PassportElementErrorFile and PassportElementErrorFiles. + - New bot method: set_passport_data_errors + - New filter: Filters.passport_data + - Field passport_data field on Message + - PassportData can be easily decrypted. + - PassportFiles are automatically decrypted if originating from decrypted PassportData. +- See new passportbot.py example for details on how to use, or go to `our telegram passport wiki page`_ for more info +- NOTE: Passport decryption requires new dependency `cryptography`. + +Inputfile rework (:pr:`1184`): + +- Change how Inputfile is handled internally +- This allows support for specifying the thumbnails of photos and videos using the thumb= argument in the different send\_ methods. +- Also allows Bot.send_media_group to actually finally send more than one media. +- Add thumb to Audio, Video and Videonote +- Add Bot.edit_message_media together with InputMediaAnimation, InputMediaAudio, and inputMediaDocument. + +Other Bot API 4.0 changes: + +- Add forusquare_type to Venue, InlineQueryResultVenue, InputVenueMessageContent, and Bot.send_venue. (:pr:`1170`) +- Add vCard support by adding vcard field to Contact, InlineQueryResultContact, InputContactMessageContent, and Bot.send_contact. (:pr:`1166`) +- Support new message entities: CASHTAG and PHONE_NUMBER. (:pr:`1179`) + - Cashtag seems to be things like `$USD` and `$GBP`, but it seems telegram doesn't currently send them to bots. + - Phone number also seems to have limited support for now +- Add Bot.send_animation, add width, height, and duration to Animation, and add Filters.animation. (:pr:`1172`) + +Non Bot API 4.0 changes: + +- Minor integer comparison fix (:pr:`1147`) +- Fix Filters.regex failing on non-text message (:pr:`1158`) +- Fix ProcessLookupError if process finishes before we kill it (:pr:`1126`) +- Add t.me links for User, Chat and Message if available and update User.mention_* (:pr:`1092`) +- Fix mention_markdown/html on py2 (:pr:`1112`) + +.. _`our telegram passport wiki page`: https://github.com/python-telegram-bot/python-telegram-bot/wiki/Telegram-Passport + +Version 10.1.0 +============== +*Released 2018-05-02* + +Fixes changing previous behaviour: + +- Add urllib3 fix for socks5h support (:pr:`1085`) +- Fix send_sticker() timeout=20 (:pr:`1088`) + +Fixes: + +- Add a caption_entity filter for filtering caption entities (:pr:`1068`) +- Inputfile encode filenames (:pr:`1086`) +- InputFile: Fix proper naming of file when reading from subprocess.PIPE (:pr:`1079`) +- Remove pytest-catchlog from requirements (:pr:`1099`) +- Documentation fixes (:pr:`1061`, :pr:`1078`, :pr:`1081`, :pr:`1096`) + +Version 10.0.2 +============== +*Released 2018-04-17* + +Important fix: + +- Handle utf8 decoding errors (:pr:`1076`) + +New features: + +- Added Filter.regex (:pr:`1028`) +- Filters for Category and file types (:pr:`1046`) +- Added video note filter (:pr:`1067`) + +Fixes: + +- Fix in telegram.Message (:pr:`1042`) +- Make chat_id a positional argument inside shortcut methods of Chat and User classes (:pr:`1050`) +- Make Bot.full_name return a unicode object. (:pr:`1063`) +- CommandHandler faster check (:pr:`1074`) +- Correct documentation of Dispatcher.add_handler (:pr:`1071`) +- Various small fixes to documentation. + +Version 10.0.1 +============== +*Released 2018-03-05* + +Fixes: + +- Fix conversationhandler timeout (PR :pr:`1032`) +- Add missing docs utils (PR :pr:`912`) + +Version 10.0.0 +============== +*Released 2018-03-02* + +Non backward compatabile changes and changed defaults + +- JobQueue: Remove deprecated prevent_autostart & put() (PR :pr:`1012`) +- Bot, Updater: Remove deprecated network_delay (PR :pr:`1012`) +- Remove deprecated Message.new_chat_member (PR :pr:`1012`) +- Retry bootstrap phase indefinitely (by default) on network errors (PR :pr:`1018`) + +New Features + +- Support v3.6 API (PR :pr:`1006`) +- User.full_name convinience property (PR :pr:`949`) +- Add `send_phone_number_to_provider` and `send_email_to_provider` arguments to send_invoice (PR :pr:`986`) +- Bot: Add shortcut methods reply_{markdown,html} (PR :pr:`827`) +- Bot: Add shortcut method reply_media_group (PR :pr:`994`) +- Added utils.helpers.effective_message_type (PR :pr:`826`) +- Bot.get_file now allows passing a file in addition to file_id (PR :pr:`963`) +- Add .get_file() to Audio, Document, PhotoSize, Sticker, Video, VideoNote and Voice (PR :pr:`963`) +- Add .send_*() methods to User and Chat (PR :pr:`963`) +- Get jobs by name (PR :pr:`1011`) +- Add Message caption html/markdown methods (PR :pr:`1013`) +- File.download_as_bytearray - new method to get a d/led file as bytearray (PR :pr:`1019`) +- File.download(): Now returns a meaningful return value (PR :pr:`1019`) +- Added conversation timeout in ConversationHandler (PR :pr:`895`) + +Changes + +- Store bot in PreCheckoutQuery (PR :pr:`953`) +- Updater: Issue INFO log upon received signal (PR :pr:`951`) +- JobQueue: Thread safety fixes (PR :pr:`977`) +- WebhookHandler: Fix exception thrown during error handling (PR :pr:`985`) +- Explicitly check update.effective_chat in ConversationHandler.check_update (PR :pr:`959`) +- Updater: Better handling of timeouts during get_updates (PR :pr:`1007`) +- Remove unnecessary to_dict() (PR :pr:`834`) +- CommandHandler - ignore strings in entities and "/" followed by whitespace (PR :pr:`1020`) +- Documentation & style fixes (PR :pr:`942`, PR :pr:`956`, PR :pr:`962`, PR :pr:`980`, PR :pr:`983`) + +Version 9.0.0 +============= +*Released 2017-12-08* + +Breaking changes (possibly) + +- Drop support for python 3.3 (PR :pr:`930`) + +New Features + +- Support Bot API 3.5 (PR :pr:`920`) + +Changes + +- Fix race condition in dispatcher start/stop (:pr:`887`) +- Log error trace if there is no error handler registered (:pr:`694`) +- Update examples with consistent string formatting (:pr:`870`) +- Various changes and improvements to the docs. + +Version 8.1.1 +============= +*Released 2017-10-15* + +- Fix Commandhandler crashing on single character messages (PR :pr:`873`). + +Version 8.1.0 +============= +*Released 2017-10-14* + +New features +- Support Bot API 3.4 (PR :pr:`865`). + +Changes +- MessageHandler & RegexHandler now consider channel_updates. +- Fix command not recognized if it is directly followed by a newline (PR :pr:`869`). +- Removed Bot._message_wrapper (PR :pr:`822`). +- Unitests are now also running on AppVeyor (Windows VM). +- Various unitest improvements. +- Documentation fixes. + +Version 8.0.0 +============= +*Released 2017-09-01* + +New features + +- Fully support Bot Api 3.3 (PR :pr:`806`). +- DispatcherHandlerStop (`see docs`_). +- Regression fix for text_html & text_markdown (PR :pr:`777`). +- Added effective_attachment to message (PR :pr:`766`). + +Non backward compatible changes + +- Removed Botan support from the library (PR :pr:`776`). +- Fully support Bot Api 3.3 (PR :pr:`806`). +- Remove de_json() (PR :pr:`789`). + +Changes + +- Sane defaults for tcp socket options on linux (PR :pr:`754`). +- Add RESTRICTED as constant to ChatMember (PR :pr:`761`). +- Add rich comparison to CallbackQuery (PR :pr:`764`). +- Fix get_game_high_scores (PR :pr:`771`). +- Warn on small con_pool_size during custom initalization of Updater (PR :pr:`793`). +- Catch exceptions in error handlerfor errors that happen during polling (PR :pr:`810`). +- For testing we switched to pytest (PR :pr:`788`). +- Lots of small improvements to our tests and documentation. + +.. _`see docs`: https://docs.python-telegram-bot.org/en/v13.11/telegram.ext.dispatcher.html?highlight=Dispatcher.add_handler#telegram.ext.Dispatcher.add_handler + +Version 7.0.1 +=============== +*Released 2017-07-28* + +- Fix TypeError exception in RegexHandler (PR #751). +- Small documentation fix (PR #749). + +Version 7.0.0 +============= +*Released 2017-07-25* + +- Fully support Bot API 3.2. +- New filters for handling messages from specific chat/user id (PR #677). +- Add the possibility to add objects as arguments to send_* methods (PR #742). +- Fixed download of URLs with UTF-8 chars in path (PR #688). +- Fixed URL parsing for ``Message`` text properties (PR #689). +- Fixed args dispatching in ``MessageQueue``'s decorator (PR #705). +- Fixed regression preventing IPv6 only hosts from connnecting to Telegram servers (Issue #720). +- ConvesationHandler - check if a user exist before using it (PR #699). +- Removed deprecated ``telegram.Emoji``. +- Removed deprecated ``Botan`` import from ``utils`` (``Botan`` is still available through ``contrib``). +- Removed deprecated ``ReplyKeyboardHide``. +- Removed deprecated ``edit_message`` argument of ``bot.set_game_score``. +- Internal restructure of files. +- Improved documentation. +- Improved unitests. + +Pre-version 7.0 +=============== + +**2017-06-18** + +*Released 6.1.0* + +- Fully support Bot API 3.0 +- Add more fine-grained filters for status updates +- Bug fixes and other improvements + +**2017-05-29** + +*Released 6.0.3* + +- Faulty PyPI release + +**2017-05-29** + +*Released 6.0.2* + +- Avoid confusion with user's ``urllib3`` by renaming vendored ``urllib3`` to ``ptb_urllib3`` + +**2017-05-19** + +*Released 6.0.1* + +- Add support for ``User.language_code`` +- Fix ``Message.text_html`` and ``Message.text_markdown`` for messages with emoji + +**2017-05-19** + +*Released 6.0.0* + +- Add support for Bot API 2.3.1 +- Add support for ``deleteMessage`` API method +- New, simpler API for ``JobQueue`` - :pr:`484` +- Download files into file-like objects - :pr:`459` +- Use vendor ``urllib3`` to address issues with timeouts + - The default timeout for messages is now 5 seconds. For sending media, the default timeout is now 20 seconds. +- String attributes that are not set are now ``None`` by default, instead of empty strings +- Add ``text_markdown`` and ``text_html`` properties to ``Message`` - :pr:`507` +- Add support for Socks5 proxy - :pr:`518` +- Add support for filters in ``CommandHandler`` - :pr:`536` +- Add the ability to invert (not) filters - :pr:`552` +- Add ``Filters.group`` and ``Filters.private`` +- Compatibility with GAE via ``urllib3.contrib`` package - :pr:`583` +- Add equality rich comparision operators to telegram objects - :pr:`604` +- Several bugfixes and other improvements +- Remove some deprecated code + +**2017-04-17** + +*Released 5.3.1* + +- Hotfix release due to bug introduced by urllib3 version 1.21 + +**2016-12-11** + +*Released 5.3* + +- Implement API changes of November 21st (Bot API 2.3) +- ``JobQueue`` now supports ``datetime.timedelta`` in addition to seconds +- ``JobQueue`` now supports running jobs only on certain days +- New ``Filters.reply`` filter +- Bugfix for ``Message.edit_reply_markup`` +- Other bugfixes + +**2016-10-25** + +*Released 5.2* + +- Implement API changes of October 3rd (games update) +- Add ``Message.edit_*`` methods +- Filters for the ``MessageHandler`` can now be combined using bitwise operators (``& and |``) +- Add a way to save user- and chat-related data temporarily +- Other bugfixes and improvements + +**2016-09-24** + +*Released 5.1* + +- Drop Python 2.6 support +- Deprecate ``telegram.Emoji`` + +- Use ``ujson`` if available +- Add instance methods to ``Message``, ``Chat``, ``User``, ``InlineQuery`` and ``CallbackQuery`` +- RegEx filtering for ``CallbackQueryHandler`` and ``InlineQueryHandler`` +- New ``MessageHandler`` filters: ``forwarded`` and ``entity`` +- Add ``Message.get_entity`` to correctly handle UTF-16 codepoints and ``MessageEntity`` offsets +- Fix bug in ``ConversationHandler`` when first handler ends the conversation +- Allow multiple ``Dispatcher`` instances +- Add ``ChatMigrated`` Exception +- Properly split and handle arguments in ``CommandHandler`` + +**2016-07-15** + +*Released 5.0* + +- Rework ``JobQueue`` +- Introduce ``ConversationHandler`` +- Introduce ``telegram.constants`` - :pr:`342` + +**2016-07-12** + +*Released 4.3.4* + +- Fix proxy support with ``urllib3`` when proxy requires auth + +**2016-07-08** + +*Released 4.3.3* + +- Fix proxy support with ``urllib3`` + +**2016-07-04** + +*Released 4.3.2* + +- Fix: Use ``timeout`` parameter in all API methods + +**2016-06-29** + +*Released 4.3.1* + +- Update wrong requirement: ``urllib3>=1.10`` + +**2016-06-28** + +*Released 4.3* + +- Use ``urllib3.PoolManager`` for connection re-use +- Rewrite ``run_async`` decorator to re-use threads +- New requirements: ``urllib3`` and ``certifi`` + +**2016-06-10** + +*Released 4.2.1* + +- Fix ``CallbackQuery.to_dict()`` bug (thanks to @jlmadurga) +- Fix ``editMessageText`` exception when receiving a ``CallbackQuery`` + +**2016-05-28** + +*Released 4.2* + +- Implement Bot API 2.1 +- Move ``botan`` module to ``telegram.contrib`` +- New exception type: ``BadRequest`` + +**2016-05-22** + +*Released 4.1.2* + +- Fix ``MessageEntity`` decoding with Bot API 2.1 changes + +**2016-05-16** + +*Released 4.1.1* + +- Fix deprecation warning in ``Dispatcher`` + +**2016-05-15** + +*Released 4.1* + +- Implement API changes from May 6, 2016 +- Fix bug when ``start_polling`` with ``clean=True`` +- Methods now have snake_case equivalent, for example ``telegram.Bot.send_message`` is the same as ``telegram.Bot.sendMessage`` + +**2016-05-01** + +*Released 4.0.3* + +- Add missing attribute ``location`` to ``InlineQuery`` + +**2016-04-29** + +*Released 4.0.2* + +- Bugfixes +- ``KeyboardReplyMarkup`` now accepts ``str`` again + +**2016-04-27** + +*Released 4.0.1* + +- Implement Bot API 2.0 +- Almost complete recode of ``Dispatcher`` +- Please read the `Transition Guide to 4.0 `_ +- **Changes from 4.0rc1** + - The syntax of filters for ``MessageHandler`` (upper/lower cases) + - Handler groups are now identified by ``int`` only, and ordered +- **Note:** v4.0 has been skipped due to a PyPI accident + +**2016-04-22** + +*Released 4.0rc1* + +- Implement Bot API 2.0 +- Almost complete recode of ``Dispatcher`` +- Please read the `Transistion Guide to 4.0 `_ + +**2016-03-22** + +*Released 3.4* + +- Move ``Updater``, ``Dispatcher`` and ``JobQueue`` to new ``telegram.ext`` submodule (thanks to @rahiel) +- Add ``disable_notification`` parameter (thanks to @aidarbiktimirov) +- Fix bug where commands sent by Telegram Web would not be recognized (thanks to @shelomentsevd) +- Add option to skip old updates on bot startup +- Send files from ``BufferedReader`` + +**2016-02-28** + +*Released 3.3* + +- Inline bots +- Send any file by URL +- Specialized exceptions: ``Unauthorized``, ``InvalidToken``, ``NetworkError`` and ``TimedOut`` +- Integration for botan.io (thanks to @ollmer) +- HTML Parsemode (thanks to @jlmadurga) +- Bugfixes and under-the-hood improvements + +**Very special thanks to Noam Meltzer (@tsnoam) for all of his work!** + +**2016-01-09** + +*Released 3.3b1* + +- Implement inline bots (beta) + +**2016-01-05** + +*Released 3.2.0* + +- Introducing ``JobQueue`` (original author: @franciscod) +- Streamlining all exceptions to ``TelegramError`` (Special thanks to @tsnoam) +- Proper locking of ``Updater`` and ``Dispatcher`` ``start`` and ``stop`` methods +- Small bugfixes + +**2015-12-29** + +*Released 3.1.2* + +- Fix custom path for file downloads +- Don't stop the dispatcher thread on uncaught errors in handlers + +**2015-12-21** + +*Released 3.1.1* + +- Fix a bug where asynchronous handlers could not have additional arguments +- Add ``groups`` and ``groupdict`` as additional arguments for regex-based handlers + +**2015-12-16** + +*Released 3.1.0* + +- The ``chat``-field in ``Message`` is now of type ``Chat``. (API update Oct 8 2015) +- ``Message`` now contains the optional fields ``supergroup_chat_created``, ``migrate_to_chat_id``, ``migrate_from_chat_id`` and ``channel_chat_created``. (API update Nov 2015) + +**2015-12-08** + +*Released 3.0.0* + +- Introducing the ``Updater`` and ``Dispatcher`` classes + +**2015-11-11** + +*Released 2.9.2* + +- Error handling on request timeouts has been improved + +**2015-11-10** + +*Released 2.9.1* + +- Add parameter ``network_delay`` to Bot.getUpdates for slow connections + +**2015-11-10** + +*Released 2.9* + +- Emoji class now uses ``bytes_to_native_str`` from ``future`` 3rd party lib +- Make ``user_from`` optional to work with channels +- Raise exception if Telegram times out on long-polling + +*Special thanks to @jh0ker for all hard work* + +**2015-10-08** + +*Released 2.8.7* + +- Type as optional for ``GroupChat`` class + +**2015-10-08** + +*Released 2.8.6* + +- Adds type to ``User`` and ``GroupChat`` classes (pre-release Telegram feature) + +**2015-09-24** + +*Released 2.8.5* + +- Handles HTTP Bad Gateway (503) errors on request +- Fixes regression on ``Audio`` and ``Document`` for unicode fields + +**2015-09-20** + +*Released 2.8.4* + +- ``getFile`` and ``File.download`` is now fully supported + +**2015-09-10** + +*Released 2.8.3* + +- Moved ``Bot._requestURL`` to its own class (``telegram.utils.request``) +- Much better, such wow, Telegram Objects tests +- Add consistency for ``str`` properties on Telegram Objects +- Better design to test if ``chat_id`` is invalid +- Add ability to set custom filename on ``Bot.sendDocument(..,filename='')`` +- Fix Sticker as ``InputFile`` +- Send JSON requests over urlencoded post data +- Markdown support for ``Bot.sendMessage(..., parse_mode=ParseMode.MARKDOWN)`` +- Refactor of ``TelegramError`` class (no more handling ``IOError`` or ``URLError``) + +**2015-09-05** + +*Released 2.8.2* + +- Fix regression on Telegram ReplyMarkup +- Add certificate to ``is_inputfile`` method + +**2015-09-05** + +*Released 2.8.1* + +- Fix regression on Telegram objects with thumb properties + +**2015-09-04** + +*Released 2.8* + +- TelegramError when ``chat_id`` is empty for send* methods +- ``setWebhook`` now supports sending self-signed certificate +- Huge redesign of existing Telegram classes +- Added support for PyPy +- Added docstring for existing classes + +**2015-08-19** + +*Released 2.7.1* + +- Fixed JSON serialization for ``message`` + +**2015-08-17** + +*Released 2.7* + +- Added support for ``Voice`` object and ``sendVoice`` method +- Due backward compatibility performer or/and title will be required for ``sendAudio`` +- Fixed JSON serialization when forwarded message + +**2015-08-15** + +*Released 2.6.1* + +- Fixed parsing image header issue on < Python 2.7.3 + +**2015-08-14** + +*Released 2.6.0* + +- Depreciation of ``require_authentication`` and ``clearCredentials`` methods +- Giving ``AUTHORS`` the proper credits for their contribution for this project +- ``Message.date`` and ``Message.forward_date`` are now ``datetime`` objects + +**2015-08-12** + +*Released 2.5.3* + +- ``telegram.Bot`` now supports to be unpickled + +**2015-08-11** + +*Released 2.5.2* + +- New changes from Telegram Bot API have been applied +- ``telegram.Bot`` now supports to be pickled +- Return empty ``str`` instead ``None`` when ``message.text`` is empty + +**2015-08-10** + +*Released 2.5.1* + +- Moved from GPLv2 to LGPLv3 + +**2015-08-09** + +*Released 2.5* + +- Fixes logging calls in API + +**2015-08-08** + +*Released 2.4* + +- Fixes ``Emoji`` class for Python 3 +- ``PEP8`` improvements + +**2015-08-08** + +*Released 2.3* + +- Fixes ``ForceReply`` class +- Remove ``logging.basicConfig`` from library + +**2015-07-25** + +*Released 2.2* + +- Allows ``debug=True`` when initializing ``telegram.Bot`` + +**2015-07-20** + +*Released 2.1* + +- Fix ``to_dict`` for ``Document`` and ``Video`` + +**2015-07-19** + +*Released 2.0* + +- Fixes bugs +- Improves ``__str__`` over ``to_json()`` +- Creates abstract class ``TelegramObject`` + +**2015-07-15** + +*Released 1.9* + +- Python 3 officially supported +- ``PEP8`` improvements + +**2015-07-12** + +*Released 1.8* + +- Fixes crash when replying an unicode text message (special thanks to JRoot3D) + +**2015-07-11** + +*Released 1.7* + +- Fixes crash when ``username`` is not defined on ``chat`` (special thanks to JRoot3D) + +**2015-07-10** + +*Released 1.6* + +- Improvements for GAE support + +**2015-07-10** + +*Released 1.5* + +- Fixes randomly unicode issues when using ``InputFile`` + +**2015-07-10** + +*Released 1.4* + +- ``requests`` lib is no longer required +- Google App Engine (GAE) is supported + +**2015-07-10** + +*Released 1.3* + +- Added support to ``setWebhook`` (special thanks to macrojames) + +**2015-07-09** + +*Released 1.2* + +- ``CustomKeyboard`` classes now available +- Emojis available +- ``PEP8`` improvements + +**2015-07-08** + +*Released 1.1* + +- PyPi package now available + +**2015-07-08** + +*Released 1.0* + +- Initial checkin of python-telegram-bot diff --git a/CODE_OF_CONDUCT.rst b/CODE_OF_CONDUCT.rst new file mode 100644 index 0000000000000000000000000000000000000000..5a1d6d26fad641e62d35922a44d3c81bdf302832 --- /dev/null +++ b/CODE_OF_CONDUCT.rst @@ -0,0 +1,52 @@ +==================================== +Contributor Covenant Code of Conduct +==================================== + +Our Pledge +========== + +In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. + +Our Standards +============= + +Examples of behavior that contributes to creating a positive environment include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or advances +* Publication of any content supporting, justifying or otherwise affiliating with terror and/or hate towards others +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a professional setting + +Our Responsibilities +==================== + +Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. + +Scope +===== + +This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. + +Enforcement +=========== + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at devs@python-telegram-bot.org. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. + +Attribution +=========== + +This Code of Conduct is adapted from the `Contributor Covenant `_, version 1.4, available at `https://www.contributor-covenant.org/version/1/4 `_. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..ae6b8d06be3c052fbf2f40d2a55fd8585d87ddd5 --- /dev/null +++ b/LICENSE @@ -0,0 +1,619 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. [http://fsf.org/] + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. diff --git a/LICENSE.dual b/LICENSE.dual new file mode 100644 index 0000000000000000000000000000000000000000..c2730fdd406582213e828b4a7b1ed33c8b62ee24 --- /dev/null +++ b/LICENSE.dual @@ -0,0 +1,792 @@ + NOTICE: You can find here the GPLv3 license and after the Lesser GPLv3 license. +You may choose either license. + + + GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. [http://fsf.org/] + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. + + + + + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. [http://fsf.org/] + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. diff --git a/LICENSE.lesser b/LICENSE.lesser new file mode 100644 index 0000000000000000000000000000000000000000..5cc63c20b453fb272056d6ce14398a593d303a90 --- /dev/null +++ b/LICENSE.lesser @@ -0,0 +1,165 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. [http://fsf.org/] + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. diff --git a/README.md b/README.md index c66486aecc0752f318df9f917a2a5d1d15988f2a..0ea5eb3adefdbbefd7f6befa3a39741c248f5127 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,7 @@ ---- -title: Santim -emoji: 🚀 -colorFrom: blue -colorTo: green -sdk: docker -pinned: false -hf_oauth: true -hf_oauth_expiration_minutes: 36000 -hf_oauth_scopes: -- read-repos -- write-repos -- manage-repos -- inference-api -- read-billing -tags: -- autotrain ---- +# Documentation tests -Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference +This directory contains tests that cover our scripting logic for automatically generating +additional elements for the documentation pages. + +These tests are not meant to be run with the general test suite, so the modules do not have any +`test_` prefix in their names. By default, `pytest` ignores files that have no such prefix. diff --git a/README.rst b/README.rst new file mode 100644 index 0000000000000000000000000000000000000000..69591953bc346d23f03e2b29a7efee6229cf4b2c --- /dev/null +++ b/README.rst @@ -0,0 +1,111 @@ +============== +Testing in PTB +============== + +PTB uses `pytest`_ for testing. To run the tests, you need to +have pytest installed along with a few other dependencies. You can find the list of dependencies +in the ``pyproject.toml`` file in the root of the repository. + +Running tests +============= + +To run the entire test suite, you can use the following command: + +.. code-block:: bash + + $ pytest + +This will run all the tests, including the ones which make a request to the Telegram servers, which +may take a long time (total > 13 mins). To run only the tests that don't require a connection, you +can run the following command: + +.. code-block:: bash + + $ pytest -m no_req + +Or alternatively, you can run the following command to run only the tests that require a connection: + +.. code-block:: bash + + $ pytest -m req + +To further speed up the tests, you can run them in parallel using the ``-n`` flag (requires `pytest-xdist`_). But beware that +this will use multiple CPU cores on your machine. The ``--dist`` flag is used to specify how the +tests will be distributed across the cores. The ``loadgroup`` option is used to distribute the tests +such that tests marked with ``@pytest.mark.xdist_group("name")`` are run on the same core — important if you want avoid race conditions in some tests: + +.. code-block:: bash + + $ pytest -n auto --dist=loadgroup + +This will result in a significant speedup, but may cause some tests to fail. If you want to run +the failed tests in isolation, you can use the ``--lf`` flag: + +.. code-block:: bash + + $ pytest --lf + + +Writing tests +============= + +PTB has a separate test file for every file in the ``telegram.*`` namespace. Further, the tests for +the ``telegram`` module are split into two classes, based on whether the test methods in them make a +request or not. When writing tests, make sure to split them into these two classes, and make sure +to name the test class as: ``TestXXXWithoutRequest`` for tests that don't make a request, and ``TestXXXWithRequest`` for tests that do. + +Writing tests is a creative process; allowing you to design your test however you'd like, but there +are a few conventions that you should follow: + +- Each new test class needs a ``test_slot_behaviour``, ``test_to_dict``, ``test_de_json`` and + ``test_equality`` (in most cases). + +- Make use of pytest's fixtures and parametrize wherever possible. Having knowledge of pytest's + tooling can help you as well. You can look at the existing tests for examples and inspiration. + +- New fixtures should go into ``conftest.py``. New auxiliary functions and classes, used either directly in the tests or in the fixtures, should go into the ``tests/auxil`` directory. + +If you have made some API changes, you may want to run ``test_official`` to validate that the changes are +complete and correct. To run it, export an environment variable first: + +.. code-block:: bash + + $ export TEST_OFFICIAL=true + +and then run ``pytest tests/test_official.py``. Note: You need py 3.10+ to run this test. + +We also have another marker, ``@pytest.mark.dev``, which you can add to tests that you want to run selectively. +Use as follows: + +.. code-block:: bash + + $ pytest -m dev + + +Debugging tests +=============== + +Writing tests can be challenging, and fixing failing tests can be even more so. To help with this, +PTB has started to adopt the use of ``logging`` in the test suite. You can insert debug logging +statements in your tests to help you understand what's going on. To enable these logs, you can set +``log_level = DEBUG`` in ``setup.cfg`` or use the ``--log-level=INFO`` flag when running the tests. +If a test is large and complicated, it is recommended to leave the debug logs for others to use as +well. + + +Bots used in tests +================== + +If you run the tests locally, the test setup will use one of the two public bots available. Which +bot of the two gets chosen for the test session is random. Whereas when the tests on the +Github Actions CI are run, the test setup allocates a different, but same bot is for every combination of Python version and +OS. The operating systems and Python versions the CI runs the tests on can be viewed in the `corresponding workflow`_. + + +That's it! If you have any questions, feel free to ask them in the `PTB dev +group`_. + +.. _pytest: https://docs.pytest.org/en/stable/ +.. _pytest-xdist: https://pypi.org/project/pytest-xdist/ +.. _PTB dev group: https://t.me/pythontelegrambotgroup +.. _corresponding workflow: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/.github/workflows/unit_tests.yml diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/__main__.py b/__main__.py new file mode 100644 index 0000000000000000000000000000000000000000..6a508e3574b14e7809ed887e7e884c54da35a6bf --- /dev/null +++ b/__main__.py @@ -0,0 +1,54 @@ +# !/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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/]. +# pylint: disable=missing-module-docstring +# ruff: noqa: T201, D100, S603, S607 +import subprocess +import sys +from typing import Optional + +from . import __version__ as telegram_ver +from .constants import BOT_API_VERSION + + +def _git_revision() -> Optional[str]: + try: + output = subprocess.check_output( + ["git", "describe", "--long", "--tags"], stderr=subprocess.STDOUT + ) + except (subprocess.SubprocessError, OSError): + return None + return output.decode().strip() + + +def print_ver_info() -> None: + """Prints version information for python-telegram-bot, the Bot API and Python.""" + git_revision = _git_revision() + print(f"python-telegram-bot {telegram_ver}" + (f" ({git_revision})" if git_revision else "")) + print(f"Bot API {BOT_API_VERSION}") + sys_version = sys.version.replace("\n", " ") + print(f"Python {sys_version}") + + +def main() -> None: + """Prints version information for python-telegram-bot, the Bot API and Python.""" + print_ver_info() + + +if __name__ == "__main__": + main() diff --git a/_birthdate.py b/_birthdate.py new file mode 100644 index 0000000000000000000000000000000000000000..06caf67d5ec96a0163c2d8c086fc0f9875366d4c --- /dev/null +++ b/_birthdate.py @@ -0,0 +1,92 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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 Birthday.""" +from datetime import date +from typing import Optional + +from telegram._telegramobject import TelegramObject +from telegram._utils.types import JSONDict + + +class Birthdate(TelegramObject): + """ + This object describes the birthdate of a user. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`day`, and :attr:`month` are equal. + + .. versionadded:: 21.1 + + Args: + day (:obj:`int`): Day of the user's birth; 1-31. + month (:obj:`int`): Month of the user's birth; 1-12. + year (:obj:`int`, optional): Year of the user's birth. + + Attributes: + day (:obj:`int`): Day of the user's birth; 1-31. + month (:obj:`int`): Month of the user's birth; 1-12. + year (:obj:`int`): Optional. Year of the user's birth. + + """ + + __slots__ = ("day", "month", "year") + + def __init__( + self, + day: int, + month: int, + year: Optional[int] = None, + *, + api_kwargs: Optional[JSONDict] = None, + ): + super().__init__(api_kwargs=api_kwargs) + + # Required + self.day: int = day + self.month: int = month + # Optional + self.year: Optional[int] = year + + self._id_attrs = ( + self.day, + self.month, + ) + + self._freeze() + + def to_date(self, year: Optional[int] = None) -> date: + """Return the birthdate as a date object. + + .. versionchanged:: 21.2 + Now returns a :obj:`datetime.date` object instead of a :obj:`datetime.datetime` object, + as was originally intended. + + Args: + year (:obj:`int`, optional): The year to use. Required, if the :attr:`year` was not + present. + + Returns: + :obj:`datetime.date`: The birthdate as a date object. + """ + if self.year is None and year is None: + raise ValueError( + "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] diff --git a/_bot.py b/_bot.py new file mode 100644 index 0000000000000000000000000000000000000000..b79df08ff1738e63f4e92e2f1fbdc64570490fc9 --- /dev/null +++ b/_bot.py @@ -0,0 +1,9649 @@ +#!/usr/bin/env python +# pylint: disable=too-many-arguments +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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 Bot.""" + +import asyncio +import contextlib +import copy +import pickle +from datetime import datetime +from types import TracebackType +from typing import ( + TYPE_CHECKING, + Any, + AsyncContextManager, + Callable, + Dict, + List, + NoReturn, + Optional, + Sequence, + Set, + Tuple, + Type, + TypeVar, + Union, + cast, + no_type_check, +) + +try: + from cryptography.hazmat.backends import default_backend + from cryptography.hazmat.primitives import serialization + + CRYPTO_INSTALLED = True +except ImportError: + default_backend = None # type: ignore[assignment] + serialization = None # type: ignore[assignment] + CRYPTO_INSTALLED = False + +from telegram._botcommand import BotCommand +from telegram._botcommandscope import BotCommandScope +from telegram._botdescription import BotDescription, BotShortDescription +from telegram._botname import BotName +from telegram._business import BusinessConnection +from telegram._chatadministratorrights import ChatAdministratorRights +from telegram._chatboost import UserChatBoosts +from telegram._chatfullinfo import ChatFullInfo +from telegram._chatinvitelink import ChatInviteLink +from telegram._chatmember import ChatMember +from telegram._chatpermissions import ChatPermissions +from telegram._files.animation import Animation +from telegram._files.audio import Audio +from telegram._files.chatphoto import ChatPhoto +from telegram._files.contact import Contact +from telegram._files.document import Document +from telegram._files.file import File +from telegram._files.inputmedia import InputMedia, InputPaidMedia +from telegram._files.location import Location +from telegram._files.photosize import PhotoSize +from telegram._files.sticker import MaskPosition, Sticker, StickerSet +from telegram._files.venue import Venue +from telegram._files.video import Video +from telegram._files.videonote import VideoNote +from telegram._files.voice import Voice +from telegram._forumtopic import ForumTopic +from telegram._games.gamehighscore import GameHighScore +from telegram._inline.inlinequeryresultsbutton import InlineQueryResultsButton +from telegram._menubutton import MenuButton +from telegram._message import Message +from telegram._messageid import MessageId +from telegram._payment.stars import StarTransactions +from telegram._poll import InputPollOption, Poll +from telegram._reaction import ReactionType, ReactionTypeCustomEmoji, ReactionTypeEmoji +from telegram._reply import ReplyParameters +from telegram._sentwebappmessage import SentWebAppMessage +from telegram._telegramobject import TelegramObject +from telegram._update import Update +from telegram._user import User +from telegram._userprofilephotos import UserProfilePhotos +from telegram._utils.argumentparsing import parse_lpo_and_dwpp, parse_sequence_arg +from telegram._utils.defaultvalue import DEFAULT_NONE, DefaultValue +from telegram._utils.files import is_local_file, parse_file_input +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.warnings import warn +from telegram._webhookinfo import WebhookInfo +from telegram.constants import InlineQueryLimit, ReactionEmoji +from telegram.error import EndPointNotFound, InvalidToken +from telegram.request import BaseRequest, RequestData +from telegram.request._httpxrequest import HTTPXRequest +from telegram.request._requestparameter import RequestParameter +from telegram.warnings import PTBDeprecationWarning, PTBUserWarning + +if TYPE_CHECKING: + from telegram import ( + InlineKeyboardMarkup, + InlineQueryResult, + InputFile, + InputMediaAudio, + InputMediaDocument, + InputMediaPhoto, + InputMediaVideo, + InputSticker, + LabeledPrice, + LinkPreviewOptions, + MessageEntity, + PassportElementError, + ShippingOption, + ) + +BT = TypeVar("BT", bound="Bot") + + +class Bot(TelegramObject, AsyncContextManager["Bot"]): + """This object represents a Telegram Bot. + + Instances of this class can be used as asyncio context managers, where + + .. code:: python + + async with bot: + # code + + is roughly equivalent to + + .. code:: python + + try: + await bot.initialize() + # code + finally: + await bot.shutdown() + + .. seealso:: :meth:`__aenter__` and :meth:`__aexit__`. + + Note: + * Most bot methods have the argument ``api_kwargs`` which allows passing arbitrary keywords + to the Telegram API. This can be used to access new features of the API before they are + incorporated into PTB. The limitations to this argument are the same as the ones + described in :meth:`do_api_request`. + * Bots should not be serialized since if you for e.g. change the bots token, then your + serialized instance will not reflect that change. Trying to pickle a bot instance will + raise :exc:`pickle.PicklingError`. Trying to deepcopy a bot instance will raise + :exc:`TypeError`. + + Examples: + :any:`Raw API Bot ` + + .. seealso:: :wiki:`Your First Bot `, + :wiki:`Builder Pattern ` + + .. versionadded:: 13.2 + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`bot` is equal. + + .. versionchanged:: 20.0 + + * Removed the deprecated methods ``kick_chat_member``, ``kickChatMember``, + ``get_chat_members_count`` and ``getChatMembersCount``. + * Removed the deprecated property ``commands``. + * Removed the deprecated ``defaults`` parameter. If you want to use + :class:`telegram.ext.Defaults`, please use the subclass :class:`telegram.ext.ExtBot` + instead. + * Attempting to pickle a bot instance will now raise :exc:`pickle.PicklingError`. + * Attempting to deepcopy a bot instance will now raise :exc:`TypeError`. + * The following are now keyword-only arguments in Bot methods: + ``location``, ``filename``, ``venue``, ``contact``, + ``{read, write, connect, pool}_timeout``, ``api_kwargs``. Use a named argument for those, + and notice that some positional arguments changed position as a result. + * For uploading files, file paths are now always accepted. If :paramref:`local_mode` is + :obj:`False`, the file contents will be read in binary mode and uploaded. Otherwise, + the file path will be passed in the + `file URI scheme `_. + + .. versionchanged:: 20.5 + Removed deprecated methods ``set_sticker_set_thumb`` and ``setStickerSetThumb``. + Use :meth:`set_sticker_set_thumbnail` and :meth:`setStickerSetThumbnail` instead. + + Args: + token (:obj:`str`): Bot's unique authentication token. + base_url (:obj:`str`, optional): Telegram Bot API service URL. + base_file_url (:obj:`str`, optional): Telegram Bot API file URL. + 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 + :class:`telegram.request.HTTPXRequest` will be used. + get_updates_request (:class:`telegram.request.BaseRequest`, optional): Pre initialized + :class:`telegram.request.BaseRequest` instances. Will be used exclusively for + :meth:`get_updates`. If not passed, an instance of + :class:`telegram.request.HTTPXRequest` will be used. + private_key (:obj:`bytes`, optional): Private key for decryption of telegram passport data. + private_key_password (:obj:`bytes`, optional): Password for above private key. + local_mode (:obj:`bool`, optional): Set to :obj:`True`, if the :paramref:`base_url` is + the URI of a `Local Bot API Server `_ that runs with the ``--local`` flag. Currently, the only effect of + this is that files are uploaded using their local path in the + `file URI scheme `_. + Defaults to :obj:`False`. + + .. versionadded:: 20.0. + + .. include:: inclusions/bot_methods.rst + + .. |removed_thumb_arg| replace:: Removed deprecated argument ``thumb``. Use + ``thumbnail`` instead. + + """ + + # This is a class variable since we want to override the logger name in ExtBot + # without having to change all places where this is used + _LOGGER = get_logger(__name__) + + __slots__ = ( + "_base_file_url", + "_base_url", + "_bot_user", + "_initialized", + "_local_mode", + "_private_key", + "_request", + "_token", + ) + + def __init__( + self, + token: str, + base_url: str = "https://api.telegram.org/bot", + base_file_url: str = "https://api.telegram.org/file/bot", + request: Optional[BaseRequest] = None, + get_updates_request: Optional[BaseRequest] = None, + private_key: Optional[bytes] = None, + private_key_password: Optional[bytes] = None, + local_mode: bool = False, + ): + super().__init__(api_kwargs=None) + if not token: + 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._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] = ( + 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 + # 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. + + warning_string = "" + + if ( + isinstance(self._request[0], HTTPXRequest) + and self._request[0].http_version == "2" + and not 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") + ): + if warning_string: + warning_string += " and request" + else: + warning_string = "request" + + if warning_string: + self._warn( + f"You set the HTTP version for the {warning_string} HTTPXRequest instance to " + "HTTP/2. The self hosted bot api instances only support HTTP/1.1. You should " + "either run a HTTP proxy in front of it which supports HTTP/2 or use HTTP/1.1.", + PTBUserWarning, + stacklevel=2, + ) + + if private_key: + if not CRYPTO_INSTALLED: + raise RuntimeError( + "To use Telegram Passports, PTB must be installed via `pip install " + '"python-telegram-bot[passport]"`.' + ) + self._private_key = serialization.load_pem_private_key( + private_key, password=private_key_password, backend=default_backend() + ) + + self._freeze() + + async def __aenter__(self: BT) -> BT: + """ + |async_context_manager| :meth:`initializes ` the Bot. + + Returns: + The initialized Bot instance. + + Raises: + :exc:`Exception`: If an exception is raised during initialization, :meth:`shutdown` + is called in this case. + """ + try: + await self.initialize() + except Exception: + await self.shutdown() + raise + return self + + async def __aexit__( + self, + exc_type: Optional[Type[BaseException]], + exc_val: Optional[BaseException], + exc_tb: Optional[TracebackType], + ) -> None: + """|async_context_manager| :meth:`shuts down ` the Bot.""" + # Make sure not to return `True` so that exceptions are not suppressed + # https://docs.python.org/3/reference/datamodel.html?#object.__aexit__ + await self.shutdown() + + def __reduce__(self) -> NoReturn: + """Customizes how :func:`copy.deepcopy` processes objects of this type. Bots can not + be pickled and this method will always raise an exception. + + .. versionadded:: 20.0 + + Raises: + :exc:`pickle.PicklingError` + """ + raise pickle.PicklingError("Bot objects cannot be pickled!") + + 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. + + .. versionadded:: 20.0 + + Raises: + :exc:`TypeError` + """ + raise TypeError("Bot objects cannot be deepcopied!") + + def __eq__(self, other: object) -> bool: + """Defines equality condition for the :class:`telegram.Bot` object. + Two objects of this class are considered to be equal if their attributes + :attr:`bot` are equal. + + Returns: + :obj:`True` if both attributes :attr:`bot` are equal. :obj:`False` otherwise. + """ + if isinstance(other, Bot): + return self.bot == other.bot + return super().__eq__(other) + + def __hash__(self) -> int: + """See :meth:`telegram.TelegramObject.__hash__`""" + if self._bot_user is None: + return super().__hash__() + return hash((self.bot, Bot)) + + def __repr__(self) -> str: + """Give a string representation of the bot in the form ``Bot[token=...]``. + + As this class doesn't implement :meth:`object.__str__`, the default implementation + will be used, which is equivalent to :meth:`__repr__`. + + Returns: + :obj:`str` + """ + return build_repr_with_selected_attrs(self, token=self.token) + + @property + def token(self) -> str: + """:obj:`str`: Bot's unique authentication token. + + .. versionadded:: 20.0 + """ + return self._token + + @property + def base_url(self) -> str: + """:obj:`str`: Telegram Bot API service URL, built from :paramref:`Bot.base_url` and + :paramref:`Bot.token`. + + .. versionadded:: 20.0 + """ + return self._base_url + + @property + def base_file_url(self) -> str: + """:obj:`str`: Telegram Bot API file URL, built from :paramref:`Bot.base_file_url` and + :paramref:`Bot.token`. + + .. versionadded:: 20.0 + """ + return self._base_file_url + + @property + def local_mode(self) -> bool: + """:obj:`bool`: Whether this bot is running in local mode. + + .. versionadded:: 20.0 + """ + return self._local_mode + + # Proper type hints are difficult because: + # 1. cryptography doesn't have a nice base class, so it would get lengthy + # 2. we can't import cryptography if it's not installed + @property + def private_key(self) -> Optional[Any]: + """Deserialized private key for decryption of telegram passport data. + + .. versionadded:: 20.0 + """ + return self._private_key + + @property + def request(self) -> BaseRequest: + """The :class:`~telegram.request.BaseRequest` object used by this bot. + + Warning: + Requests to the Bot API are made by the various methods of this class. This attribute + should *not* be used manually. + """ + return self._request[1] + + @property + def bot(self) -> User: + """:class:`telegram.User`: User instance for the bot as returned by :meth:`get_me`. + + Warning: + This value is the cached return value of :meth:`get_me`. If the bots profile is + changed during runtime, this value won't reflect the changes until :meth:`get_me` is + called again. + + .. seealso:: :meth:`initialize` + """ + if self._bot_user is None: + raise RuntimeError( + f"{self.__class__.__name__} is not properly initialized. Call " + f"`{self.__class__.__name__}.initialize` before accessing this property." + ) + return self._bot_user + + @property + def id(self) -> int: + """:obj:`int`: Unique identifier for this bot. Shortcut for the corresponding attribute of + :attr:`bot`. + """ + return self.bot.id + + @property + def first_name(self) -> str: + """:obj:`str`: Bot's first name. Shortcut for the corresponding attribute of + :attr:`bot`. + """ + return self.bot.first_name + + @property + def last_name(self) -> str: + """:obj:`str`: Optional. Bot's last name. Shortcut for the corresponding attribute of + :attr:`bot`. + """ + return self.bot.last_name # type: ignore + + @property + def username(self) -> str: + """:obj:`str`: Bot's username. Shortcut for the corresponding attribute of + :attr:`bot`. + """ + return self.bot.username # type: ignore + + @property + def link(self) -> str: + """:obj:`str`: Convenience property. Returns the t.me link of the bot.""" + return f"https://t.me/{self.username}" + + @property + def can_join_groups(self) -> bool: + """:obj:`bool`: Bot's :attr:`telegram.User.can_join_groups` attribute. Shortcut for the + corresponding attribute of :attr:`bot`. + """ + return self.bot.can_join_groups # type: ignore + + @property + def can_read_all_group_messages(self) -> bool: + """:obj:`bool`: Bot's :attr:`telegram.User.can_read_all_group_messages` attribute. + Shortcut for the corresponding attribute of :attr:`bot`. + """ + return self.bot.can_read_all_group_messages # type: ignore + + @property + def supports_inline_queries(self) -> bool: + """:obj:`bool`: Bot's :attr:`telegram.User.supports_inline_queries` attribute. + Shortcut for the corresponding attribute of :attr:`bot`. + """ + return self.bot.supports_inline_queries # type: ignore + + @property + def name(self) -> str: + """:obj:`str`: Bot's @username. Shortcut for the corresponding attribute of :attr:`bot`.""" + return f"@{self.username}" + + @classmethod + def _warn( + cls, + message: Union[str, PTBUserWarning], + category: Type[Warning] = PTBUserWarning, + stacklevel: int = 0, + ) -> None: + """Convenience method to issue a warning. This method is here mostly to make it easier + for ExtBot to add 1 level to all warning calls. + """ + warn(message=message, category=category, stacklevel=stacklevel + 1) + + def _parse_file_input( + self, + file_input: Union[FileInput, "TelegramObject"], + tg_type: Optional[Type["TelegramObject"]] = None, + filename: Optional[str] = None, + attach: bool = False, + ) -> Union[str, "InputFile", Any]: + return parse_file_input( + file_input=file_input, + tg_type=tg_type, + filename=filename, + attach=attach, + local_mode=self._local_mode, + ) + + 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* + be done in ExtBot instead of Bot, shortcuts like `Message.reply_text` need to work for both + Bot and ExtBot, so they also have the `DEFAULT_NONE` default values. + + This makes it necessary to convert `DefaultValue(obj)` to `obj` at some point between + `Message.reply_text` and the request to TG. Doing this here in a centralized manner is a + rather clean and minimally invasive solution, i.e. the link between tg and tg.ext is as + small as possible. + See also _insert_defaults_for_ilq + ExtBot overrides this method to actually insert default values. + + If in the future we come up with a better way of making `Defaults` work, we can cut this + link as well. + """ + # We + # 1) set the correct parse_mode for all InputMedia objects + # 2) replace all DefaultValue instances with the corresponding normal value. + for key, val in data.items(): + # 1) + if isinstance(val, InputMedia): + # Copy object as not to edit it in-place + new = copy.copy(val) + with new._unfrozen(): + new.parse_mode = DefaultValue.get_value(new.parse_mode) + data[key] = new + elif ( + key == "media" + and isinstance(val, Sequence) + and not isinstance(val[0], InputPaidMedia) + ): + # Copy objects as not to edit them in-place + copy_list = [copy.copy(media) for media in val] + for media in copy_list: + with media._unfrozen(): + media.parse_mode = DefaultValue.get_value(media.parse_mode) + data[key] = copy_list + # 2) + else: + data[key] = DefaultValue.get_value(val) + + async def _post( + self, + endpoint: str, + data: Optional[JSONDict] = 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, + ) -> Any: + # 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: + data = {} + + if api_kwargs: + data.update(api_kwargs) + + # Insert is in-place, so no return value for data + self._insert_defaults(data) + + # Drop any None values because Telegram doesn't handle them well + data = {key: value for key, value in data.items() if value is not None} + + return await self._do_post( + endpoint=endpoint, + data=data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + ) + + async def _do_post( + self, + endpoint: str, + data: JSONDict, + *, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + ) -> Union[bool, JSONDict, 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 + request_data = RequestData( + parameters=[RequestParameter.from_input(key, value) for key, value in data.items()], + ) + + request = self._request[0] if endpoint == "getUpdates" else self._request[1] + + self._LOGGER.debug("Calling Bot API endpoint `%s` with parameters `%s`", endpoint, data) + result = await request.post( + url=f"{self._base_url}/{endpoint}", + request_data=request_data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + ) + self._LOGGER.debug( + "Call to Bot API endpoint `%s` finished with return value `%s`", endpoint, result + ) + + return result + + async def _send_message( + self, + endpoint: str, + data: JSONDict, + disable_notification: ODVInput[bool] = DEFAULT_NONE, + reply_markup: Optional[ReplyMarkup] = None, + protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: Optional[int] = None, + caption: Optional[str] = None, + parse_mode: ODVInput[str] = DEFAULT_NONE, + caption_entities: Optional[Sequence["MessageEntity"]] = None, + link_preview_options: ODVInput["LinkPreviewOptions"] = None, + reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, + message_effect_id: Optional[str] = None, + *, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + reply_to_message_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, + ) -> Any: + """Protected method to send or edit messages of any type. + + It is here to reduce repetition of if-else closes in the different bot methods, + i.e. this method takes care of adding its parameters to `data` if appropriate. + + Depending on the bot method, returns either `True` or the message. + However, it's hard to tell mypy which methods expects which of these return values and + using `Any` instead saves us a lot of `type: ignore` comments + """ + # We don't check if (DEFAULT_)None here, so that _post is able to insert the defaults + # correctly, if necessary: + if allow_sending_without_reply is not DEFAULT_NONE and reply_parameters is not None: + raise ValueError( + "`allow_sending_without_reply` and `reply_parameters` are mutually exclusive." + ) + + if reply_to_message_id is not None and reply_parameters is not None: + raise ValueError( + "`reply_to_message_id` and `reply_parameters` are mutually exclusive." + ) + + if reply_to_message_id is not None: + reply_parameters = ReplyParameters( + message_id=reply_to_message_id, + 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 + + result = await self._post( + endpoint, + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + if result is True: + return result + + return Message.de_json(result, self) + + async def initialize(self) -> None: + """Initialize resources used by this class. Currently calls :meth:`get_me` to + cache :attr:`bot` and calls :meth:`telegram.request.BaseRequest.initialize` for + the request objects used by this bot. + + .. seealso:: :meth:`shutdown` + + .. versionadded:: 20.0 + """ + if self._initialized: + self._LOGGER.debug("This Bot is already initialized.") + return + + await asyncio.gather(self._request[0].initialize(), self._request[1].initialize()) + # 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 + :meth:`telegram.request.BaseRequest.shutdown` for the request objects used by this bot. + + .. seealso:: :meth:`initialize` + + .. versionadded:: 20.0 + """ + if not self._initialized: + self._LOGGER.debug("This Bot is already shut down. Returning.") + return + + await asyncio.gather(self._request[0].shutdown(), self._request[1].shutdown()) + self._initialized = False + + async def do_api_request( + self, + endpoint: str, + api_kwargs: Optional[JSONDict] = None, + return_type: Optional[Type[TelegramObject]] = 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, + ) -> Any: + """Do a request to the Telegram API. + + This method is here to make it easier to use new API methods that are not yet supported + by this library. + + Hint: + Since PTB does not know which arguments are passed to this method, some caution is + necessary in terms of PTBs utility functionalities. In particular + + * passing objects of any class defined in the :mod:`telegram` module is supported + * when uploading files, a :class:`telegram.InputFile` must be passed as the value for + the corresponding argument. Passing a file path or file-like object will not work. + File paths will work only in combination with :paramref:`~Bot.local_mode`. + * when uploading files, PTB can still correctly determine that + a special write timeout value should be used instead of the default + :paramref:`telegram.request.HTTPXRequest.write_timeout`. + * insertion of default values specified via :class:`telegram.ext.Defaults` will not + work (only relevant for :class:`telegram.ext.ExtBot`). + * The only exception is :class:`telegram.ext.Defaults.tzinfo`, which will be correctly + applied to :class:`datetime.datetime` objects. + + .. versionadded:: 20.8 + + Args: + endpoint (:obj:`str`): The API endpoint to use, e.g. ``getMe`` or ``get_me``. + api_kwargs (:obj:`dict`, optional): The keyword arguments to pass to the API call. + If not specified, no arguments are passed. + return_type (:class:`telegram.TelegramObject`, optional): If specified, the result of + the API call will be deserialized into an instance of this class or tuple of + instances of this class. If not specified, the raw result of the API call will be + returned. + + Returns: + The result of the API call. If :paramref:`return_type` is not specified, this is a + :obj:`dict` or :obj:`bool`, otherwise an instance of :paramref:`return_type` or a + tuple of :paramref:`return_type`. + + Raises: + :class:`telegram.error.TelegramError` + """ + if hasattr(self, endpoint): + self._warn( + ( + f"Please use 'Bot.{endpoint}' instead of " + f"'Bot.do_api_request(\"{endpoint}\", ...)'" + ), + stacklevel=2, + ) + + camel_case_endpoint = to_camel_case(endpoint) + try: + result = await self._post( + camel_case_endpoint, + api_kwargs=api_kwargs, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + ) + except InvalidToken as exc: + # TG returns 404 Not found for + # 1) malformed tokens + # 2) correct tokens but non-existing method, e.g. api.tg.org/botTOKEN/unkonwnMethod + # 2) is relevant only for Bot.do_api_request, that's why we have special handling for + # that here rather than in BaseRequest._request_wrapper + if self._initialized: + raise EndPointNotFound( + f"Endpoint '{camel_case_endpoint}' not found in Bot API" + ) from exc + + raise InvalidToken( + "Either the bot token was rejected by Telegram or the endpoint " + f"'{camel_case_endpoint}' does not exist." + ) from exc + + if return_type is None or isinstance(result, bool): + return result + + if isinstance(result, list): + return return_type.de_list(result, self) + return return_type.de_json(result, self) + + async def get_me( + 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, + ) -> User: + """A simple method for testing your bot's auth token. Requires no parameters. + + Returns: + :class:`telegram.User`: A :class:`telegram.User` instance representing that bot if the + credentials are valid, :obj:`None` otherwise. + + Raises: + :class:`telegram.error.TelegramError` + + """ + result = await self._post( + "getMe", + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + self._bot_user = User.de_json(result, self) + return self._bot_user # type: ignore[return-value] + + async def send_message( + self, + chat_id: Union[int, str], + text: str, + parse_mode: ODVInput[str] = DEFAULT_NONE, + entities: Optional[Sequence["MessageEntity"]] = None, + disable_notification: ODVInput[bool] = DEFAULT_NONE, + protect_content: ODVInput[bool] = DEFAULT_NONE, + reply_markup: Optional[ReplyMarkup] = None, + message_thread_id: Optional[int] = None, + link_preview_options: ODVInput["LinkPreviewOptions"] = DEFAULT_NONE, + reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, + message_effect_id: Optional[str] = None, + *, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + reply_to_message_id: Optional[int] = None, + disable_web_page_preview: 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, + ) -> Message: + """Use this method to send text messages. + + Args: + chat_id (:obj:`int` | :obj:`str`): |chat_id_channel| + text (:obj:`str`): Text of the message to be sent. Max + :tg-const:`telegram.constants.MessageLimit.MAX_TEXT_LENGTH` characters after + entities parsing. + parse_mode (:obj:`str`): |parse_mode| + entities (Sequence[:class:`telegram.MessageEntity`], optional): Sequence of special + entities that appear in message text, which can be specified instead of + :paramref:`parse_mode`. + + .. versionchanged:: 20.0 + |sequenceargs| + link_preview_options (:obj:`LinkPreviewOptions`, optional): Link preview generation + options for the message. Mutually exclusive with + :paramref:`disable_web_page_preview`. + + .. versionadded:: 20.8 + + disable_notification (:obj:`bool`, optional): |disable_notification| + protect_content (:obj:`bool`, optional): |protect_content| + + .. versionadded:: 13.10 + + reply_markup (:class:`InlineKeyboardMarkup` | :class:`ReplyKeyboardMarkup` | \ + :class:`ReplyKeyboardRemove` | :class:`ForceReply`, optional): + Additional interface options. An object for an inline keyboard, custom reply + keyboard, instructions to remove reply keyboard or to force a reply from the user. + message_thread_id (:obj:`int`, optional): |message_thread_id_arg| + + .. versionadded:: 20.0 + reply_parameters (:class:`telegram.ReplyParameters`, optional): |reply_parameters| + + .. versionadded:: 20.8 + business_connection_id (:obj:`str`, optional): |business_id_str| + + .. versionadded:: 21.1 + message_effect_id (:obj:`str`, optional): |message_effect_id| + + .. versionadded:: 21.3 + + Keyword Args: + allow_sending_without_reply (:obj:`bool`, optional): |allow_sending_without_reply| + Mutually exclusive with :paramref:`reply_parameters`, which this is a convenience + parameter for + + .. versionchanged:: 20.8 + Bot API 7.0 introduced :paramref:`reply_parameters` |rtm_aswr_deprecated| + + .. versionchanged:: 21.0 + |keyword_only_arg| + reply_to_message_id (:obj:`int`, optional): |reply_to_msg_id| + Mutually exclusive with :paramref:`reply_parameters`, which this is a convenience + parameter for + + .. versionchanged:: 20.8 + Bot API 7.0 introduced :paramref:`reply_parameters` |rtm_aswr_deprecated| + + .. versionchanged:: 21.0 + |keyword_only_arg| + disable_web_page_preview (:obj:`bool`, optional): Disables link previews for links in + this message. Convenience parameter for setting :paramref:`link_preview_options`. + Mutually exclusive with :paramref:`link_preview_options`. + + .. versionchanged:: 20.8 + Bot API 7.0 introduced :paramref:`link_preview_options` replacing this + argument. PTB will automatically convert this argument to that one, but + for advanced options, please use :paramref:`link_preview_options` directly. + + .. versionchanged:: 21.0 + |keyword_only_arg| + + Returns: + :class:`telegram.Message`: On success, the sent message is returned. + + Raises: + :exc:`ValueError`: If both :paramref:`disable_web_page_preview` and + :paramref:`link_preview_options` are passed. + :class:`telegram.error.TelegramError`: For other errors. + + """ + data: JSONDict = {"chat_id": chat_id, "text": text, "entities": entities} + link_preview_options = parse_lpo_and_dwpp(disable_web_page_preview, link_preview_options) + + return await self._send_message( + "sendMessage", + data, + reply_to_message_id=reply_to_message_id, + disable_notification=disable_notification, + reply_markup=reply_markup, + allow_sending_without_reply=allow_sending_without_reply, + protect_content=protect_content, + message_thread_id=message_thread_id, + business_connection_id=business_connection_id, + parse_mode=parse_mode, + link_preview_options=link_preview_options, + reply_parameters=reply_parameters, + message_effect_id=message_effect_id, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def delete_message( + self, + chat_id: Union[str, int], + message_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: + """ + Use this method to delete a message, including service messages, with the following + limitations: + + - A message can only be deleted if it was sent less than 48 hours ago. + - Service messages about a supergroup, channel, or forum topic creation can't be deleted. + - A dice message in a private chat can only be deleted if it was sent more than 24 + hours ago. + - Bots can delete outgoing messages in private chats, groups, and supergroups. + - Bots can delete incoming messages in private chats. + - Bots granted :attr:`~telegram.ChatMemberAdministrator.can_post_messages` permissions + can delete outgoing messages in channels. + - If the bot is an administrator of a group, it can delete any message there. + - If the bot has :attr:`~telegram.ChatMemberAdministrator.can_delete_messages` + permission in a supergroup or a channel, it can delete any message there. + + .. + The method CallbackQuery.delete_message() will not be found when automatically + generating "Shortcuts" admonitions for Bot methods because it has no calls + to Bot methods in its return statement(s). So it is manually included in "See also". + + .. seealso:: + :meth:`telegram.CallbackQuery.delete_message` (calls :meth:`delete_message` + indirectly, via :meth:`telegram.Message.delete`) + + Args: + chat_id (:obj:`int` | :obj:`str`): |chat_id_channel| + message_id (:obj:`int`): Identifier of the message to delete. + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + Raises: + :class:`telegram.error.TelegramError` + + """ + data: JSONDict = {"chat_id": chat_id, "message_id": message_id} + return await self._post( + "deleteMessage", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def delete_messages( + self, + chat_id: Union[int, str], + message_ids: Sequence[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: + """ + Use this method to delete multiple messages simultaneously. If some of the specified + messages can't be found, they are skipped. + + .. versionadded:: 20.8 + + Args: + chat_id (:obj:`int` | :obj:`str`): |chat_id_channel| + message_ids (Sequence[:obj:`int`]): A list of + :tg-const:`telegram.constants.BulkRequestLimit.MIN_LIMIT`- + :tg-const:`telegram.constants.BulkRequestLimit.MAX_LIMIT` identifiers of messages + to delete. See :meth:`delete_message` for limitations on which messages can be + deleted. + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + Raises: + :class:`telegram.error.TelegramError` + """ + data: JSONDict = {"chat_id": chat_id, "message_ids": message_ids} + return await self._post( + "deleteMessages", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def forward_message( + self, + chat_id: Union[int, str], + from_chat_id: Union[str, int], + message_id: int, + disable_notification: ODVInput[bool] = DEFAULT_NONE, + protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_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, + ) -> Message: + """ + Use this method to forward messages of any kind. Service messages can't be forwarded. + + Note: + Since the release of Bot API 5.5 it can be impossible to forward messages from + some chats. Use the attributes :attr:`telegram.Message.has_protected_content` and + :attr:`telegram.ChatFullInfo.has_protected_content` to check this. + + As a workaround, it is still possible to use :meth:`copy_message`. However, this + behaviour is undocumented and might be changed by Telegram. + + Args: + chat_id (:obj:`int` | :obj:`str`): |chat_id_channel| + 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 + :paramref:`from_chat_id`. + disable_notification (:obj:`bool`, optional): |disable_notification| + protect_content (:obj:`bool`, optional): |protect_content| + + .. versionadded:: 13.10 + message_thread_id (:obj:`int`, optional): |message_thread_id_arg| + + .. versionadded:: 20.0 + + Returns: + :class:`telegram.Message`: On success, the sent Message is returned. + + Raises: + :class:`telegram.error.TelegramError` + """ + data: JSONDict = { + "chat_id": chat_id, + "from_chat_id": from_chat_id, + "message_id": message_id, + } + + return await self._send_message( + "forwardMessage", + data, + disable_notification=disable_notification, + protect_content=protect_content, + message_thread_id=message_thread_id, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def forward_messages( + self, + chat_id: Union[int, str], + from_chat_id: Union[str, int], + message_ids: Sequence[int], + disable_notification: ODVInput[bool] = DEFAULT_NONE, + protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_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, + ) -> 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 + can't be forwarded. Album grouping is kept for forwarded messages. + + .. versionadded:: 20.8 + + Args: + chat_id (:obj:`int` | :obj:`str`): |chat_id_channel| + 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_ids (Sequence[:obj:`int`]): A list of + :tg-const:`telegram.constants.BulkRequestLimit.MIN_LIMIT`- + :tg-const:`telegram.constants.BulkRequestLimit.MAX_LIMIT` identifiers of messages + in the chat :paramref:`from_chat_id` to forward. The identifiers must be specified + in a strictly increasing order. + disable_notification (:obj:`bool`, optional): |disable_notification| + protect_content (:obj:`bool`, optional): |protect_content| + message_thread_id (:obj:`int`, optional): |message_thread_id_arg| + + Returns: + Tuple[:class:`telegram.Message`]: On success, a tuple of ``MessageId`` of sent messages + is returned. + + Raises: + :class:`telegram.error.TelegramError` + """ + data: JSONDict = { + "chat_id": chat_id, + "from_chat_id": from_chat_id, + "message_ids": message_ids, + "disable_notification": disable_notification, + "protect_content": protect_content, + "message_thread_id": message_thread_id, + } + + result = await self._post( + "forwardMessages", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + return MessageId.de_list(result, self) + + async def send_photo( + self, + chat_id: Union[int, str], + photo: Union[FileInput, "PhotoSize"], + caption: Optional[str] = None, + disable_notification: ODVInput[bool] = DEFAULT_NONE, + reply_markup: Optional[ReplyMarkup] = None, + parse_mode: ODVInput[str] = DEFAULT_NONE, + caption_entities: Optional[Sequence["MessageEntity"]] = None, + protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: Optional[int] = None, + has_spoiler: Optional[bool] = None, + reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, + message_effect_id: Optional[str] = None, + show_caption_above_media: Optional[bool] = None, + *, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + reply_to_message_id: Optional[int] = None, + filename: 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, + ) -> Message: + """Use this method to send photos. + + .. seealso:: :wiki:`Working with Files and Media ` + + Args: + chat_id (:obj:`int` | :obj:`str`): |chat_id_channel| + 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. + + Caution: + * The photo must be at most 10MB in size. + * The photo's width and height must not exceed 10000 in total. + * Width and height ratio must be at most 20. + + .. versionchanged:: 13.2 + Accept :obj:`bytes` as input. + + .. versionchanged:: 20.0 + File paths as input is also accepted for bots *not* running in + :paramref:`~telegram.Bot.local_mode`. + caption (:obj:`str`, optional): Photo caption (may also be used when resending photos + by file_id), 0-:tg-const:`telegram.constants.MessageLimit.CAPTION_LENGTH` + characters after entities parsing. + parse_mode (:obj:`str`, optional): |parse_mode| + caption_entities (Sequence[:class:`telegram.MessageEntity`], optional): + |caption_entities| + + .. versionchanged:: 20.0 + |sequenceargs| + disable_notification (:obj:`bool`, optional): |disable_notification| + protect_content (:obj:`bool`, optional): |protect_content| + + .. versionadded:: 13.10 + message_thread_id (:obj:`int`, optional): |message_thread_id_arg| + + .. versionadded:: 20.0 + + reply_markup (:class:`InlineKeyboardMarkup` | :class:`ReplyKeyboardMarkup` | \ + :class:`ReplyKeyboardRemove` | :class:`ForceReply`, optional): + Additional interface options. An object for an inline keyboard, custom reply + keyboard, instructions to remove reply keyboard or to force a reply from the user. + has_spoiler (:obj:`bool`, optional): Pass :obj:`True` if the photo needs to be covered + with a spoiler animation. + + .. versionadded:: 20.0 + reply_parameters (:class:`telegram.ReplyParameters`, optional): |reply_parameters| + + .. versionadded:: 20.8 + business_connection_id (:obj:`str`, optional): |business_id_str| + + .. versionadded:: 21.1 + message_effect_id (:obj:`str`, optional): |message_effect_id| + + .. versionadded:: 21.3 + show_caption_above_media (:obj:`bool`, optional): Pass |show_cap_above_med| + + .. versionadded:: 21.3 + + Keyword Args: + allow_sending_without_reply (:obj:`bool`, optional): |allow_sending_without_reply| + Mutually exclusive with :paramref:`reply_parameters`, which this is a convenience + parameter for + + .. versionchanged:: 20.8 + Bot API 7.0 introduced :paramref:`reply_parameters` |rtm_aswr_deprecated| + + .. versionchanged:: 21.0 + |keyword_only_arg| + reply_to_message_id (:obj:`int`, optional): |reply_to_msg_id| + Mutually exclusive with :paramref:`reply_parameters`, which this is a convenience + parameter for + + .. versionchanged:: 20.8 + Bot API 7.0 introduced :paramref:`reply_parameters` |rtm_aswr_deprecated| + + .. versionchanged:: 21.0 + |keyword_only_arg| + filename (:obj:`str`, optional): Custom file name for the photo, when uploading a + new file. Convenience parameter, useful e.g. when sending files generated by the + :obj:`tempfile` module. + + .. versionadded:: 13.1 + + Returns: + :class:`telegram.Message`: On success, the sent Message is returned. + + Raises: + :class:`telegram.error.TelegramError` + + """ + data: JSONDict = { + "chat_id": chat_id, + "photo": self._parse_file_input(photo, PhotoSize, filename=filename), + "has_spoiler": has_spoiler, + "show_caption_above_media": show_caption_above_media, + } + + return await self._send_message( + "sendPhoto", + data, + reply_to_message_id=reply_to_message_id, + disable_notification=disable_notification, + reply_markup=reply_markup, + allow_sending_without_reply=allow_sending_without_reply, + protect_content=protect_content, + message_thread_id=message_thread_id, + caption=caption, + parse_mode=parse_mode, + caption_entities=caption_entities, + reply_parameters=reply_parameters, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + business_connection_id=business_connection_id, + message_effect_id=message_effect_id, + ) + + async def send_audio( + self, + chat_id: Union[int, str], + audio: Union[FileInput, "Audio"], + duration: Optional[int] = None, + performer: Optional[str] = None, + title: Optional[str] = None, + caption: Optional[str] = None, + disable_notification: ODVInput[bool] = DEFAULT_NONE, + reply_markup: Optional[ReplyMarkup] = None, + parse_mode: ODVInput[str] = DEFAULT_NONE, + caption_entities: Optional[Sequence["MessageEntity"]] = None, + protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: Optional[int] = None, + thumbnail: Optional[FileInput] = None, + reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, + message_effect_id: Optional[str] = None, + *, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + reply_to_message_id: Optional[int] = None, + filename: 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, + ) -> Message: + """ + Use this method to send audio files, if you want Telegram clients to display them in the + music player. Your audio must be in the ``.mp3`` or ``.m4a`` format. + + Bots can currently send audio files of up to + :tg-const:`telegram.constants.FileSizeLimit.FILESIZE_UPLOAD` in size, this limit may be + changed in the future. + + For sending voice messages, use the :meth:`send_voice` method instead. + + .. seealso:: :wiki:`Working with Files and Media ` + + .. versionchanged:: 20.5 + |removed_thumb_arg| + + Args: + chat_id (:obj:`int` | :obj:`str`): |chat_id_channel| + 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 + Accept :obj:`bytes` as input. + + .. versionchanged:: 20.0 + File paths as input is also accepted for bots *not* running in + :paramref:`~telegram.Bot.local_mode`. + caption (:obj:`str`, optional): Audio caption, + 0-:tg-const:`telegram.constants.MessageLimit.CAPTION_LENGTH` characters after + entities parsing. + parse_mode (:obj:`str`, optional): |parse_mode| + caption_entities (Sequence[:class:`telegram.MessageEntity`], optional): + |caption_entities| + + .. versionchanged:: 20.0 + |sequenceargs| + duration (:obj:`int`, optional): Duration of sent audio in seconds. + performer (:obj:`str`, optional): Performer. + title (:obj:`str`, optional): Track name. + disable_notification (:obj:`bool`, optional): |disable_notification| + protect_content (:obj:`bool`, optional): |protect_content| + + .. versionadded:: 13.10 + message_thread_id (:obj:`int`, optional): |message_thread_id_arg| + + .. versionadded:: 20.0 + + reply_markup (:class:`InlineKeyboardMarkup` | :class:`ReplyKeyboardMarkup` | \ + :class:`ReplyKeyboardRemove` | :class:`ForceReply`, optional): + Additional interface options. An object for an inline keyboard, custom reply + keyboard, instructions to remove reply keyboard or to force a reply from the user. + thumbnail (:term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | :obj:`str`, \ + optional): |thumbdocstring| + + .. versionadded:: 20.2 + reply_parameters (:obj:`ReplyParameters`, optional): |reply_parameters| + + .. versionadded:: 20.8 + business_connection_id (:obj:`str`, optional): |business_id_str| + + .. versionadded:: 21.1 + message_effect_id (:obj:`str`, optional): |message_effect_id| + + .. versionadded:: 21.3 + + Keyword Args: + allow_sending_without_reply (:obj:`bool`, optional): |allow_sending_without_reply| + Mutually exclusive with :paramref:`reply_parameters`, which this is a convenience + parameter for + + .. versionchanged:: 20.8 + Bot API 7.0 introduced :paramref:`reply_parameters` |rtm_aswr_deprecated| + + .. versionchanged:: 21.0 + |keyword_only_arg| + reply_to_message_id (:obj:`int`, optional): |reply_to_msg_id| + Mutually exclusive with :paramref:`reply_parameters`, which this is a convenience + parameter for + + .. versionchanged:: 20.8 + Bot API 7.0 introduced :paramref:`reply_parameters` |rtm_aswr_deprecated| + + .. versionchanged:: 21.0 + |keyword_only_arg| + filename (:obj:`str`, optional): Custom file name for the audio, when uploading a + new file. Convenience parameter, useful e.g. when sending files generated by the + :obj:`tempfile` module. + + .. versionadded:: 13.1 + + Returns: + :class:`telegram.Message`: On success, the sent Message is returned. + + Raises: + :class:`telegram.error.TelegramError` + + """ + data: JSONDict = { + "chat_id": chat_id, + "audio": self._parse_file_input(audio, Audio, filename=filename), + "duration": duration, + "performer": performer, + "title": title, + "thumbnail": self._parse_file_input(thumbnail, attach=True) if thumbnail else None, + } + + return await self._send_message( + "sendAudio", + data, + reply_to_message_id=reply_to_message_id, + disable_notification=disable_notification, + reply_markup=reply_markup, + allow_sending_without_reply=allow_sending_without_reply, + protect_content=protect_content, + message_thread_id=message_thread_id, + caption=caption, + parse_mode=parse_mode, + caption_entities=caption_entities, + reply_parameters=reply_parameters, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + business_connection_id=business_connection_id, + message_effect_id=message_effect_id, + ) + + async def send_document( + self, + chat_id: Union[int, str], + document: Union[FileInput, "Document"], + caption: Optional[str] = None, + disable_notification: ODVInput[bool] = DEFAULT_NONE, + reply_markup: Optional[ReplyMarkup] = None, + parse_mode: ODVInput[str] = DEFAULT_NONE, + disable_content_type_detection: Optional[bool] = None, + caption_entities: Optional[Sequence["MessageEntity"]] = None, + protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: Optional[int] = None, + thumbnail: Optional[FileInput] = None, + reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, + message_effect_id: Optional[str] = None, + *, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + reply_to_message_id: Optional[int] = None, + filename: 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, + ) -> Message: + """ + Use this method to send general files. + + Bots can currently send files of any type of up to + :tg-const:`telegram.constants.FileSizeLimit.FILESIZE_UPLOAD` in size, this limit may be + changed in the future. + + .. seealso:: :wiki:`Working with Files and Media ` + + .. versionchanged:: 20.5 + |removed_thumb_arg| + + Args: + chat_id (:obj:`int` | :obj:`str`): |chat_id_channel| + 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. + + Note: + Sending by URL will currently only work ``GIF``, ``PDF`` & ``ZIP`` files. + + .. versionchanged:: 13.2 + Accept :obj:`bytes` as input. + + .. versionchanged:: 20.0 + File paths as input is also accepted for bots *not* running in + :paramref:`~telegram.Bot.local_mode`. + caption (:obj:`str`, optional): Document caption (may also be used when resending + documents by file_id), 0-:tg-const:`telegram.constants.MessageLimit.CAPTION_LENGTH` + characters after entities parsing. + disable_content_type_detection (:obj:`bool`, optional): Disables automatic server-side + content type detection for files uploaded using multipart/form-data. + parse_mode (:obj:`str`, optional): |parse_mode| + caption_entities (Sequence[:class:`telegram.MessageEntity`], optional): + |caption_entities| + + .. versionchanged:: 20.0 + |sequenceargs| + disable_notification (:obj:`bool`, optional): |disable_notification| + protect_content (:obj:`bool`, optional): |protect_content| + + .. versionadded:: 13.10 + message_thread_id (:obj:`int`, optional): |message_thread_id_arg| + + .. versionadded:: 20.0 + + reply_markup (:class:`InlineKeyboardMarkup` | :class:`ReplyKeyboardMarkup` | \ + :class:`ReplyKeyboardRemove` | :class:`ForceReply`, optional): + Additional interface options. An object for an inline keyboard, custom reply + keyboard, instructions to remove reply keyboard or to force a reply from the user. + thumbnail (:term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | :obj:`str`, \ + optional): |thumbdocstring| + + .. versionadded:: 20.2 + reply_parameters (:class:`telegram.ReplyParameters`, optional): |reply_parameters| + + .. versionadded:: 20.8 + business_connection_id (:obj:`str`, optional): |business_id_str| + + .. versionadded:: 21.1 + message_effect_id (:obj:`str`, optional): |message_effect_id| + + .. versionadded:: 21.3 + + Keyword Args: + allow_sending_without_reply (:obj:`bool`, optional): |allow_sending_without_reply| + Mutually exclusive with :paramref:`reply_parameters`, which this is a convenience + parameter for + + .. versionchanged:: 20.8 + Bot API 7.0 introduced :paramref:`reply_parameters` |rtm_aswr_deprecated| + + .. versionchanged:: 21.0 + |keyword_only_arg| + reply_to_message_id (:obj:`int`, optional): |reply_to_msg_id| + Mutually exclusive with :paramref:`reply_parameters`, which this is a convenience + parameter for + + .. versionchanged:: 20.8 + Bot API 7.0 introduced :paramref:`reply_parameters` |rtm_aswr_deprecated| + + .. versionchanged:: 21.0 + |keyword_only_arg| + filename (:obj:`str`, optional): Custom file name for the document, when uploading a + new file. Convenience parameter, useful e.g. when sending files generated by the + :obj:`tempfile` module. + + Returns: + :class:`telegram.Message`: On success, the sent Message is returned. + + Raises: + :class:`telegram.error.TelegramError` + + """ + data: JSONDict = { + "chat_id": chat_id, + "document": self._parse_file_input(document, Document, filename=filename), + "disable_content_type_detection": disable_content_type_detection, + "thumbnail": self._parse_file_input(thumbnail, attach=True) if thumbnail else None, + } + + return await self._send_message( + "sendDocument", + data, + reply_to_message_id=reply_to_message_id, + disable_notification=disable_notification, + reply_markup=reply_markup, + allow_sending_without_reply=allow_sending_without_reply, + protect_content=protect_content, + message_thread_id=message_thread_id, + caption=caption, + parse_mode=parse_mode, + caption_entities=caption_entities, + reply_parameters=reply_parameters, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + business_connection_id=business_connection_id, + message_effect_id=message_effect_id, + ) + + async def send_sticker( + self, + chat_id: Union[int, str], + sticker: Union[FileInput, "Sticker"], + disable_notification: ODVInput[bool] = DEFAULT_NONE, + reply_markup: Optional[ReplyMarkup] = None, + protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: Optional[int] = None, + emoji: Optional[str] = None, + reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, + message_effect_id: Optional[str] = None, + *, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + reply_to_message_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, + ) -> Message: + """ + Use this method to send static ``.WEBP``, animated ``.TGS``, or video ``.WEBM`` stickers. + + .. seealso:: :wiki:`Working with Files and Media ` + + Args: + chat_id (:obj:`int` | :obj:`str`): |chat_id_channel| + 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. + + Lastly you can pass an existing :class:`telegram.Sticker` object to send. + + .. versionchanged:: 13.2 + Accept :obj:`bytes` as input. + + .. versionchanged:: 20.0 + File paths as input is also accepted for bots *not* running in + :paramref:`~telegram.Bot.local_mode`. + emoji (:obj:`str`, optional): Emoji associated with the sticker; only for just + uploaded stickers + + .. versionadded:: 20.2 + disable_notification (:obj:`bool`, optional): |disable_notification| + protect_content (:obj:`bool`, optional): |protect_content| + + .. versionadded:: 13.10 + message_thread_id (:obj:`int`, optional): |message_thread_id_arg| + + .. versionadded:: 20.0 + + reply_markup (:class:`InlineKeyboardMarkup` | :class:`ReplyKeyboardMarkup` | \ + :class:`ReplyKeyboardRemove` | :class:`ForceReply`, optional): + Additional interface options. An object for an inline keyboard, custom reply + keyboard, instructions to remove reply keyboard or to force a reply from the user. + reply_parameters (:class:`telegram.ReplyParameters`, optional): |reply_parameters| + + .. versionadded:: 20.8 + business_connection_id (:obj:`str`, optional): |business_id_str| + + .. versionadded:: 21.1 + message_effect_id (:obj:`str`, optional): |message_effect_id| + + .. versionadded:: 21.3 + + Keyword Args: + allow_sending_without_reply (:obj:`bool`, optional): |allow_sending_without_reply| + Mutually exclusive with :paramref:`reply_parameters`, which this is a convenience + parameter for + + .. versionchanged:: 20.8 + Bot API 7.0 introduced :paramref:`reply_parameters` |rtm_aswr_deprecated| + + .. versionchanged:: 21.0 + |keyword_only_arg| + reply_to_message_id (:obj:`int`, optional): |reply_to_msg_id| + Mutually exclusive with :paramref:`reply_parameters`, which this is a convenience + parameter for + + .. versionchanged:: 20.8 + Bot API 7.0 introduced :paramref:`reply_parameters` |rtm_aswr_deprecated| + + .. versionchanged:: 21.0 + |keyword_only_arg| + + Returns: + :class:`telegram.Message`: On success, the sent Message is returned. + + Raises: + :class:`telegram.error.TelegramError` + + """ + data: JSONDict = { + "chat_id": chat_id, + "sticker": self._parse_file_input(sticker, Sticker), + "emoji": emoji, + } + return await self._send_message( + "sendSticker", + data, + reply_to_message_id=reply_to_message_id, + disable_notification=disable_notification, + reply_markup=reply_markup, + allow_sending_without_reply=allow_sending_without_reply, + protect_content=protect_content, + message_thread_id=message_thread_id, + reply_parameters=reply_parameters, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + business_connection_id=business_connection_id, + message_effect_id=message_effect_id, + ) + + async def send_video( + self, + chat_id: Union[int, str], + video: Union[FileInput, "Video"], + duration: Optional[int] = None, + caption: Optional[str] = None, + disable_notification: ODVInput[bool] = DEFAULT_NONE, + reply_markup: Optional[ReplyMarkup] = None, + width: Optional[int] = None, + height: Optional[int] = None, + parse_mode: ODVInput[str] = DEFAULT_NONE, + supports_streaming: Optional[bool] = None, + caption_entities: Optional[Sequence["MessageEntity"]] = None, + protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: Optional[int] = None, + has_spoiler: Optional[bool] = None, + thumbnail: Optional[FileInput] = None, + reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, + message_effect_id: Optional[str] = None, + show_caption_above_media: Optional[bool] = None, + *, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + reply_to_message_id: Optional[int] = None, + filename: 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, + ) -> Message: + """Use this method to send video files, Telegram clients support mp4 videos + (other formats may be sent as Document). + + Bots can currently send video files of up to + :tg-const:`telegram.constants.FileSizeLimit.FILESIZE_UPLOAD` in size, this limit may be + changed in the future. + + Note: + :paramref:`thumbnail` will be ignored for small video files, for which Telegram can + easily generate thumbnails. However, this behaviour is undocumented and might be + changed by Telegram. + + .. seealso:: :wiki:`Working with Files and Media ` + + .. versionchanged:: 20.5 + |removed_thumb_arg| + + Args: + chat_id (:obj:`int` | :obj:`str`): |chat_id_channel| + 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. + + .. versionchanged:: 13.2 + Accept :obj:`bytes` as input. + + .. 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. + width (:obj:`int`, optional): Video width. + height (:obj:`int`, optional): Video height. + 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. + parse_mode (:obj:`str`, optional): |parse_mode| + caption_entities (Sequence[:class:`telegram.MessageEntity`], optional): + |caption_entities| + + .. versionchanged:: 20.0 + |sequenceargs| + supports_streaming (:obj:`bool`, optional): Pass :obj:`True`, if the uploaded video is + suitable for streaming. + disable_notification (:obj:`bool`, optional): |disable_notification| + protect_content (:obj:`bool`, optional): |protect_content| + + .. versionadded:: 13.10 + message_thread_id (:obj:`int`, optional): |message_thread_id_arg| + + .. versionadded:: 20.0 + + reply_markup (:class:`InlineKeyboardMarkup` | :class:`ReplyKeyboardMarkup` | \ + :class:`ReplyKeyboardRemove` | :class:`ForceReply`, optional): + Additional interface options. An object for an inline keyboard, custom reply + keyboard, instructions to remove reply keyboard or to force a reply from the user. + has_spoiler (:obj:`bool`, optional): Pass :obj:`True` if the video needs to be covered + with a spoiler animation. + + .. versionadded:: 20.0 + thumbnail (:term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | :obj:`str`, \ + optional): |thumbdocstring| + + .. versionadded:: 20.2 + reply_parameters (:class:`telegram.ReplyParameters`, optional): |reply_parameters| + + .. versionadded:: 20.8 + business_connection_id (:obj:`str`, optional): |business_id_str| + + .. versionadded:: 21.1 + message_effect_id (:obj:`str`, optional): |message_effect_id| + + .. versionadded:: 21.3 + show_caption_above_media (:obj:`bool`, optional): Pass |show_cap_above_med| + + .. versionadded:: 21.3 + + Keyword Args: + allow_sending_without_reply (:obj:`bool`, optional): |allow_sending_without_reply| + Mutually exclusive with :paramref:`reply_parameters`, which this is a convenience + parameter for + + .. versionchanged:: 20.8 + Bot API 7.0 introduced :paramref:`reply_parameters` |rtm_aswr_deprecated| + + .. versionchanged:: 21.0 + |keyword_only_arg| + reply_to_message_id (:obj:`int`, optional): |reply_to_msg_id| + Mutually exclusive with :paramref:`reply_parameters`, which this is a convenience + parameter for + + .. versionchanged:: 20.8 + Bot API 7.0 introduced :paramref:`reply_parameters` |rtm_aswr_deprecated| + + .. versionchanged:: 21.0 + |keyword_only_arg| + filename (:obj:`str`, optional): Custom file name for the video, when uploading a + new file. Convenience parameter, useful e.g. when sending files generated by the + :obj:`tempfile` module. + + .. versionadded:: 13.1 + + Returns: + :class:`telegram.Message`: On success, the sent Message is returned. + + Raises: + :class:`telegram.error.TelegramError` + + """ + data: JSONDict = { + "chat_id": chat_id, + "video": self._parse_file_input(video, Video, filename=filename), + "duration": duration, + "width": width, + "height": height, + "supports_streaming": supports_streaming, + "thumbnail": self._parse_file_input(thumbnail, attach=True) if thumbnail else None, + "has_spoiler": has_spoiler, + "show_caption_above_media": show_caption_above_media, + } + + return await self._send_message( + "sendVideo", + data, + reply_to_message_id=reply_to_message_id, + disable_notification=disable_notification, + reply_markup=reply_markup, + allow_sending_without_reply=allow_sending_without_reply, + protect_content=protect_content, + message_thread_id=message_thread_id, + caption=caption, + parse_mode=parse_mode, + caption_entities=caption_entities, + reply_parameters=reply_parameters, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + business_connection_id=business_connection_id, + message_effect_id=message_effect_id, + ) + + async def send_video_note( + self, + chat_id: Union[int, str], + video_note: Union[FileInput, "VideoNote"], + duration: Optional[int] = None, + length: Optional[int] = None, + disable_notification: ODVInput[bool] = DEFAULT_NONE, + reply_markup: Optional[ReplyMarkup] = None, + protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: Optional[int] = None, + thumbnail: Optional[FileInput] = None, + reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, + message_effect_id: Optional[str] = None, + *, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + reply_to_message_id: Optional[int] = None, + filename: 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, + ) -> Message: + """ + As of v.4.0, Telegram clients support rounded square mp4 videos of up to 1 minute long. + Use this method to send video messages. + + Note: + :paramref:`thumbnail` will be ignored for small video files, for which Telegram can + easily generate thumbnails. However, this behaviour is undocumented and might be + changed by Telegram. + + .. seealso:: :wiki:`Working with Files and Media ` + + .. versionchanged:: 20.5 + |removed_thumb_arg| + + Args: + chat_id (:obj:`int` | :obj:`str`): |chat_id_channel| + 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| + Lastly you can pass an existing :class:`telegram.VideoNote` object to send. + Sending video notes by a URL is currently unsupported. + + .. versionchanged:: 13.2 + Accept :obj:`bytes` as input. + + .. 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. + length (:obj:`int`, optional): Video width and height, i.e. diameter of the video + message. + disable_notification (:obj:`bool`, optional): |disable_notification| + protect_content (:obj:`bool`, optional): |protect_content| + + .. versionadded:: 13.10 + message_thread_id (:obj:`int`, optional): |message_thread_id_arg| + + .. versionadded:: 20.0 + + reply_markup (:class:`InlineKeyboardMarkup` | :class:`ReplyKeyboardMarkup` | \ + :class:`ReplyKeyboardRemove` | :class:`ForceReply`, optional): + Additional interface options. An object for an inline keyboard, custom reply + keyboard, instructions to remove reply keyboard or to force a reply from the user. + thumbnail (:term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | :obj:`str`, \ + optional): |thumbdocstring| + + .. versionadded:: 20.2 + reply_parameters (:class:`telegram.ReplyParameters`, optional): |reply_parameters| + + .. versionadded:: 20.8 + business_connection_id (:obj:`str`, optional): |business_id_str| + + .. versionadded:: 21.1 + message_effect_id (:obj:`str`, optional): |message_effect_id| + + .. versionadded:: 21.3 + + Keyword Args: + allow_sending_without_reply (:obj:`bool`, optional): |allow_sending_without_reply| + Mutually exclusive with :paramref:`reply_parameters`, which this is a convenience + parameter for + + .. versionchanged:: 20.8 + Bot API 7.0 introduced :paramref:`reply_parameters` |rtm_aswr_deprecated| + + .. versionchanged:: 21.0 + |keyword_only_arg| + reply_to_message_id (:obj:`int`, optional): |reply_to_msg_id| + Mutually exclusive with :paramref:`reply_parameters`, which this is a convenience + parameter for + + .. versionchanged:: 20.8 + Bot API 7.0 introduced :paramref:`reply_parameters` |rtm_aswr_deprecated| + + .. versionchanged:: 21.0 + |keyword_only_arg| + filename (:obj:`str`, optional): Custom file name for the video note, when uploading a + new file. Convenience parameter, useful e.g. when sending files generated by the + :obj:`tempfile` module. + + .. versionadded:: 13.1 + + Returns: + :class:`telegram.Message`: On success, the sent Message is returned. + + Raises: + :class:`telegram.error.TelegramError` + + """ + data: JSONDict = { + "chat_id": chat_id, + "video_note": self._parse_file_input(video_note, VideoNote, filename=filename), + "duration": duration, + "length": length, + "thumbnail": self._parse_file_input(thumbnail, attach=True) if thumbnail else None, + } + + return await self._send_message( + "sendVideoNote", + data, + reply_to_message_id=reply_to_message_id, + disable_notification=disable_notification, + reply_markup=reply_markup, + allow_sending_without_reply=allow_sending_without_reply, + protect_content=protect_content, + message_thread_id=message_thread_id, + reply_parameters=reply_parameters, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + business_connection_id=business_connection_id, + message_effect_id=message_effect_id, + ) + + async def send_animation( + self, + chat_id: Union[int, str], + animation: Union[FileInput, "Animation"], + duration: Optional[int] = None, + width: Optional[int] = None, + height: Optional[int] = None, + caption: Optional[str] = None, + parse_mode: ODVInput[str] = DEFAULT_NONE, + disable_notification: ODVInput[bool] = DEFAULT_NONE, + reply_markup: Optional[ReplyMarkup] = None, + caption_entities: Optional[Sequence["MessageEntity"]] = None, + protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: Optional[int] = None, + has_spoiler: Optional[bool] = None, + thumbnail: Optional[FileInput] = None, + reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, + message_effect_id: Optional[str] = None, + show_caption_above_media: Optional[bool] = None, + *, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + reply_to_message_id: Optional[int] = None, + filename: 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, + ) -> Message: + """ + Use this method to send animation files (GIF or H.264/MPEG-4 AVC video without sound). + Bots can currently send animation files of up to + :tg-const:`telegram.constants.FileSizeLimit.FILESIZE_UPLOAD` in size, this limit may be + changed in the future. + + Note: + :paramref:`thumbnail` will be ignored for small files, for which Telegram can easily + generate thumbnails. However, this behaviour is undocumented and might be changed + by Telegram. + + .. seealso:: :wiki:`Working with Files and Media ` + + .. versionchanged:: 20.5 + |removed_thumb_arg| + + Args: + chat_id (:obj:`int` | :obj:`str`): |chat_id_channel| + 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. + width (:obj:`int`, optional): Animation width. + height (:obj:`int`, optional): Animation height. + caption (:obj:`str`, optional): Animation caption (may also be used when resending + animations by file_id), + 0-:tg-const:`telegram.constants.MessageLimit.CAPTION_LENGTH` characters after + entities parsing. + parse_mode (:obj:`str`, optional): |parse_mode| + caption_entities (Sequence[:class:`telegram.MessageEntity`], optional): + |caption_entities| + + .. versionchanged:: 20.0 + |sequenceargs| + disable_notification (:obj:`bool`, optional): |disable_notification| + protect_content (:obj:`bool`, optional): |protect_content| + + .. versionadded:: 13.10 + message_thread_id (:obj:`int`, optional): |message_thread_id_arg| + + .. versionadded:: 20.0 + + reply_markup (:class:`InlineKeyboardMarkup` | :class:`ReplyKeyboardMarkup` | \ + :class:`ReplyKeyboardRemove` | :class:`ForceReply`, optional): + Additional interface options. An object for an inline keyboard, custom reply + keyboard, instructions to remove reply keyboard or to force a reply from the user. + has_spoiler (:obj:`bool`, optional): Pass :obj:`True` if the animation needs to be + covered with a spoiler animation. + + .. versionadded:: 20.0 + thumbnail (:term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | :obj:`str`, \ + optional): |thumbdocstring| + + .. versionadded:: 20.2 + reply_parameters (:class:`telegram.ReplyParameters`, optional): |reply_parameters| + + .. versionadded:: 20.8 + business_connection_id (:obj:`str`, optional): |business_id_str| + + .. versionadded:: 21.1 + message_effect_id (:obj:`str`, optional): |message_effect_id| + + .. versionadded:: 21.3 + show_caption_above_media (:obj:`bool`, optional): Pass |show_cap_above_med| + + .. versionadded:: 21.3 + + Keyword Args: + allow_sending_without_reply (:obj:`bool`, optional): |allow_sending_without_reply| + Mutually exclusive with :paramref:`reply_parameters`, which this is a convenience + parameter for + + .. versionchanged:: 20.8 + Bot API 7.0 introduced :paramref:`reply_parameters` |rtm_aswr_deprecated| + + .. versionchanged:: 21.0 + |keyword_only_arg| + reply_to_message_id (:obj:`int`, optional): |reply_to_msg_id| + Mutually exclusive with :paramref:`reply_parameters`, which this is a convenience + parameter for + + .. versionchanged:: 20.8 + Bot API 7.0 introduced :paramref:`reply_parameters` |rtm_aswr_deprecated| + + .. versionchanged:: 21.0 + |keyword_only_arg| + filename (:obj:`str`, optional): Custom file name for the animation, when uploading a + new file. Convenience parameter, useful e.g. when sending files generated by the + :obj:`tempfile` module. + + .. versionadded:: 13.1 + + Returns: + :class:`telegram.Message`: On success, the sent Message is returned. + + Raises: + :class:`telegram.error.TelegramError` + + """ + data: JSONDict = { + "chat_id": chat_id, + "animation": self._parse_file_input(animation, Animation, filename=filename), + "duration": duration, + "width": width, + "height": height, + "thumbnail": self._parse_file_input(thumbnail, attach=True) if thumbnail else None, + "has_spoiler": has_spoiler, + "show_caption_above_media": show_caption_above_media, + } + + return await self._send_message( + "sendAnimation", + data, + reply_to_message_id=reply_to_message_id, + disable_notification=disable_notification, + reply_markup=reply_markup, + allow_sending_without_reply=allow_sending_without_reply, + protect_content=protect_content, + message_thread_id=message_thread_id, + caption=caption, + parse_mode=parse_mode, + caption_entities=caption_entities, + reply_parameters=reply_parameters, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + business_connection_id=business_connection_id, + message_effect_id=message_effect_id, + ) + + async def send_voice( + self, + chat_id: Union[int, str], + voice: Union[FileInput, "Voice"], + duration: Optional[int] = None, + caption: Optional[str] = None, + disable_notification: ODVInput[bool] = DEFAULT_NONE, + reply_markup: Optional[ReplyMarkup] = None, + parse_mode: ODVInput[str] = DEFAULT_NONE, + caption_entities: Optional[Sequence["MessageEntity"]] = None, + protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: Optional[int] = None, + reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, + message_effect_id: Optional[str] = None, + *, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + reply_to_message_id: Optional[int] = None, + filename: 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, + ) -> Message: + """ + Use this method to send audio files, if you want Telegram clients to display the file + as a playable voice message. For this to work, your audio must be in an ``.ogg`` file + encoded with OPUS , or in .MP3 format, or in .M4A format (other formats may be sent as + :class:`~telegram.Audio` or :class:`~telegram.Document`). Bots can currently send voice + messages of up to :tg-const:`telegram.constants.FileSizeLimit.FILESIZE_UPLOAD` in size, + this limit may be changed in the future. + + Note: + To use this method, the file must have the type :mimetype:`audio/ogg` and be no more + than :tg-const:`telegram.constants.FileSizeLimit.VOICE_NOTE_FILE_SIZE` in size. + :tg-const:`telegram.constants.FileSizeLimit.VOICE_NOTE_FILE_SIZE`- + :tg-const:`telegram.constants.FileSizeLimit.FILESIZE_DOWNLOAD` voice notes will be + sent as files. + + .. seealso:: :wiki:`Working with Files and Media ` + + Args: + chat_id (:obj:`int` | :obj:`str`): |chat_id_channel| + 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. + + .. versionchanged:: 13.2 + Accept :obj:`bytes` as input. + + .. versionchanged:: 20.0 + File paths as input is also accepted for bots *not* running in + :paramref:`~telegram.Bot.local_mode`. + caption (:obj:`str`, optional): Voice message caption, + 0-:tg-const:`telegram.constants.MessageLimit.CAPTION_LENGTH` characters after + entities parsing. + parse_mode (:obj:`str`, optional): |parse_mode| + caption_entities (Sequence[:class:`telegram.MessageEntity`], optional): + |caption_entities| + + .. versionchanged:: 20.0 + |sequenceargs| + duration (:obj:`int`, optional): Duration of the voice message in seconds. + disable_notification (:obj:`bool`, optional): |disable_notification| + protect_content (:obj:`bool`, optional): |protect_content| + + .. versionadded:: 13.10 + message_thread_id (:obj:`int`, optional): |message_thread_id_arg| + + .. versionadded:: 20.0 + + reply_markup (:class:`InlineKeyboardMarkup` | :class:`ReplyKeyboardMarkup` | \ + :class:`ReplyKeyboardRemove` | :class:`ForceReply`, optional): + Additional interface options. An object for an inline keyboard, custom reply + keyboard, instructions to remove reply keyboard or to force a reply from the user. + reply_parameters (:class:`telegram.ReplyParameters`, optional): |reply_parameters| + + .. versionadded:: 20.8 + business_connection_id (:obj:`str`, optional): |business_id_str| + + .. versionadded:: 21.1 + message_effect_id (:obj:`str`, optional): |message_effect_id| + + .. versionadded:: 21.3 + + Keyword Args: + allow_sending_without_reply (:obj:`bool`, optional): |allow_sending_without_reply| + Mutually exclusive with :paramref:`reply_parameters`, which this is a convenience + parameter for + + .. versionchanged:: 20.8 + Bot API 7.0 introduced :paramref:`reply_parameters` |rtm_aswr_deprecated| + + .. versionchanged:: 21.0 + |keyword_only_arg| + reply_to_message_id (:obj:`int`, optional): |reply_to_msg_id| + Mutually exclusive with :paramref:`reply_parameters`, which this is a convenience + parameter for + + .. versionchanged:: 20.8 + Bot API 7.0 introduced :paramref:`reply_parameters` |rtm_aswr_deprecated| + + .. versionchanged:: 21.0 + |keyword_only_arg| + filename (:obj:`str`, optional): Custom file name for the voice, when uploading a + new file. Convenience parameter, useful e.g. when sending files generated by the + :obj:`tempfile` module. + + .. versionadded:: 13.1 + + Returns: + :class:`telegram.Message`: On success, the sent Message is returned. + + Raises: + :class:`telegram.error.TelegramError` + + """ + data: JSONDict = { + "chat_id": chat_id, + "voice": self._parse_file_input(voice, Voice, filename=filename), + "duration": duration, + } + + return await self._send_message( + "sendVoice", + data, + reply_to_message_id=reply_to_message_id, + disable_notification=disable_notification, + reply_markup=reply_markup, + allow_sending_without_reply=allow_sending_without_reply, + protect_content=protect_content, + message_thread_id=message_thread_id, + caption=caption, + parse_mode=parse_mode, + caption_entities=caption_entities, + reply_parameters=reply_parameters, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + business_connection_id=business_connection_id, + message_effect_id=message_effect_id, + ) + + async def send_media_group( + self, + chat_id: Union[int, str], + media: Sequence[ + Union["InputMediaAudio", "InputMediaDocument", "InputMediaPhoto", "InputMediaVideo"] + ], + disable_notification: ODVInput[bool] = DEFAULT_NONE, + protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: Optional[int] = None, + reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, + message_effect_id: Optional[str] = None, + *, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + reply_to_message_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, + caption: Optional[str] = None, + parse_mode: ODVInput[str] = DEFAULT_NONE, + caption_entities: Optional[Sequence["MessageEntity"]] = None, + ) -> 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. + + Note: + If you supply a :paramref:`caption` (along with either :paramref:`parse_mode` or + :paramref:`caption_entities`), then items in :paramref:`media` must have no captions, + and vice versa. + + .. seealso:: :wiki:`Working with Files and Media ` + + .. versionchanged:: 20.0 + Returns a tuple instead of a list. + + Args: + chat_id (:obj:`int` | :obj:`str`): |chat_id_channel| + media (Sequence[:class:`telegram.InputMediaAudio`,\ + :class:`telegram.InputMediaDocument`, :class:`telegram.InputMediaPhoto`,\ + :class:`telegram.InputMediaVideo`]): An array + describing messages to be sent, must include + :tg-const:`telegram.constants.MediaGroupLimit.MIN_MEDIA_LENGTH`- + :tg-const:`telegram.constants.MediaGroupLimit.MAX_MEDIA_LENGTH` items. + + .. versionchanged:: 20.0 + |sequenceargs| + disable_notification (:obj:`bool`, optional): |disable_notification| + protect_content (:obj:`bool`, optional): |protect_content| + + .. versionadded:: 13.10 + message_thread_id (:obj:`int`, optional): |message_thread_id_arg| + + .. versionadded:: 20.0 + + reply_parameters (:class:`telegram.ReplyParameters`, optional): |reply_parameters| + + .. versionadded:: 20.8 + business_connection_id (:obj:`str`, optional): |business_id_str| + + .. versionadded:: 21.1 + message_effect_id (:obj:`str`, optional): |message_effect_id| + + .. versionadded:: 21.3 + + Keyword Args: + allow_sending_without_reply (:obj:`bool`, optional): |allow_sending_without_reply| + Mutually exclusive with :paramref:`reply_parameters`, which this is a convenience + parameter for + + .. versionchanged:: 20.8 + Bot API 7.0 introduced :paramref:`reply_parameters` |rtm_aswr_deprecated| + + .. versionchanged:: 21.0 + |keyword_only_arg| + reply_to_message_id (:obj:`int`, optional): |reply_to_msg_id| + Mutually exclusive with :paramref:`reply_parameters`, which this is a convenience + parameter for + + .. versionchanged:: 20.8 + Bot API 7.0 introduced :paramref:`reply_parameters` |rtm_aswr_deprecated| + + .. versionchanged:: 21.0 + |keyword_only_arg| + caption (:obj:`str`, optional): Caption that will be added to the + first element of :paramref:`media`, so that it will be used as caption for the + whole media group. + Defaults to :obj:`None`. + + .. versionadded:: 20.0 + parse_mode (:obj:`str` | :obj:`None`, optional): + Parse mode for :paramref:`caption`. + See the constants in :class:`telegram.constants.ParseMode` for the + available modes. + + .. versionadded:: 20.0 + caption_entities (Sequence[:class:`telegram.MessageEntity`], optional): + List of special entities for :paramref:`caption`, + which can be specified instead of :paramref:`parse_mode`. + Defaults to :obj:`None`. + + .. versionadded:: 20.0 + + Returns: + Tuple[:class:`telegram.Message`]: An array of the sent Messages. + + Raises: + :class:`telegram.error.TelegramError` + """ + if caption and any( + [ + any(item.caption for item in media), + any(item.caption_entities for item in media), + # if parse_mode was set explicitly, even to None, error must be raised + any(item.parse_mode is not DEFAULT_NONE for item in media), + ] + ): + raise ValueError("You can only supply either group caption or media with captions.") + + if caption: + # Copy first item (to avoid mutation of original object), apply group caption to it. + # This will lead to the group being shown with this caption. + item_to_get_caption = copy.copy(media[0]) + with item_to_get_caption._unfrozen(): + item_to_get_caption.caption = caption + if parse_mode is not DEFAULT_NONE: + item_to_get_caption.parse_mode = parse_mode + item_to_get_caption.caption_entities = parse_sequence_arg(caption_entities) + + # copy the list (just the references) to avoid mutating the original list + media = list(media) + media[0] = item_to_get_caption + + if allow_sending_without_reply is not DEFAULT_NONE and reply_parameters is not None: + raise ValueError( + "`allow_sending_without_reply` and `reply_parameters` are mutually exclusive." + ) + + if reply_to_message_id is not None and reply_parameters is not None: + raise ValueError( + "`reply_to_message_id` and `reply_parameters` are mutually exclusive." + ) + + if reply_to_message_id is not None: + reply_parameters = ReplyParameters( + message_id=reply_to_message_id, + allow_sending_without_reply=allow_sending_without_reply, + ) + + data: JSONDict = { + "chat_id": chat_id, + "media": media, + "disable_notification": disable_notification, + "protect_content": protect_content, + "message_thread_id": message_thread_id, + "reply_parameters": reply_parameters, + "business_connection_id": business_connection_id, + "message_effect_id": message_effect_id, + } + + result = await self._post( + "sendMediaGroup", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + return Message.de_list(result, self) + + async def send_location( + self, + chat_id: Union[int, str], + latitude: Optional[float] = None, + longitude: Optional[float] = None, + disable_notification: ODVInput[bool] = DEFAULT_NONE, + reply_markup: Optional[ReplyMarkup] = None, + live_period: Optional[int] = None, + horizontal_accuracy: Optional[float] = None, + heading: Optional[int] = None, + proximity_alert_radius: Optional[int] = None, + protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: Optional[int] = None, + reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, + message_effect_id: Optional[str] = None, + *, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + reply_to_message_id: Optional[int] = None, + location: Optional[Location] = 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, + ) -> Message: + """Use this method to send point on the map. + + Note: + You can either supply a :paramref:`latitude` and :paramref:`longitude` or a + :paramref:`location`. + + Args: + chat_id (:obj:`int` | :obj:`str`): |chat_id_channel| + latitude (:obj:`float`, optional): Latitude of location. + longitude (:obj:`float`, optional): Longitude of 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 + 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. + 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 + :tg-const:`telegram.constants.LocationLimit.MAX_HEADING` if specified. + proximity_alert_radius (:obj:`int`, optional): For live locations, a maximum distance + for proximity alerts about approaching another chat member, in meters. Must be + between :tg-const:`telegram.constants.LocationLimit.MIN_PROXIMITY_ALERT_RADIUS` + and :tg-const:`telegram.constants.LocationLimit.MAX_PROXIMITY_ALERT_RADIUS` + if specified. + disable_notification (:obj:`bool`, optional): |disable_notification| + protect_content (:obj:`bool`, optional): |protect_content| + + .. versionadded:: 13.10 + message_thread_id (:obj:`int`, optional): |message_thread_id_arg| + + .. versionadded:: 20.0 + + reply_markup (:class:`InlineKeyboardMarkup` | :class:`ReplyKeyboardMarkup` | \ + :class:`ReplyKeyboardRemove` | :class:`ForceReply`, optional): + Additional interface options. An object for an inline keyboard, custom reply + keyboard, instructions to remove reply keyboard or to force a reply from the user. + reply_parameters (:class:`telegram.ReplyParameters`, optional): |reply_parameters| + + .. versionadded:: 20.8 + business_connection_id (:obj:`str`, optional): |business_id_str| + + .. versionadded:: 21.1 + message_effect_id (:obj:`str`, optional): |message_effect_id| + + .. versionadded:: 21.3 + + Keyword Args: + allow_sending_without_reply (:obj:`bool`, optional): |allow_sending_without_reply| + Mutually exclusive with :paramref:`reply_parameters`, which this is a convenience + parameter for + + .. versionchanged:: 20.8 + Bot API 7.0 introduced :paramref:`reply_parameters` |rtm_aswr_deprecated| + + .. versionchanged:: 21.0 + |keyword_only_arg| + reply_to_message_id (:obj:`int`, optional): |reply_to_msg_id| + Mutually exclusive with :paramref:`reply_parameters`, which this is a convenience + parameter for + + .. versionchanged:: 20.8 + Bot API 7.0 introduced :paramref:`reply_parameters` |rtm_aswr_deprecated| + + .. versionchanged:: 21.0 + |keyword_only_arg| + location (:class:`telegram.Location`, optional): The location to send. + + Returns: + :class:`telegram.Message`: On success, the sent Message is returned. + + Raises: + :class:`telegram.error.TelegramError` + + """ + if not ((latitude is not None and longitude is not None) or location): + raise ValueError( + "Either location or latitude and longitude must be passed as argument." + ) + + if not (latitude is not None or longitude is not None) ^ bool(location): + raise ValueError( + "Either location or latitude and longitude must be passed as argument. Not both." + ) + + if isinstance(location, Location): + latitude = location.latitude + longitude = location.longitude + + data: JSONDict = { + "chat_id": chat_id, + "latitude": latitude, + "longitude": longitude, + "horizontal_accuracy": horizontal_accuracy, + "live_period": live_period, + "heading": heading, + "proximity_alert_radius": proximity_alert_radius, + } + + return await self._send_message( + "sendLocation", + data, + reply_to_message_id=reply_to_message_id, + disable_notification=disable_notification, + reply_markup=reply_markup, + allow_sending_without_reply=allow_sending_without_reply, + protect_content=protect_content, + message_thread_id=message_thread_id, + reply_parameters=reply_parameters, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + business_connection_id=business_connection_id, + message_effect_id=message_effect_id, + ) + + async def edit_message_live_location( + self, + chat_id: Optional[Union[str, int]] = None, + message_id: Optional[int] = None, + inline_message_id: Optional[str] = None, + latitude: Optional[float] = None, + longitude: Optional[float] = None, + reply_markup: Optional["InlineKeyboardMarkup"] = None, + horizontal_accuracy: Optional[float] = None, + heading: Optional[int] = None, + proximity_alert_radius: Optional[int] = None, + live_period: Optional[int] = None, + business_connection_id: Optional[str] = None, + *, + location: Optional[Location] = 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, + ) -> Union[Message, bool]: + """Use this method to edit live location messages sent by the bot or via the bot + (for inline bots). A location can be edited until its :attr:`telegram.Location.live_period` + expires or editing is explicitly disabled by a call to :meth:`stop_message_live_location`. + + Note: + You can either supply a :paramref:`latitude` and :paramref:`longitude` or a + :paramref:`location`. + + Args: + chat_id (:obj:`int` | :obj:`str`, optional): Required if :paramref:`inline_message_id` + is not specified. |chat_id_channel| + message_id (:obj:`int`, optional): Required if :paramref:`inline_message_id` is not + specified. Identifier of the message to edit. + inline_message_id (:obj:`str`, optional): Required if :paramref:`chat_id` and + :paramref:`message_id` are not specified. Identifier of the inline message. + latitude (:obj:`float`, optional): Latitude of location. + longitude (:obj:`float`, optional): Longitude of location. + horizontal_accuracy (:obj:`float`, optional): The radius of uncertainty for the + location, measured in meters; + 0-:tg-const:`telegram.constants.LocationLimit.HORIZONTAL_ACCURACY`. + heading (:obj:`int`, optional): Direction in which the user is moving, in degrees. Must + be between :tg-const:`telegram.constants.LocationLimit.MIN_HEADING` + and :tg-const:`telegram.constants.LocationLimit.MAX_HEADING` if specified. + proximity_alert_radius (:obj:`int`, optional): Maximum distance for proximity alerts + about approaching another chat member, in meters. Must be between + :tg-const:`telegram.constants.LocationLimit.MIN_PROXIMITY_ALERT_RADIUS` + and :tg-const:`telegram.constants.LocationLimit.MAX_PROXIMITY_ALERT_RADIUS` + 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 + 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 + the current ``live_period`` by more than a day, and the live location expiration + date must remain within the next 90 days. If not specified, then ``live_period`` + remains unchanged + + .. versionadded:: 21.2. + business_connection_id (:obj:`str`, optional): |business_id_str_edit| + + .. versionadded:: 21.4 + + Keyword Args: + location (:class:`telegram.Location`, optional): The location to send. + + Returns: + :class:`telegram.Message`: On success, if edited message is not an inline message, the + edited message is returned, otherwise :obj:`True` is returned. + """ + # The location parameter is a convenience functionality added by us, so enforcing the + # mutual exclusivity here is nothing that Telegram would handle anyway + if not (all([latitude, longitude]) or location): + raise ValueError( + "Either location or latitude and longitude must be passed as argument." + ) + if not (latitude is not None or longitude is not None) ^ bool(location): + raise ValueError( + "Either location or latitude and longitude must be passed as argument. Not both." + ) + + if isinstance(location, Location): + latitude = location.latitude + longitude = location.longitude + + data: JSONDict = { + "latitude": latitude, + "longitude": longitude, + "chat_id": chat_id, + "message_id": message_id, + "inline_message_id": inline_message_id, + "horizontal_accuracy": horizontal_accuracy, + "heading": heading, + "proximity_alert_radius": proximity_alert_radius, + "live_period": live_period, + } + + return await self._send_message( + "editMessageLiveLocation", + data, + reply_markup=reply_markup, + business_connection_id=business_connection_id, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def stop_message_live_location( + self, + chat_id: Optional[Union[str, int]] = None, + message_id: Optional[int] = None, + inline_message_id: Optional[str] = None, + reply_markup: Optional["InlineKeyboardMarkup"] = None, + business_connection_id: 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, + ) -> Union[Message, bool]: + """Use this method to stop updating a live location message sent by the bot or via the bot + (for inline bots) before :paramref:`~telegram.Location.live_period` expires. + + Args: + chat_id (:obj:`int` | :obj:`str`, optional): Required if :paramref:`inline_message_id` + is not specified. |chat_id_channel| + message_id (:obj:`int`, optional): Required if :paramref:`inline_message_id` is not + specified. Identifier of the sent message with live location to stop. + inline_message_id (:obj:`str`, optional): Required if :paramref:`chat_id` and + :paramref:`message_id` are not specified. Identifier of the inline message. + reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): An object for a new + inline keyboard. + business_connection_id (:obj:`str`, optional): |business_id_str_edit| + + .. versionadded:: 21.4 + + Returns: + :class:`telegram.Message`: On success, if edited message is not an inline message, the + edited message is returned, otherwise :obj:`True` is returned. + """ + data: JSONDict = { + "chat_id": chat_id, + "message_id": message_id, + "inline_message_id": inline_message_id, + } + + return await self._send_message( + "stopMessageLiveLocation", + data, + reply_markup=reply_markup, + business_connection_id=business_connection_id, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def send_venue( + self, + chat_id: Union[int, str], + latitude: Optional[float] = None, + longitude: Optional[float] = None, + title: Optional[str] = None, + address: Optional[str] = None, + foursquare_id: Optional[str] = None, + disable_notification: ODVInput[bool] = DEFAULT_NONE, + reply_markup: Optional[ReplyMarkup] = None, + foursquare_type: Optional[str] = None, + google_place_id: Optional[str] = None, + google_place_type: Optional[str] = None, + protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: Optional[int] = None, + reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, + message_effect_id: Optional[str] = None, + *, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + reply_to_message_id: Optional[int] = None, + venue: Optional[Venue] = 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, + ) -> Message: + """Use this method to send information about a venue. + + Note: + * You can either supply :paramref:`venue`, or :paramref:`latitude`, + :paramref:`longitude`, :paramref:`title` and :paramref:`address` and optionally + :paramref:`foursquare_id` and :paramref:`foursquare_type` or optionally + :paramref:`google_place_id` and :paramref:`google_place_type`. + * Foursquare details and Google Place details are mutually exclusive. However, this + behaviour is undocumented and might be changed by Telegram. + + Args: + chat_id (:obj:`int` | :obj:`str`): |chat_id_channel| + latitude (:obj:`float`, optional): Latitude of venue. + longitude (:obj:`float`, optional): Longitude of venue. + title (:obj:`str`, optional): Name of the venue. + address (:obj:`str`, optional): Address of the venue. + foursquare_id (:obj:`str`, optional): Foursquare identifier of the venue. + foursquare_type (:obj:`str`, optional): Foursquare type of the venue, if known. + (For example, "arts_entertainment/default", "arts_entertainment/aquarium" or + "food/icecream".) + google_place_id (:obj:`str`, optional): Google Places identifier of the venue. + google_place_type (:obj:`str`, optional): Google Places type of the venue. (See + `supported types \ + `_.) + disable_notification (:obj:`bool`, optional): |disable_notification| + protect_content (:obj:`bool`, optional): |protect_content| + + .. versionadded:: 13.10 + message_thread_id (:obj:`int`, optional): |message_thread_id_arg| + + .. versionadded:: 20.0 + + reply_markup (:class:`InlineKeyboardMarkup` | :class:`ReplyKeyboardMarkup` | \ + :class:`ReplyKeyboardRemove` | :class:`ForceReply`, optional): + Additional interface options. An object for an inline keyboard, custom reply + keyboard, instructions to remove reply keyboard or to force a reply from the user. + reply_parameters (:class:`telegram.ReplyParameters`, optional): |reply_parameters| + + .. versionadded:: 20.8 + business_connection_id (:obj:`str`, optional): |business_id_str| + + .. versionadded:: 21.1 + message_effect_id (:obj:`str`, optional): |message_effect_id| + + .. versionadded:: 21.3 + + Keyword Args: + allow_sending_without_reply (:obj:`bool`, optional): |allow_sending_without_reply| + Mutually exclusive with :paramref:`reply_parameters`, which this is a convenience + parameter for + + .. versionchanged:: 20.8 + Bot API 7.0 introduced :paramref:`reply_parameters` |rtm_aswr_deprecated| + + .. versionchanged:: 21.0 + |keyword_only_arg| + reply_to_message_id (:obj:`int`, optional): |reply_to_msg_id| + Mutually exclusive with :paramref:`reply_parameters`, which this is a convenience + parameter for + + .. versionchanged:: 20.8 + Bot API 7.0 introduced :paramref:`reply_parameters` |rtm_aswr_deprecated| + + .. versionchanged:: 21.0 + |keyword_only_arg| + venue (:class:`telegram.Venue`, optional): The venue to send. + + Returns: + :class:`telegram.Message`: On success, the sent Message is returned. + + Raises: + :class:`telegram.error.TelegramError` + + """ + # The venue parameter is a convenience functionality added by us, so enforcing the + # mutual exclusivity here is nothing that Telegram would handle anyway + if not (venue or all([latitude, longitude, address, title])): + raise ValueError( + "Either venue or latitude, longitude, address and title must be " + "passed as arguments." + ) + if not bool(venue) ^ any([latitude, longitude, address, title]): + raise ValueError( + "Either venue or latitude, longitude, address and title must be " + "passed as arguments. Not both." + ) + + if isinstance(venue, Venue): + latitude = venue.location.latitude + longitude = venue.location.longitude + address = venue.address + title = venue.title + foursquare_id = venue.foursquare_id + foursquare_type = venue.foursquare_type + google_place_id = venue.google_place_id + google_place_type = venue.google_place_type + + data: JSONDict = { + "chat_id": chat_id, + "latitude": latitude, + "longitude": longitude, + "address": address, + "title": title, + "foursquare_id": foursquare_id, + "foursquare_type": foursquare_type, + "google_place_id": google_place_id, + "google_place_type": google_place_type, + } + + return await self._send_message( + "sendVenue", + data, + reply_to_message_id=reply_to_message_id, + disable_notification=disable_notification, + reply_markup=reply_markup, + allow_sending_without_reply=allow_sending_without_reply, + protect_content=protect_content, + message_thread_id=message_thread_id, + reply_parameters=reply_parameters, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + business_connection_id=business_connection_id, + message_effect_id=message_effect_id, + ) + + async def send_contact( + self, + chat_id: Union[int, str], + phone_number: Optional[str] = None, + first_name: Optional[str] = None, + last_name: Optional[str] = None, + disable_notification: ODVInput[bool] = DEFAULT_NONE, + reply_markup: Optional[ReplyMarkup] = None, + vcard: Optional[str] = None, + protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: Optional[int] = None, + reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, + message_effect_id: Optional[str] = None, + *, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + reply_to_message_id: Optional[int] = None, + contact: Optional[Contact] = 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, + ) -> Message: + """Use this method to send phone contacts. + + Note: + You can either supply :paramref:`contact` or :paramref:`phone_number` and + :paramref:`first_name` with optionally :paramref:`last_name` and optionally + :paramref:`vcard`. + + Args: + chat_id (:obj:`int` | :obj:`str`): |chat_id_channel| + phone_number (:obj:`str`, optional): Contact's phone number. + first_name (:obj:`str`, optional): Contact's first name. + last_name (:obj:`str`, optional): Contact's last name. + vcard (:obj:`str`, optional): Additional data about the contact in the form of a vCard, + 0-:tg-const:`telegram.constants.ContactLimit.VCARD` bytes. + disable_notification (:obj:`bool`, optional): |disable_notification| + protect_content (:obj:`bool`, optional): |protect_content| + + .. versionadded:: 13.10 + message_thread_id (:obj:`int`, optional): |message_thread_id_arg| + + .. versionadded:: 20.0 + + reply_markup (:class:`InlineKeyboardMarkup` | :class:`ReplyKeyboardMarkup` | \ + :class:`ReplyKeyboardRemove` | :class:`ForceReply`, optional): + Additional interface options. An object for an inline keyboard, custom reply + keyboard, instructions to remove reply keyboard or to force a reply from the user. + reply_parameters (:class:`telegram.ReplyParameters`, optional): |reply_parameters| + + .. versionadded:: 20.8 + business_connection_id (:obj:`str`, optional): |business_id_str| + + .. versionadded:: 21.1 + message_effect_id (:obj:`str`, optional): |message_effect_id| + + .. versionadded:: 21.3 + + Keyword Args: + allow_sending_without_reply (:obj:`bool`, optional): |allow_sending_without_reply| + Mutually exclusive with :paramref:`reply_parameters`, which this is a convenience + parameter for + + .. versionchanged:: 20.8 + Bot API 7.0 introduced :paramref:`reply_parameters` |rtm_aswr_deprecated| + + .. versionchanged:: 21.0 + |keyword_only_arg| + reply_to_message_id (:obj:`int`, optional): |reply_to_msg_id| + Mutually exclusive with :paramref:`reply_parameters`, which this is a convenience + parameter for + + .. versionchanged:: 20.8 + Bot API 7.0 introduced :paramref:`reply_parameters` |rtm_aswr_deprecated| + + .. versionchanged:: 21.0 + |keyword_only_arg| + contact (:class:`telegram.Contact`, optional): The contact to send. + + Returns: + :class:`telegram.Message`: On success, the sent Message is returned. + + Raises: + :class:`telegram.error.TelegramError` + + """ + # The contact parameter is a convenience functionality added by us, so enforcing the + # mutual exclusivity here is nothing that Telegram would handle anyway + if (not contact) and (not all([phone_number, first_name])): + raise ValueError( + "Either contact or phone_number and first_name must be passed as arguments." + ) + if not bool(contact) ^ any([phone_number, first_name]): + raise ValueError( + "Either contact or phone_number and first_name must be passed as arguments. " + "Not both." + ) + + if isinstance(contact, Contact): + phone_number = contact.phone_number + first_name = contact.first_name + last_name = contact.last_name + vcard = contact.vcard + + data: JSONDict = { + "chat_id": chat_id, + "phone_number": phone_number, + "first_name": first_name, + "last_name": last_name, + "vcard": vcard, + } + + return await self._send_message( + "sendContact", + data, + reply_to_message_id=reply_to_message_id, + disable_notification=disable_notification, + reply_markup=reply_markup, + allow_sending_without_reply=allow_sending_without_reply, + protect_content=protect_content, + message_thread_id=message_thread_id, + reply_parameters=reply_parameters, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + business_connection_id=business_connection_id, + message_effect_id=message_effect_id, + ) + + async def send_game( + self, + chat_id: int, + game_short_name: str, + disable_notification: ODVInput[bool] = DEFAULT_NONE, + reply_markup: Optional["InlineKeyboardMarkup"] = None, + protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: Optional[int] = None, + reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, + message_effect_id: Optional[str] = None, + *, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + reply_to_message_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, + ) -> Message: + """Use this method to send a game. + + Args: + chat_id (:obj:`int`): Unique identifier for the target chat. + game_short_name (:obj:`str`): Short name of the game, serves as the unique identifier + for the game. Set up your games via `@BotFather `_. + disable_notification (:obj:`bool`, optional): |disable_notification| + protect_content (:obj:`bool`, optional): |protect_content| + + .. versionadded:: 13.10 + message_thread_id (:obj:`int`, optional): |message_thread_id_arg| + + .. versionadded:: 20.0 + + reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): An object for a new + inline keyboard. If empty, one "Play game_title" button will be + shown. If not empty, the first button must launch the game. + reply_parameters (:class:`telegram.ReplyParameters`, optional): |reply_parameters| + + .. versionadded:: 20.8 + business_connection_id (:obj:`str`, optional): |business_id_str| + + .. versionadded:: 21.1 + message_effect_id (:obj:`str`, optional): |message_effect_id| + + .. versionadded:: 21.3 + + Keyword Args: + allow_sending_without_reply (:obj:`bool`, optional): |allow_sending_without_reply| + Mutually exclusive with :paramref:`reply_parameters`, which this is a convenience + parameter for + + .. versionchanged:: 20.8 + Bot API 7.0 introduced :paramref:`reply_parameters` |rtm_aswr_deprecated| + + .. versionchanged:: 21.0 + |keyword_only_arg| + reply_to_message_id (:obj:`int`, optional): |reply_to_msg_id| + Mutually exclusive with :paramref:`reply_parameters`, which this is a convenience + parameter for + + .. versionchanged:: 20.8 + Bot API 7.0 introduced :paramref:`reply_parameters` |rtm_aswr_deprecated| + + .. versionchanged:: 21.0 + |keyword_only_arg| + + Returns: + :class:`telegram.Message`: On success, the sent Message is returned. + + Raises: + :class:`telegram.error.TelegramError` + + """ + data: JSONDict = {"chat_id": chat_id, "game_short_name": game_short_name} + + return await self._send_message( + "sendGame", + data, + reply_to_message_id=reply_to_message_id, + disable_notification=disable_notification, + reply_markup=reply_markup, + allow_sending_without_reply=allow_sending_without_reply, + protect_content=protect_content, + message_thread_id=message_thread_id, + reply_parameters=reply_parameters, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + business_connection_id=business_connection_id, + message_effect_id=message_effect_id, + ) + + async def send_chat_action( + self, + chat_id: Union[str, int], + action: str, + message_thread_id: Optional[int] = None, + business_connection_id: 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: + """ + Use this method when you need to tell the user that something is happening on the bot's + side. The status is set for 5 seconds or less (when a message arrives from your bot, + Telegram clients clear its typing status). Telegram only recommends using this method when + a response from the bot will take a noticeable amount of time to arrive. + + Args: + chat_id (:obj:`int` | :obj:`str`): |chat_id_channel| + action(:obj:`str`): Type of action to broadcast. Choose one, depending on what the user + is about to receive. For convenience look at the constants in + :class:`telegram.constants.ChatAction`. + message_thread_id (:obj:`int`, optional): |message_thread_id_arg| + + .. versionadded:: 20.0 + business_connection_id (:obj:`str`, optional): |business_id_str| + + .. versionadded:: 21.1 + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + Raises: + :class:`telegram.error.TelegramError` + + """ + data: JSONDict = { + "chat_id": chat_id, + "action": action, + "message_thread_id": message_thread_id, + "business_connection_id": business_connection_id, + } + return await self._post( + "sendChatAction", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + def _effective_inline_results( + self, + results: Union[ + Sequence["InlineQueryResult"], Callable[[int], Optional[Sequence["InlineQueryResult"]]] + ], + next_offset: Optional[str] = None, + current_offset: Optional[str] = None, + ) -> Tuple[Sequence["InlineQueryResult"], Optional[str]]: + """ + Builds the effective results from the results input. + We make this a stand-alone method so tg.ext.ExtBot can wrap it. + + Returns: + Tuple of 1. the effective results and 2. correct the next_offset + + """ + if current_offset is not None and next_offset is not None: + raise ValueError("`current_offset` and `next_offset` are mutually exclusive!") + + if current_offset is not None: + # Convert the string input to integer + current_offset_int = 0 if not current_offset else int(current_offset) + + # for now set to empty string, stating that there are no more results + # might change later + next_offset = "" + + if callable(results): + callable_output = results(current_offset_int) + if not callable_output: + effective_results: Sequence[InlineQueryResult] = [] + else: + effective_results = callable_output + # the callback *might* return more results on the next call, so we increment + # the page count + next_offset = str(current_offset_int + 1) + + elif len(results) > (current_offset_int + 1) * InlineQueryLimit.RESULTS: + # we expect more results for the next page + next_offset_int = current_offset_int + 1 + next_offset = str(next_offset_int) + effective_results = results[ + current_offset_int + * InlineQueryLimit.RESULTS : next_offset_int + * InlineQueryLimit.RESULTS + ] + else: + effective_results = results[current_offset_int * InlineQueryLimit.RESULTS :] + else: + effective_results = results # type: ignore[assignment] + + return effective_results, next_offset + + @no_type_check # mypy doesn't play too well with hasattr + def _insert_defaults_for_ilq_results(self, res: "InlineQueryResult") -> "InlineQueryResult": + """The reason why this method exists is similar to the description of _insert_defaults + The reason why we do this in rather than in _insert_defaults is because converting + DEFAULT_NONE to NONE *before* calling to_dict() makes it way easier to drop None entries + from the json data. + + Must return the correct object instead of editing in-place! + """ + # Copy the objects that need modification to avoid modifying the original object + copied = False + if hasattr(res, "parse_mode"): + res = copy.copy(res) + copied = True + with res._unfrozen(): + res.parse_mode = DefaultValue.get_value(res.parse_mode) + if hasattr(res, "input_message_content") and res.input_message_content: + if hasattr(res.input_message_content, "parse_mode"): + if not copied: + res = copy.copy(res) + copied = True + + with res._unfrozen(): + res.input_message_content = copy.copy(res.input_message_content) + with res.input_message_content._unfrozen(): + res.input_message_content.parse_mode = DefaultValue.get_value( + res.input_message_content.parse_mode + ) + if hasattr(res.input_message_content, "link_preview_options"): + if not copied: + res = copy.copy(res) + + with res._unfrozen(): + res.input_message_content = copy.copy(res.input_message_content) + with res.input_message_content._unfrozen(): + res.input_message_content.link_preview_options = DefaultValue.get_value( + res.input_message_content.link_preview_options + ) + + return res + + async def answer_inline_query( + self, + inline_query_id: str, + results: Union[ + Sequence["InlineQueryResult"], Callable[[int], Optional[Sequence["InlineQueryResult"]]] + ], + cache_time: Optional[int] = None, + is_personal: Optional[bool] = None, + next_offset: Optional[str] = None, + button: Optional[InlineQueryResultsButton] = None, + *, + current_offset: 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: + """ + Use this method to send answers to an inline query. No more than + :tg-const:`telegram.InlineQuery.MAX_RESULTS` results per query are allowed. + + Warning: + In most use cases :paramref:`current_offset` should not be passed manually. Instead of + calling this method directly, use the shortcut :meth:`telegram.InlineQuery.answer` with + :paramref:`telegram.InlineQuery.answer.auto_pagination` set to :obj:`True`, which will + take care of passing the correct value. + + .. seealso:: :wiki:`Working with Files and Media ` + + + .. versionchanged:: 20.5 + Removed deprecated arguments ``switch_pm_text`` and ``switch_pm_parameter``. + + Args: + inline_query_id (:obj:`str`): Unique identifier for the answered query. + 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 + result of the inline query may be cached on the server. Defaults to ``300``. + 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. + next_offset (:obj:`str`, optional): Pass the offset that a client should send in the + next query with the same text to receive more results. Pass an empty string if + there are no more results or if you don't support pagination. Offset length can't + exceed :tg-const:`telegram.InlineQuery.MAX_OFFSET_LENGTH` bytes. + button (:class:`telegram.InlineQueryResultsButton`, optional): A button to be shown + above the inline query results. + + .. versionadded:: 20.3 + + Keyword Args: + current_offset (:obj:`str`, optional): The :attr:`telegram.InlineQuery.offset` of + the inline query to answer. If passed, PTB will automatically take care of + the pagination for you, i.e. pass the correct :paramref:`next_offset` and truncate + the results list/get the results from the callable you passed. + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + Raises: + :class:`telegram.error.TelegramError` + + """ + effective_results, next_offset = self._effective_inline_results( + results=results, next_offset=next_offset, current_offset=current_offset + ) + + # Apply defaults + effective_results = [ + self._insert_defaults_for_ilq_results(result) for result in effective_results + ] + + data: JSONDict = { + "inline_query_id": inline_query_id, + "results": effective_results, + "next_offset": next_offset, + "cache_time": cache_time, + "is_personal": is_personal, + "button": button, + } + + return await self._post( + "answerInlineQuery", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def get_user_profile_photos( + self, + user_id: int, + offset: Optional[int] = None, + limit: 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, + ) -> UserProfilePhotos: + """Use this method to get a list of profile pictures for a user. + + Args: + user_id (:obj:`int`): Unique identifier of the target user. + offset (:obj:`int`, optional): Sequential number of the first photo to be returned. + By default, all photos are returned. + limit (:obj:`int`, optional): Limits the number of photos to be retrieved. Values + between :tg-const:`telegram.constants.UserProfilePhotosLimit.MIN_LIMIT`- + :tg-const:`telegram.constants.UserProfilePhotosLimit.MAX_LIMIT` are accepted. + Defaults to ``100``. + + Returns: + :class:`telegram.UserProfilePhotos` + + Raises: + :class:`telegram.error.TelegramError` + + """ + data: JSONDict = {"user_id": user_id, "offset": offset, "limit": limit} + + result = await self._post( + "getUserProfilePhotos", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + return UserProfilePhotos.de_json(result, self) # type: ignore[return-value] + + async def get_file( + self, + file_id: Union[ + str, Animation, Audio, ChatPhoto, Document, PhotoSize, Sticker, Video, VideoNote, Voice + ], + *, + 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, + ) -> File: + """ + Use this method to get basic info about a file and prepare it for downloading. For the + moment, bots can download files of up to + :tg-const:`telegram.constants.FileSizeLimit.FILESIZE_DOWNLOAD` in size. The file can then + be e.g. downloaded with :meth:`telegram.File.download_to_drive`. It is guaranteed that + the link will be valid for at least 1 hour. When the link expires, a new one can be + requested by calling get_file again. + + Note: + This function may not preserve the original file name and MIME type. + You should save the file's MIME type and name (if available) when the File object + is received. + + .. seealso:: :wiki:`Working with Files and Media ` + + Args: + file_id (:obj:`str` | :class:`telegram.Animation` | :class:`telegram.Audio` | \ + :class:`telegram.ChatPhoto` | :class:`telegram.Document` | \ + :class:`telegram.PhotoSize` | :class:`telegram.Sticker` | \ + :class:`telegram.Video` | :class:`telegram.VideoNote` | \ + :class:`telegram.Voice`): + Either the file identifier or an object that has a file_id attribute + to get file information about. + + Returns: + :class:`telegram.File` + + Raises: + :class:`telegram.error.TelegramError` + + """ + # Try to get the file_id from the object, if it fails, assume it's a string + with contextlib.suppress(AttributeError): + file_id = file_id.file_id # type: ignore[union-attr] + + data: JSONDict = {"file_id": file_id} + + result = await self._post( + "getFile", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + file_path = cast(dict, result).get("file_path") + 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] + + async def ban_chat_member( + self, + chat_id: Union[str, int], + user_id: int, + until_date: Optional[Union[int, datetime]] = None, + revoke_messages: 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: + """ + Use this method to ban a user from a group, supergroup or a channel. In the case of + supergroups and channels, the user will not be able to return to the group on their own + using invite links, etc., unless unbanned first. The bot must be an administrator in the + chat for this to work and must have the appropriate admin rights. + + .. versionadded:: 13.7 + + Args: + chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target group or username + of the target supergroup or channel (in the format ``@channelusername``). + user_id (:obj:`int`): Unique identifier of the target user. + until_date (:obj:`int` | :obj:`datetime.datetime`, optional): Date when the user will + 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. + 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. + Always :obj:`True` for supergroups and channels. + + .. versionadded:: 13.4 + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + Raises: + :class:`telegram.error.TelegramError` + + """ + data: JSONDict = { + "chat_id": chat_id, + "user_id": user_id, + "revoke_messages": revoke_messages, + "until_date": until_date, + } + + return await self._post( + "banChatMember", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def ban_chat_sender_chat( + self, + chat_id: Union[str, int], + sender_chat_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: + """ + Use this method to ban a channel chat in a supergroup or a channel. Until the chat is + unbanned, the owner of the banned chat won't be able to send messages on behalf of **any of + their channels**. The bot must be an administrator in the supergroup or channel for this + to work and must have the appropriate administrator rights. + + .. versionadded:: 13.9 + + Args: + chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target group or username + of the target supergroup or channel (in the format ``@channelusername``). + sender_chat_id (:obj:`int`): Unique identifier of the target sender chat. + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + Raises: + :class:`telegram.error.TelegramError` + + """ + data: JSONDict = {"chat_id": chat_id, "sender_chat_id": sender_chat_id} + + return await self._post( + "banChatSenderChat", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def unban_chat_member( + self, + chat_id: Union[str, int], + user_id: int, + only_if_banned: 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: + """Use this method to unban a previously kicked user in a supergroup or channel. + + The user will *not* return to the group or channel automatically, but will be able to join + via link, etc. The bot must be an administrator for this to work. By default, this method + guarantees that after the call the user is not a member of the chat, but will be able to + join it. So if the user is a member of the chat they will also be *removed* from the chat. + If you don't want this, use the parameter :paramref:`only_if_banned`. + + Args: + chat_id (:obj:`int` | :obj:`str`): |chat_id_channel| + user_id (:obj:`int`): Unique identifier of the target user. + only_if_banned (:obj:`bool`, optional): Do nothing if the user is not banned. + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + Raises: + :class:`telegram.error.TelegramError` + + """ + data: JSONDict = {"chat_id": chat_id, "user_id": user_id, "only_if_banned": only_if_banned} + + return await self._post( + "unbanChatMember", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def unban_chat_sender_chat( + self, + chat_id: Union[str, int], + sender_chat_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: + """Use this method to unban a previously banned channel in a supergroup or channel. + The bot must be an administrator for this to work and must have the + appropriate administrator rights. + + .. versionadded:: 13.9 + + Args: + chat_id (:obj:`int` | :obj:`str`): |chat_id_channel| + sender_chat_id (:obj:`int`): Unique identifier of the target sender chat. + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + Raises: + :class:`telegram.error.TelegramError` + + """ + data: JSONDict = {"chat_id": chat_id, "sender_chat_id": sender_chat_id} + + return await self._post( + "unbanChatSenderChat", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def answer_callback_query( + self, + callback_query_id: str, + text: Optional[str] = None, + show_alert: Optional[bool] = None, + url: Optional[str] = None, + cache_time: 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: + """ + Use this method to send answers to callback queries sent from inline keyboards. The answer + will be displayed to the user as a notification at the top of the chat screen or as an + alert. + Alternatively, the user can be redirected to the specified Game URL. For this option to + work, you must first create a game for your bot via `@BotFather `_ + and accept the terms. Otherwise, you may use links like t.me/your_bot?start=XXXX that open + your bot with a parameter. + + Args: + callback_query_id (:obj:`str`): Unique identifier for the query to be answered. + text (:obj:`str`, optional): Text of the notification. If not specified, nothing will + be shown to the user, 0-:tg-const:`telegram.CallbackQuery.MAX_ANSWER_TEXT_LENGTH` + characters. + show_alert (:obj:`bool`, optional): If :obj:`True`, an alert will be shown by the + client instead of a notification at the top of the chat screen. Defaults to + :obj:`False`. + url (:obj:`str`, optional): URL that will be opened by the user's client. If you have + created a Game and accepted the conditions via + `@BotFather `_, specify the URL that + 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 + result of the callback query may be cached client-side. Defaults to 0. + + Returns: + :obj:`bool` On success, :obj:`True` is returned. + + Raises: + :class:`telegram.error.TelegramError` + + """ + data: JSONDict = { + "callback_query_id": callback_query_id, + "cache_time": cache_time, + "text": text, + "show_alert": show_alert, + "url": url, + } + + return await self._post( + "answerCallbackQuery", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def edit_message_text( + self, + text: str, + chat_id: Optional[Union[str, int]] = None, + message_id: Optional[int] = None, + inline_message_id: Optional[str] = None, + parse_mode: ODVInput[str] = DEFAULT_NONE, + reply_markup: Optional["InlineKeyboardMarkup"] = None, + entities: Optional[Sequence["MessageEntity"]] = None, + link_preview_options: ODVInput["LinkPreviewOptions"] = DEFAULT_NONE, + business_connection_id: Optional[str] = None, + *, + disable_web_page_preview: 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, + ) -> Union[Message, bool]: + """ + Use this method to edit text and game messages. + + Note: + * |editreplymarkup| + * |bcid_edit_time| + + .. seealso:: :attr:`telegram.Game.text` + + Args: + chat_id (:obj:`int` | :obj:`str`, optional): Required if :paramref:`inline_message_id` + is not specified. |chat_id_channel| + message_id (:obj:`int`, optional): Required if :paramref:`inline_message_id` is not + specified. Identifier of the message to edit. + inline_message_id (:obj:`str`, optional): Required if :paramref:`chat_id` and + :paramref:`message_id` are not specified. Identifier of the inline message. + text (:obj:`str`): New text of the message, + :tg-const:`telegram.constants.MessageLimit.MIN_TEXT_LENGTH`- + :tg-const:`telegram.constants.MessageLimit.MAX_TEXT_LENGTH` characters after + entities parsing. + parse_mode (:obj:`str`, optional): |parse_mode| + entities (Sequence[:class:`telegram.MessageEntity`], optional): Sequence of special + entities that appear in message text, which can be specified instead of + :paramref:`parse_mode`. + + .. versionchanged:: 20.0 + |sequenceargs| + + link_preview_options (:obj:`LinkPreviewOptions`, optional): Link preview generation + options for the message. Mutually exclusive with + :paramref:`disable_web_page_preview`. + + .. versionadded:: 20.8 + + reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): An object for an + inline keyboard. + business_connection_id (:obj:`str`, optional): |business_id_str_edit| + + .. versionadded:: 21.4 + + Keyword Args: + disable_web_page_preview (:obj:`bool`, optional): Disables link previews for links in + this message. Convenience parameter for setting :paramref:`link_preview_options`. + Mutually exclusive with :paramref:`link_preview_options`. + + .. versionchanged:: 20.8 + Bot API 7.0 introduced :paramref:`link_preview_options` replacing this + argument. PTB will automatically convert this argument to that one, but + for advanced options, please use :paramref:`link_preview_options` directly. + + .. versionchanged:: 21.0 + |keyword_only_arg| + + + Returns: + :class:`telegram.Message`: On success, if edited message is not an inline message, the + edited message is returned, otherwise :obj:`True` is returned. + + Raises: + :exc:`ValueError`: If both :paramref:`disable_web_page_preview` and + :paramref:`link_preview_options` are passed. + :class:`telegram.error.TelegramError`: For other errors. + + """ + data: JSONDict = { + "text": text, + "chat_id": chat_id, + "message_id": message_id, + "inline_message_id": inline_message_id, + "entities": entities, + } + + link_preview_options = parse_lpo_and_dwpp(disable_web_page_preview, link_preview_options) + + return await self._send_message( + "editMessageText", + data, + reply_markup=reply_markup, + parse_mode=parse_mode, + link_preview_options=link_preview_options, + business_connection_id=business_connection_id, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def edit_message_caption( + self, + chat_id: Optional[Union[str, int]] = None, + message_id: Optional[int] = None, + inline_message_id: Optional[str] = None, + caption: Optional[str] = None, + reply_markup: Optional["InlineKeyboardMarkup"] = None, + parse_mode: ODVInput[str] = DEFAULT_NONE, + caption_entities: Optional[Sequence["MessageEntity"]] = None, + show_caption_above_media: Optional[bool] = None, + business_connection_id: 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, + ) -> Union[Message, bool]: + """ + Use this method to edit captions of messages. + + Note: + * |editreplymarkup| + * |bcid_edit_time| + + Args: + chat_id (:obj:`int` | :obj:`str`, optional): Required if inline_message_id is not + specified. |chat_id_channel| + message_id (:obj:`int`, optional): Required if inline_message_id is not specified. + Identifier of the message to edit. + inline_message_id (:obj:`str`, optional): Required if chat_id and message_id are not + specified. Identifier of the inline message. + caption (:obj:`str`, optional): New caption of the message, + 0-:tg-const:`telegram.constants.MessageLimit.CAPTION_LENGTH` characters after + entities parsing. + parse_mode (:obj:`str`, optional): |parse_mode| + caption_entities (Sequence[:class:`telegram.MessageEntity`], optional): + |caption_entities| + + .. versionchanged:: 20.0 + |sequenceargs| + reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): An object for an + inline keyboard. + show_caption_above_media (:obj:`bool`, optional): Pass |show_cap_above_med| + + .. versionadded:: 21.3 + business_connection_id (:obj:`str`, optional): |business_id_str_edit| + + .. versionadded:: 21.4 + + Returns: + :class:`telegram.Message`: On success, if edited message is not an inline message, the + edited message is returned, otherwise :obj:`True` is returned. + + Raises: + :class:`telegram.error.TelegramError` + + """ + data: JSONDict = { + "chat_id": chat_id, + "message_id": message_id, + "inline_message_id": inline_message_id, + "show_caption_above_media": show_caption_above_media, + } + + return await self._send_message( + "editMessageCaption", + data, + reply_markup=reply_markup, + caption=caption, + parse_mode=parse_mode, + caption_entities=caption_entities, + business_connection_id=business_connection_id, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def edit_message_media( + self, + media: "InputMedia", + chat_id: Optional[Union[str, int]] = None, + message_id: Optional[int] = None, + inline_message_id: Optional[str] = None, + reply_markup: Optional["InlineKeyboardMarkup"] = None, + business_connection_id: 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, + ) -> Union[Message, bool]: + """ + Use this method to edit animation, audio, document, photo, or video 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 + :attr:`~telegram.File.file_id` or specify a URL. + + Note: + * |editreplymarkup| + * |bcid_edit_time| + + .. seealso:: :wiki:`Working with Files and Media ` + + Args: + media (:class:`telegram.InputMedia`): An object for a new media content + of the message. + chat_id (:obj:`int` | :obj:`str`, optional): Required if inline_message_id is not + specified. |chat_id_channel| + message_id (:obj:`int`, optional): Required if inline_message_id is not specified. + Identifier of the message to edit. + inline_message_id (:obj:`str`, optional): Required if chat_id and message_id are not + specified. Identifier of the inline message. + reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): An object for an + inline keyboard. + business_connection_id (:obj:`str`, optional): |business_id_str_edit| + + .. versionadded:: 21.4 + + Returns: + :class:`telegram.Message`: On success, if edited message is not an inline message, the + edited Message is returned, otherwise :obj:`True` is returned. + + Raises: + :class:`telegram.error.TelegramError` + """ + data: JSONDict = { + "media": media, + "chat_id": chat_id, + "message_id": message_id, + "inline_message_id": inline_message_id, + } + + return await self._send_message( + "editMessageMedia", + data, + reply_markup=reply_markup, + business_connection_id=business_connection_id, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def edit_message_reply_markup( + self, + chat_id: Optional[Union[str, int]] = None, + message_id: Optional[int] = None, + inline_message_id: Optional[str] = None, + reply_markup: Optional["InlineKeyboardMarkup"] = None, + business_connection_id: 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, + ) -> Union[Message, bool]: + """ + Use this method to edit only the reply markup of messages sent by the bot or via the bot + (for inline bots). + + Note: + * |editreplymarkup| + * |bcid_edit_time| + + Args: + chat_id (:obj:`int` | :obj:`str`, optional): Required if inline_message_id is not + specified. |chat_id_channel| + message_id (:obj:`int`, optional): Required if inline_message_id is not specified. + Identifier of the message to edit. + inline_message_id (:obj:`str`, optional): Required if chat_id and message_id are not + specified. Identifier of the inline message. + reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): An object for an + inline keyboard. + business_connection_id (:obj:`str`, optional): |business_id_str_edit| + + .. versionadded:: 21.4 + + Returns: + :class:`telegram.Message`: On success, if edited message is not an inline message, the + edited message is returned, otherwise :obj:`True` is returned. + + Raises: + :class:`telegram.error.TelegramError` + + """ + data: JSONDict = { + "chat_id": chat_id, + "message_id": message_id, + "inline_message_id": inline_message_id, + } + + return await self._send_message( + "editMessageReplyMarkup", + data, + reply_markup=reply_markup, + business_connection_id=business_connection_id, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def get_updates( + self, + offset: Optional[int] = None, + limit: Optional[int] = None, + timeout: Optional[int] = None, # noqa: ASYNC109 + allowed_updates: Optional[Sequence[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, + ) -> Tuple[Update, ...]: + """Use this method to receive incoming updates using long polling. + + Note: + 1. This method will not work if an outgoing webhook is set up. + 2. In order to avoid getting duplicate updates, recalculate offset after each + server response. + 3. To take full advantage of this library take a look at :class:`telegram.ext.Updater` + + .. seealso:: :meth:`telegram.ext.Application.run_polling`, + :meth:`telegram.ext.Updater.start_polling` + + .. versionchanged:: 20.0 + Returns a tuple instead of a list. + + Args: + offset (:obj:`int`, optional): Identifier of the first update to be returned. Must be + greater by one than the highest among the identifiers of previously received + updates. By default, updates starting with the earliest unconfirmed update are + returned. An update is considered confirmed as soon as this method is called with + an offset higher than its :attr:`telegram.Update.update_id`. The negative offset + can be specified to retrieve updates starting from -offset update from the end of + the updates queue. All previous updates will be forgotten. + limit (:obj:`int`, optional): Limits the number of updates to be retrieved. Values + between :tg-const:`telegram.constants.PollingLimit.MIN_LIMIT`- + :tg-const:`telegram.constants.PollingLimit.MAX_LIMIT` are accepted. + Defaults to ``100``. + timeout (:obj:`int`, optional): Timeout in seconds for long polling. Defaults to ``0``, + i.e. usual short polling. Should be positive, short polling should be used for + testing purposes only. + allowed_updates (Sequence[:obj:`str`]), optional): A sequence the types of + updates you want your bot to receive. For example, specify ["message", + "edited_channel_post", "callback_query"] to only receive updates of these types. + See :class:`telegram.Update` for a complete list of available update types. + Specify an empty sequence to receive all updates except + :attr:`telegram.Update.chat_member`, :attr:`telegram.Update.message_reaction` and + :attr:`telegram.Update.message_reaction_count` (default). If not specified, the + previous setting will be used. Please note that this parameter doesn't affect + updates created before the call to the get_updates, so unwanted updates may be + received for a short period of time. + + .. versionchanged:: 20.0 + |sequenceargs| + + Returns: + Tuple[:class:`telegram.Update`] + + Raises: + :class:`telegram.error.TelegramError` + + """ + data: JSONDict = { + "timeout": timeout, + "offset": offset, + "limit": limit, + "allowed_updates": allowed_updates, + } + + # The "or 0" is needed for the case where read_timeout is None. + 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, + ) + + # Ideally we'd use an aggressive read timeout for the polling. However, + # * Short polling should return within 2 seconds. + # * Long polling poses a different problem: the connection might have been dropped while + # waiting for the server to return and there's no way of knowing the connection had been + # dropped in real time. + result = cast( + List[JSONDict], + await self._post( + "getUpdates", + data, + read_timeout=arg_read_timeout + timeout if timeout else arg_read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ), + ) + + if result: + self._LOGGER.debug("Getting updates: %s", [u["update_id"] for u in result]) + else: + self._LOGGER.debug("No new updates found.") + + try: + return Update.de_list(result, self) + except Exception as exc: + # This logging is in place mostly b/c we can't access the raw json data in Updater, + # where the exception is caught and logged again. Still, it might also be beneficial + # for custom usages of `get_updates`. + self._LOGGER.critical( + "Error while parsing updates! Received data was %r", result, exc_info=exc + ) + raise + + async def set_webhook( + self, + url: str, + certificate: Optional[FileInput] = None, + max_connections: Optional[int] = None, + allowed_updates: Optional[Sequence[str]] = None, + ip_address: Optional[str] = None, + drop_pending_updates: Optional[bool] = None, + secret_token: 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: + """ + 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. + + 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 + ``X-Telegram-Bot-Api-Secret-Token`` with the secret token as content. + + 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 :paramref:`certificate` parameter. Please upload as + :class:`~telegram.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` + + Examples: + :any:`Custom Webhook Bot ` + + Args: + url (:obj:`str`): HTTPS url to send updates to. Use an empty string to remove webhook + integration. + certificate (:term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | :obj:`str`): + Upload your public key certificate so that the root + certificate in use can be checked. See our :wiki:`self-signed guide\ + ` for details. + |uploadinputnopath| + ip_address (:obj:`str`, optional): The fixed IP address which will be used to send + webhook requests instead of the IP address resolved through DNS. + max_connections (:obj:`int`, optional): Maximum allowed number of simultaneous HTTPS + connections to the webhook for update delivery, + :tg-const:`telegram.constants.WebhookLimit.MIN_CONNECTIONS_LIMIT`- + :tg-const:`telegram.constants.WebhookLimit.MAX_CONNECTIONS_LIMIT`. + Defaults to ``40``. Use lower values to limit the load on your bot's server, + and higher values to increase your bot's throughput. + allowed_updates (Sequence[:obj:`str`], optional): A sequence of the types of + updates you want your bot to receive. For example, specify ["message", + "edited_channel_post", "callback_query"] to only receive updates of these types. + See :class:`telegram.Update` for a complete list of available update types. + Specify an empty sequence to receive all updates except + :attr:`telegram.Update.chat_member`, + :attr:`telegram.Update.message_reaction` + and :attr:`telegram.Update.message_reaction_count` (default). If not + specified, the previous setting will be used. Please note that this + parameter doesn't affect + updates created before the call to the set_webhook, so unwanted update + may be received for a short period of time. + + .. versionchanged:: 20.0 + |sequenceargs| + drop_pending_updates (:obj:`bool`, optional): Pass :obj:`True` to drop all pending + updates. + secret_token (:obj:`str`, optional): A secret token to be sent in a header + ``X-Telegram-Bot-Api-Secret-Token`` in every webhook request, + :tg-const:`telegram.constants.WebhookLimit.MIN_SECRET_TOKEN_LENGTH`- + :tg-const:`telegram.constants.WebhookLimit.MAX_SECRET_TOKEN_LENGTH` characters. + Only characters ``A-Z``, ``a-z``, ``0-9``, ``_`` and ``-`` are allowed. + The header is useful to ensure that the request comes from a webhook set by you. + + .. versionadded:: 20.0 + + Returns: + :obj:`bool` On success, :obj:`True` is returned. + + Raises: + :class:`telegram.error.TelegramError` + + .. _`guide to Webhooks`: https://core.telegram.org/bots/webhooks + + """ + data: JSONDict = { + "url": url, + "max_connections": max_connections, + "allowed_updates": allowed_updates, + "ip_address": ip_address, + "drop_pending_updates": drop_pending_updates, + "secret_token": secret_token, + "certificate": self._parse_file_input(certificate), # type: ignore[arg-type] + } + + return await self._post( + "setWebhook", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def delete_webhook( + self, + drop_pending_updates: 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: + """ + Use this method to remove webhook integration if you decide to switch back to + :meth:`get_updates()`. + + Args: + drop_pending_updates (:obj:`bool`, optional): Pass :obj:`True` to drop all pending + updates. + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + Raises: + :class:`telegram.error.TelegramError` + + """ + data = {"drop_pending_updates": drop_pending_updates} + + return await self._post( + "deleteWebhook", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def leave_chat( + self, + chat_id: Union[str, 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: + """Use this method for your bot to leave a group, supergroup or channel. + + 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( + "leaveChat", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def get_chat( + self, + chat_id: Union[str, 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, + ) -> ChatFullInfo: + """ + Use this method to get up to date information about the chat (current name of the user for + one-on-one conversations, current username of a user, group or channel, etc.). + + .. versionchanged:: 21.2 + In accordance to Bot API 7.3, this method now returns a :class:`telegram.ChatFullInfo`. + + Args: + chat_id (:obj:`int` | :obj:`str`): |chat_id_channel| + + Returns: + :class:`telegram.ChatFullInfo` + + Raises: + :class:`telegram.error.TelegramError` + + """ + data: JSONDict = {"chat_id": chat_id} + + result = await self._post( + "getChat", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + return ChatFullInfo.de_json(result, self) # type: ignore[return-value] + + async def get_chat_administrators( + self, + chat_id: Union[str, 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, + ) -> Tuple[ChatMember, ...]: + """ + Use this method to get a list of administrators in a chat. + + .. versionchanged:: 20.0 + Returns a tuple instead of a list. + + Args: + chat_id (:obj:`int` | :obj:`str`): |chat_id_channel| + + Returns: + 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. + + Raises: + :class:`telegram.error.TelegramError` + + """ + data: JSONDict = {"chat_id": chat_id} + result = await self._post( + "getChatAdministrators", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + return ChatMember.de_list(result, self) + + async def get_chat_member_count( + self, + chat_id: Union[str, 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, + ) -> int: + """Use this method to get the number of members in a chat. + + .. versionadded:: 13.7 + + Args: + chat_id (:obj:`int` | :obj:`str`): |chat_id_channel| + + Returns: + :obj:`int`: Number of members in the chat. + + Raises: + :class:`telegram.error.TelegramError` + + """ + data: JSONDict = {"chat_id": chat_id} + return await self._post( + "getChatMemberCount", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def get_chat_member( + self, + chat_id: Union[str, int], + 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, + ) -> ChatMember: + """Use this method to get information about a member of a chat. The method is only + guaranteed to work for other users if the bot is an administrator in the chat. + + Args: + chat_id (:obj:`int` | :obj:`str`): |chat_id_channel| + user_id (:obj:`int`): Unique identifier of the target user. + + Returns: + :class:`telegram.ChatMember` + + Raises: + :class:`telegram.error.TelegramError` + + """ + data: JSONDict = {"chat_id": chat_id, "user_id": user_id} + result = await self._post( + "getChatMember", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + return ChatMember.de_json(result, self) # type: ignore[return-value] + + async def set_chat_sticker_set( + self, + chat_id: Union[str, int], + sticker_set_name: 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: + """Use this method to set a new group sticker set for a supergroup. + The bot must be an administrator in the chat for this to work and must have the appropriate + admin rights. Use the field :attr:`telegram.ChatFullInfo.can_set_sticker_set` optionally + returned in :meth:`get_chat` requests to check if the bot can use this method. + + Args: + chat_id (:obj:`int` | :obj:`str`): |chat_id_group| + sticker_set_name (:obj:`str`): Name of the sticker set to be set as the group + sticker set. + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + """ + data: JSONDict = {"chat_id": chat_id, "sticker_set_name": sticker_set_name} + return await self._post( + "setChatStickerSet", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def delete_chat_sticker_set( + self, + chat_id: Union[str, 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: + """Use this method to delete a group sticker set from a supergroup. The bot must be an + administrator in the chat for this to work and must have the appropriate admin rights. + Use the field :attr:`telegram.ChatFullInfo.can_set_sticker_set` optionally returned in + :meth:`get_chat` requests to check if the bot can use this method. + + Args: + chat_id (:obj:`int` | :obj:`str`): |chat_id_group| + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + """ + data: JSONDict = {"chat_id": chat_id} + return await self._post( + "deleteChatStickerSet", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def get_webhook_info( + 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, + ) -> WebhookInfo: + """Use this method to get current webhook status. Requires no parameters. + + If the bot is using :meth:`get_updates`, will return an object with the + :attr:`telegram.WebhookInfo.url` field empty. + + Returns: + :class:`telegram.WebhookInfo` + + """ + result = await self._post( + "getWebhookInfo", + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + return WebhookInfo.de_json(result, self) # type: ignore[return-value] + + async def set_game_score( + self, + user_id: int, + score: int, + chat_id: Optional[int] = None, + message_id: Optional[int] = None, + inline_message_id: Optional[str] = None, + force: Optional[bool] = None, + disable_edit_message: 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, + ) -> Union[Message, bool]: + """ + Use this method to set the score of the specified user in a game message. + + .. seealso:: :attr:`telegram.Game.text` + + Args: + user_id (:obj:`int`): User identifier. + score (:obj:`int`): New score, must be non-negative. + force (:obj:`bool`, optional): Pass :obj:`True`, if the high score is allowed to + decrease. This can be useful when fixing mistakes or banning cheaters. + disable_edit_message (:obj:`bool`, optional): Pass :obj:`True`, if the game message + should not be automatically edited to include the current scoreboard. + chat_id (:obj:`int`, optional): Required if :paramref:`inline_message_id` + is not specified. Unique identifier for the target chat. + message_id (:obj:`int`, optional): Required if :paramref:`inline_message_id` is not + specified. Identifier of the sent message. + inline_message_id (:obj:`str`, optional): Required if :paramref:`chat_id` and + :paramref:`message_id` are not specified. Identifier of the inline message. + + Returns: + :class:`telegram.Message`: The edited message. If the message is not an inline message + , :obj:`True`. + + Raises: + :class:`telegram.error.TelegramError`: If the new score is not greater than the user's + current score in the chat and :paramref:`force` is :obj:`False`. + + """ + data: JSONDict = { + "user_id": user_id, + "score": score, + "force": force, + "disable_edit_message": disable_edit_message, + "chat_id": chat_id, + "message_id": message_id, + "inline_message_id": inline_message_id, + } + + return await self._send_message( + "setGameScore", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def get_game_high_scores( + self, + user_id: int, + chat_id: Optional[int] = None, + message_id: Optional[int] = None, + inline_message_id: 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, + ) -> 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. + + Note: + This method will currently return scores for the target user, plus two of their + closest neighbors on each side. Will also return the top three users if the user and + his neighbors are not among them. Please note that this behavior is subject to change. + + .. versionchanged:: 20.0 + Returns a tuple instead of a list. + + Args: + user_id (:obj:`int`): Target user id. + chat_id (:obj:`int`, optional): Required if :paramref:`inline_message_id` + is not specified. Unique identifier for the target chat. + message_id (:obj:`int`, optional): Required if :paramref:`inline_message_id` is not + specified. Identifier of the sent message. + inline_message_id (:obj:`str`, optional): Required if :paramref:`chat_id` and + :paramref:`message_id` are not specified. Identifier of the inline message. + + Returns: + Tuple[:class:`telegram.GameHighScore`] + + Raises: + :class:`telegram.error.TelegramError` + + """ + data: JSONDict = { + "user_id": user_id, + "chat_id": chat_id, + "message_id": message_id, + "inline_message_id": inline_message_id, + } + + result = await self._post( + "getGameHighScores", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + return GameHighScore.de_list(result, self) + + async def send_invoice( + self, + chat_id: Union[int, str], + 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"], + start_parameter: Optional[str] = None, + photo_url: Optional[str] = None, + photo_size: Optional[int] = None, + photo_width: Optional[int] = None, + photo_height: Optional[int] = None, + need_name: Optional[bool] = None, + need_phone_number: Optional[bool] = None, + need_email: Optional[bool] = None, + need_shipping_address: Optional[bool] = None, + is_flexible: Optional[bool] = None, + disable_notification: ODVInput[bool] = DEFAULT_NONE, + reply_markup: Optional["InlineKeyboardMarkup"] = None, + provider_data: Optional[Union[str, object]] = None, + send_phone_number_to_provider: Optional[bool] = None, + send_email_to_provider: Optional[bool] = None, + max_tip_amount: Optional[int] = None, + suggested_tip_amounts: Optional[Sequence[int]] = None, + protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: Optional[int] = None, + reply_parameters: Optional["ReplyParameters"] = None, + message_effect_id: Optional[str] = None, + *, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + reply_to_message_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, + ) -> Message: + """Use this method to send invoices. + + Warning: + As of API 5.2 :paramref:`start_parameter` is an optional argument and therefore the + order of the arguments had to be changed. Use keyword arguments to make sure that the + arguments are passed correctly. + + .. versionchanged:: 13.5 + As of Bot API 5.2, the parameter :paramref:`start_parameter` is optional. + + Args: + chat_id (:obj:`int` | :obj:`str`): |chat_id_channel| + 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. + :tg-const:`telegram.Invoice.MIN_DESCRIPTION_LENGTH`- + :tg-const:`telegram.Invoice.MAX_DESCRIPTION_LENGTH` characters. + 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 it for your internal processes. + provider_token (:obj:`str`): 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. + + currency (:obj:`str`): Three-letter ISO 4217 currency code, see `more on currencies + `_. Pass ``XTR`` for + payment in |tg_stars|. + prices (Sequence[:class:`telegram.LabeledPrice`]): Price breakdown, a sequence + of components (e.g. product price, tax, discount, delivery cost, delivery tax, + bonus, etc.). Must contain exactly one item for payment in |tg_stars|. + + .. versionchanged:: 20.0 + |sequenceargs| + 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`` + parameter in `currencies.json + `_, it shows the number of + digits past the decimal point for each currency (2 for the majority of currencies). + Defaults to ``0``. Not supported for payment in |tg_stars|. + + .. versionadded:: 13.5 + suggested_tip_amounts (Sequence[:obj:`int`], optional): An array of + suggested amounts of tips in the *smallest units* of the currency (integer, **not** + float/double). At most :tg-const:`telegram.Invoice.MAX_TIP_AMOUNTS` suggested tip + amounts can be specified. The suggested tip amounts must be positive, passed in a + strictly increased order and must not exceed :paramref:`max_tip_amount`. + + .. versionadded:: 13.5 + + .. versionchanged:: 20.0 + |sequenceargs| + start_parameter (:obj:`str`, optional): Unique deep-linking parameter. If left empty, + *forwarded copies* of the sent message will have a *Pay* button, allowing + multiple users to pay directly from the forwarded message, using the same invoice. + If non-empty, forwarded copies of the sent message will have a *URL* button with a + deep link to the bot (instead of a *Pay* button), with the value used as the + start parameter. + + .. versionchanged:: 13.5 + As of Bot API 5.2, this parameter is optional. + provider_data (:obj:`str` | :obj:`object`, optional): data about the + invoice, which will be shared with the payment provider. A detailed description of + required fields should be provided by the payment provider. When an object is + passed, it will be encoded as JSON. + photo_url (:obj:`str`, optional): URL of the product photo for the invoice. Can be a + photo of the goods or a marketing image for a service. People like it better when + they see what they are paying for. + photo_size (:obj:`str`, optional): Photo size. + photo_width (:obj:`int`, optional): Photo width. + photo_height (:obj:`int`, optional): Photo height. + need_name (:obj:`bool`, optional): Pass :obj:`True`, if you require the user's full + name to complete the order. Ignored for payments in |tg_stars|. + need_phone_number (:obj:`bool`, optional): Pass :obj:`True`, if you require the user's + phone number to complete the order. Ignored for payments in |tg_stars|. + need_email (:obj:`bool`, optional): Pass :obj:`True`, if you require the user's email + to complete the order. Ignored for payments in |tg_stars|. + need_shipping_address (:obj:`bool`, optional): Pass :obj:`True`, if you require the + user's shipping address to complete the order. Ignored for payments in + |tg_stars|. + send_phone_number_to_provider (:obj:`bool`, optional): Pass :obj:`True`, if user's + phone number should be sent to provider. Ignored for payments in |tg_stars|. + send_email_to_provider (:obj:`bool`, optional): Pass :obj:`True`, if user's email + address should be sent to provider. Ignored for payments in |tg_stars|. + is_flexible (:obj:`bool`, optional): Pass :obj:`True`, if the final price depends on + the shipping method. Ignored for payments in |tg_stars|. + disable_notification (:obj:`bool`, optional): |disable_notification| + protect_content (:obj:`bool`, optional): |protect_content| + + .. versionadded:: 13.10 + message_thread_id (:obj:`int`, optional): |message_thread_id_arg| + + .. versionadded:: 20.0 + + reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): An object for an + inline keyboard. If empty, one 'Pay total price' button will be + shown. If not empty, the first button must be a Pay button. + reply_parameters (:class:`telegram.ReplyParameters`, optional): |reply_parameters| + + .. versionadded:: 20.8 + message_effect_id (:obj:`str`, optional): |message_effect_id| + + .. versionadded:: 21.3 + + Keyword Args: + allow_sending_without_reply (:obj:`bool`, optional): |allow_sending_without_reply| + Mutually exclusive with :paramref:`reply_parameters`, which this is a convenience + parameter for + + .. versionchanged:: 20.8 + Bot API 7.0 introduced :paramref:`reply_parameters` |rtm_aswr_deprecated| + + .. versionchanged:: 21.0 + |keyword_only_arg| + reply_to_message_id (:obj:`int`, optional): |reply_to_msg_id| + Mutually exclusive with :paramref:`reply_parameters`, which this is a convenience + parameter for + + .. versionchanged:: 20.8 + Bot API 7.0 introduced :paramref:`reply_parameters` |rtm_aswr_deprecated| + + .. versionchanged:: 21.0 + |keyword_only_arg| + + Returns: + :class:`telegram.Message`: On success, the sent Message is returned. + + Raises: + :class:`telegram.error.TelegramError` + + """ + data: JSONDict = { + "chat_id": chat_id, + "title": title, + "description": description, + "payload": payload, + "provider_token": provider_token, + "currency": currency, + "prices": prices, + "max_tip_amount": max_tip_amount, + "suggested_tip_amounts": suggested_tip_amounts, + "start_parameter": start_parameter, + "provider_data": provider_data, + "photo_url": photo_url, + "photo_size": photo_size, + "photo_width": photo_width, + "photo_height": photo_height, + "need_name": need_name, + "need_phone_number": need_phone_number, + "need_email": need_email, + "need_shipping_address": need_shipping_address, + "is_flexible": is_flexible, + "send_phone_number_to_provider": send_phone_number_to_provider, + "send_email_to_provider": send_email_to_provider, + } + + return await self._send_message( + "sendInvoice", + data, + reply_to_message_id=reply_to_message_id, + disable_notification=disable_notification, + reply_markup=reply_markup, + allow_sending_without_reply=allow_sending_without_reply, + protect_content=protect_content, + message_thread_id=message_thread_id, + reply_parameters=reply_parameters, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + message_effect_id=message_effect_id, + ) + + async def answer_shipping_query( + self, + shipping_query_id: str, + ok: bool, + shipping_options: Optional[Sequence["ShippingOption"]] = None, + error_message: 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: + """ + If you sent an invoice requesting a shipping address and the parameter + :paramref:`send_invoice.is_flexible` was specified, the Bot API will send an + :class:`telegram.Update` with a :attr:`telegram.Update.shipping_query` field to the bot. + Use this method to reply to shipping queries. + + Args: + shipping_query_id (:obj:`str`): Unique identifier for the query to be answered. + ok (:obj:`bool`): Specify :obj:`True` if delivery to the specified address is possible + and :obj:`False` if there are any problems (for example, if delivery to the + specified address is not possible). + shipping_options (Sequence[:class:`telegram.ShippingOption`]), optional): Required if + :paramref:`ok` is :obj:`True`. A sequence of available shipping options. + + .. versionchanged:: 20.0 + |sequenceargs| + error_message (:obj:`str`, optional): Required if :paramref:`ok` is :obj:`False`. + Error message in human readable form that explains why it is impossible to complete + the order (e.g. "Sorry, delivery to your desired address is unavailable"). Telegram + will display this message to the user. + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + Raises: + :class:`telegram.error.TelegramError` + + """ + data: JSONDict = { + "shipping_query_id": shipping_query_id, + "ok": ok, + "shipping_options": shipping_options, + "error_message": error_message, + } + + return await self._post( + "answerShippingQuery", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def answer_pre_checkout_query( + self, + pre_checkout_query_id: str, + ok: bool, + error_message: 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: + """ + Once the user has confirmed their payment and shipping details, the Bot API sends the final + confirmation in the form of an :class:`telegram.Update` with the field + :attr:`telegram.Update.pre_checkout_query`. Use this method to respond to such pre-checkout + queries. + + Note: + The Bot API must receive an answer within 10 seconds after the pre-checkout + query was sent. + + Args: + pre_checkout_query_id (:obj:`str`): Unique identifier for the query to be answered. + ok (:obj:`bool`): Specify :obj:`True` if everything is alright + (goods are available, etc.) and the bot is ready to proceed with the order. Use + :obj:`False` if there are any problems. + error_message (:obj:`str`, optional): Required if :paramref:`ok` is :obj:`False`. Error + message in human readable form that explains the reason for failure to proceed with + the checkout (e.g. "Sorry, somebody just bought the last of our amazing black + T-shirts while you were busy filling out your payment details. Please choose a + different color or garment!"). Telegram will display this message to the user. + + Returns: + :obj:`bool`: On success, :obj:`True` is returned + + Raises: + :class:`telegram.error.TelegramError` + + """ + data: JSONDict = { + "pre_checkout_query_id": pre_checkout_query_id, + "ok": ok, + "error_message": error_message, + } + + return await self._post( + "answerPreCheckoutQuery", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def answer_web_app_query( + self, + web_app_query_id: str, + result: "InlineQueryResult", + *, + 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, + ) -> SentWebAppMessage: + """Use this method to set the result of an interaction with a Web App and send a + corresponding message on behalf of the user to the chat from which the query originated. + + .. versionadded:: 20.0 + + Args: + web_app_query_id (:obj:`str`): Unique identifier for the query to be answered. + result (:class:`telegram.InlineQueryResult`): An object describing the message to be + sent. + + Returns: + :class:`telegram.SentWebAppMessage`: On success, a sent + :class:`telegram.SentWebAppMessage` is returned. + + Raises: + :class:`telegram.error.TelegramError` + + """ + data: JSONDict = { + "web_app_query_id": web_app_query_id, + "result": self._insert_defaults_for_ilq_results(result), + } + + api_result = await self._post( + "answerWebAppQuery", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + return SentWebAppMessage.de_json(api_result, self) # type: ignore[return-value] + + async def restrict_chat_member( + self, + chat_id: Union[str, int], + user_id: int, + permissions: ChatPermissions, + until_date: Optional[Union[int, datetime]] = None, + use_independent_chat_permissions: 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: + """ + Use this method to restrict a user in a supergroup. The bot must be an administrator in + the supergroup for this to work and must have the appropriate admin rights. Pass + :obj:`True` for all boolean parameters to lift restrictions from a user. + + .. seealso:: :meth:`telegram.ChatPermissions.all_permissions` + + Args: + chat_id (:obj:`int` | :obj:`str`): |chat_id_group| + user_id (:obj:`int`): Unique identifier of the target user. + until_date (:obj:`int` | :obj:`datetime.datetime`, optional): Date when restrictions + 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. + permissions (:class:`telegram.ChatPermissions`): An object for new user + permissions. + use_independent_chat_permissions (:obj:`bool`, optional): Pass :obj:`True` if chat + permissions are set independently. Otherwise, the + :attr:`~telegram.ChatPermissions.can_send_other_messages` and + :attr:`~telegram.ChatPermissions.can_add_web_page_previews` permissions will imply + the :attr:`~telegram.ChatPermissions.can_send_messages`, + :attr:`~telegram.ChatPermissions.can_send_audios`, + :attr:`~telegram.ChatPermissions.can_send_documents`, + :attr:`~telegram.ChatPermissions.can_send_photos`, + :attr:`~telegram.ChatPermissions.can_send_videos`, + :attr:`~telegram.ChatPermissions.can_send_video_notes`, and + :attr:`~telegram.ChatPermissions.can_send_voice_notes` permissions; the + :attr:`~telegram.ChatPermissions.can_send_polls` permission will imply the + :attr:`~telegram.ChatPermissions.can_send_messages` permission. + + .. versionadded: 20.1 + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + Raises: + :class:`telegram.error.TelegramError` + """ + data: JSONDict = { + "chat_id": chat_id, + "user_id": user_id, + "permissions": permissions, + "until_date": until_date, + "use_independent_chat_permissions": use_independent_chat_permissions, + } + + return await self._post( + "restrictChatMember", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def promote_chat_member( + self, + chat_id: Union[str, int], + user_id: int, + can_change_info: Optional[bool] = None, + can_post_messages: Optional[bool] = None, + can_edit_messages: Optional[bool] = None, + can_delete_messages: Optional[bool] = None, + can_invite_users: Optional[bool] = None, + can_restrict_members: Optional[bool] = None, + can_pin_messages: Optional[bool] = None, + can_promote_members: Optional[bool] = None, + is_anonymous: Optional[bool] = None, + can_manage_chat: Optional[bool] = None, + can_manage_video_chats: Optional[bool] = None, + can_manage_topics: Optional[bool] = None, + can_post_stories: Optional[bool] = None, + can_edit_stories: Optional[bool] = None, + can_delete_stories: 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: + """ + Use this method to promote or demote a user in a supergroup or a channel. The bot must be + an administrator in the chat for this to work and must have the appropriate admin rights. + Pass :obj:`False` for all boolean parameters to demote a user. + + .. versionchanged:: 20.0 + The argument ``can_manage_voice_chats`` was renamed to + :paramref:`can_manage_video_chats` in accordance to Bot API 6.0. + + Args: + chat_id (:obj:`int` | :obj:`str`): |chat_id_channel| + user_id (:obj:`int`): Unique identifier of the target user. + is_anonymous (:obj:`bool`, optional): Pass :obj:`True`, if the administrator's presence + in the chat is hidden. + can_manage_chat (:obj:`bool`, optional): Pass :obj:`True`, if the administrator can + access the chat event log, get boost list, see hidden supergroup and channel + members, report spam messages and ignore slow mode. Implied by any other + administrator privilege. + + .. versionadded:: 13.4 + + can_manage_video_chats (:obj:`bool`, optional): Pass :obj:`True`, if the administrator + can manage video chats. + + .. versionadded:: 20.0 + + can_change_info (:obj:`bool`, optional): Pass :obj:`True`, if the administrator can + change chat title, photo and other settings. + can_post_messages (:obj:`bool`, optional): Pass :obj:`True`, if the administrator can + post messages in the channel, or access channel statistics; for channels only. + can_edit_messages (:obj:`bool`, optional): Pass :obj:`True`, if the administrator can + edit messages of other users and can pin messages, for channels only. + can_delete_messages (:obj:`bool`, optional): Pass :obj:`True`, if the administrator can + delete messages of other users. + can_invite_users (:obj:`bool`, optional): Pass :obj:`True`, if the administrator can + invite new users to the chat. + can_restrict_members (:obj:`bool`, optional): Pass :obj:`True`, if the administrator + can restrict, ban or unban chat members, or access supergroup statistics. + can_pin_messages (:obj:`bool`, optional): Pass :obj:`True`, if the administrator can + pin messages, for supergroups only. + can_promote_members (:obj:`bool`, optional): Pass :obj:`True`, if the administrator can + add new administrators with a subset of their own privileges or demote + administrators that they have promoted, directly or indirectly + (promoted by administrators that were appointed by the user). + can_manage_topics (:obj:`bool`, optional): Pass :obj:`True`, if the user is + allowed to create, rename, close, and reopen forum topics; for supergroups only. + + .. versionadded:: 20.0 + can_post_stories (:obj:`bool`, optional): Pass :obj:`True`, if the administrator can + post stories to the chat. + + .. versionadded:: 20.6 + can_edit_stories (:obj:`bool`, optional): Pass :obj:`True`, if the administrator can + edit stories posted by other users, post stories to the chat page, pin chat + stories, and access the chat's story archive + + .. versionadded:: 20.6 + can_delete_stories (:obj:`bool`, optional): Pass :obj:`True`, if the administrator can + delete stories posted by other users. + + .. versionadded:: 20.6 + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + Raises: + :class:`telegram.error.TelegramError` + + """ + data: JSONDict = { + "chat_id": chat_id, + "user_id": user_id, + "is_anonymous": is_anonymous, + "can_change_info": can_change_info, + "can_post_messages": can_post_messages, + "can_edit_messages": can_edit_messages, + "can_delete_messages": can_delete_messages, + "can_invite_users": can_invite_users, + "can_restrict_members": can_restrict_members, + "can_pin_messages": can_pin_messages, + "can_promote_members": can_promote_members, + "can_manage_chat": can_manage_chat, + "can_manage_video_chats": can_manage_video_chats, + "can_manage_topics": can_manage_topics, + "can_post_stories": can_post_stories, + "can_edit_stories": can_edit_stories, + "can_delete_stories": can_delete_stories, + } + + return await self._post( + "promoteChatMember", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def set_chat_permissions( + self, + chat_id: Union[str, int], + permissions: ChatPermissions, + use_independent_chat_permissions: 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: + """ + Use this method to set default chat permissions for all members. The bot must be an + administrator in the group or a supergroup for this to work and must have the + :attr:`telegram.ChatMemberAdministrator.can_restrict_members` admin rights. + + Args: + chat_id (:obj:`int` | :obj:`str`): |chat_id_group| + permissions (:class:`telegram.ChatPermissions`): New default chat permissions. + use_independent_chat_permissions (:obj:`bool`, optional): Pass :obj:`True` if chat + permissions are set independently. Otherwise, the + :attr:`~telegram.ChatPermissions.can_send_other_messages` and + :attr:`~telegram.ChatPermissions.can_add_web_page_previews` permissions will imply + the :attr:`~telegram.ChatPermissions.can_send_messages`, + :attr:`~telegram.ChatPermissions.can_send_audios`, + :attr:`~telegram.ChatPermissions.can_send_documents`, + :attr:`~telegram.ChatPermissions.can_send_photos`, + :attr:`~telegram.ChatPermissions.can_send_videos`, + :attr:`~telegram.ChatPermissions.can_send_video_notes`, and + :attr:`~telegram.ChatPermissions.can_send_voice_notes` permissions; the + :attr:`~telegram.ChatPermissions.can_send_polls` permission will imply the + :attr:`~telegram.ChatPermissions.can_send_messages` permission. + + .. versionadded: 20.1 + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + Raises: + :class:`telegram.error.TelegramError` + + """ + data: JSONDict = { + "chat_id": chat_id, + "permissions": permissions, + "use_independent_chat_permissions": use_independent_chat_permissions, + } + return await self._post( + "setChatPermissions", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def set_chat_administrator_custom_title( + self, + chat_id: Union[int, str], + user_id: int, + custom_title: 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: + """ + Use this method to set a custom title for administrators promoted by the bot in a + supergroup. The bot must be an administrator for this to work. + + Args: + chat_id (:obj:`int` | :obj:`str`): |chat_id_group| + user_id (:obj:`int`): Unique identifier of the target administrator. + custom_title (:obj:`str`): New custom title for the administrator; + 0-:tg-const:`telegram.constants.ChatLimit.CHAT_ADMINISTRATOR_CUSTOM_TITLE_LENGTH` + characters, emoji are not allowed. + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + Raises: + :class:`telegram.error.TelegramError` + + """ + data: JSONDict = {"chat_id": chat_id, "user_id": user_id, "custom_title": custom_title} + + return await self._post( + "setChatAdministratorCustomTitle", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def export_chat_invite_link( + self, + chat_id: Union[str, 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, + ) -> str: + """ + Use this method to generate a new primary invite link for a chat; any previously generated + link is revoked. The bot must be an administrator in the chat for this to work and must + have the appropriate admin rights. + + Note: + Each administrator in a chat generates their own invite links. Bots can't use invite + links generated by other administrators. If you want your bot to work with invite + links, it will need to generate its own link using :meth:`export_chat_invite_link` or + by calling the :meth:`get_chat` method. If your bot needs to generate a new primary + invite link replacing its previous one, use :meth:`export_chat_invite_link` again. + + Args: + chat_id (:obj:`int` | :obj:`str`): |chat_id_channel| + + Returns: + :obj:`str`: New invite link on success. + + Raises: + :class:`telegram.error.TelegramError` + + """ + data: JSONDict = {"chat_id": chat_id} + return await self._post( + "exportChatInviteLink", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def create_chat_invite_link( + self, + chat_id: Union[str, int], + expire_date: Optional[Union[int, datetime]] = None, + member_limit: Optional[int] = None, + name: Optional[str] = None, + creates_join_request: 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, + ) -> ChatInviteLink: + """ + Use this method to create an additional invite link for a chat. The bot must be an + administrator in the chat for this to work and must have the appropriate admin rights. + The link can be revoked using the method :meth:`revoke_chat_invite_link`. + + Note: + When joining *public* groups via an invite link, Telegram clients may display the + usual "Join" button, effectively ignoring the invite link. In particular, the parameter + :paramref:`creates_join_request` has no effect in this case. + However, this behavior is undocument and may be subject to change. + See `this GitHub thread `_ + for some discussion. + + .. versionadded:: 13.4 + + Args: + 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. + 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`- + :tg-const:`telegram.constants.ChatInviteLinkLimit.MAX_MEMBER_LIMIT`. + name (:obj:`str`, optional): Invite link name; + 0-:tg-const:`telegram.constants.ChatInviteLinkLimit.NAME_LENGTH` characters. + + .. versionadded:: 13.8 + creates_join_request (:obj:`bool`, optional): :obj:`True`, if users joining the chat + via the link need to be approved by chat administrators. + If :obj:`True`, :paramref:`member_limit` can't be specified. + + .. versionadded:: 13.8 + + Returns: + :class:`telegram.ChatInviteLink` + + Raises: + :class:`telegram.error.TelegramError` + + """ + data: JSONDict = { + "chat_id": chat_id, + "expire_date": expire_date, + "member_limit": member_limit, + "name": name, + "creates_join_request": creates_join_request, + } + + result = await self._post( + "createChatInviteLink", + 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) # type: ignore[return-value] + + async def edit_chat_invite_link( + self, + chat_id: Union[str, int], + invite_link: Union[str, "ChatInviteLink"], + expire_date: Optional[Union[int, datetime]] = None, + member_limit: Optional[int] = None, + name: Optional[str] = None, + creates_join_request: 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, + ) -> ChatInviteLink: + """ + Use this method to edit a non-primary invite link created by the bot. The bot must be an + administrator in the chat for this to work and must have the appropriate admin rights. + + Note: + Though not stated explicitly in the official docs, Telegram changes not only the + optional parameters that are explicitly passed, but also replaces all other optional + parameters to the default values. However, since not documented, this behaviour may + change unbeknown to PTB. + + .. versionadded:: 13.4 + + Args: + chat_id (:obj:`int` | :obj:`str`): |chat_id_channel| + invite_link (:obj:`str` | :class:`telegram.ChatInviteLink`): The invite link to edit. + + .. versionchanged:: 20.0 + 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. + 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`- + :tg-const:`telegram.constants.ChatInviteLinkLimit.MAX_MEMBER_LIMIT`. + name (:obj:`str`, optional): Invite link name; + 0-:tg-const:`telegram.constants.ChatInviteLinkLimit.NAME_LENGTH` characters. + + .. versionadded:: 13.8 + creates_join_request (:obj:`bool`, optional): :obj:`True`, if users joining the chat + via the link need to be approved by chat administrators. + If :obj:`True`, :paramref:`member_limit` can't be specified. + + .. versionadded:: 13.8 + + 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, + "expire_date": expire_date, + "member_limit": member_limit, + "name": name, + "creates_join_request": creates_join_request, + } + + result = await self._post( + "editChatInviteLink", + 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) # type: ignore[return-value] + + async def revoke_chat_invite_link( + self, + chat_id: Union[str, int], + invite_link: Union[str, "ChatInviteLink"], + *, + 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 revoke an invite link created by the bot. If the primary link is + revoked, a new link is automatically generated. The bot must be an administrator in the + chat for this to work and must have the appropriate admin rights. + + .. versionadded:: 13.4 + + Args: + chat_id (:obj:`int` | :obj:`str`): |chat_id_channel| + invite_link (:obj:`str` | :class:`telegram.ChatInviteLink`): The invite link to revoke. + + .. versionchanged:: 20.0 + Now also accepts :class:`telegram.ChatInviteLink` instances. + + 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} + + result = await self._post( + "revokeChatInviteLink", + 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) # type: ignore[return-value] + + async def approve_chat_join_request( + self, + chat_id: Union[str, int], + 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: + """Use this method to approve a chat join request. + + The bot must be an administrator in the chat for this to work and must have the + :attr:`telegram.ChatPermissions.can_invite_users` administrator right. + + .. versionadded:: 13.8 + + Args: + chat_id (:obj:`int` | :obj:`str`): |chat_id_channel| + 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 = {"chat_id": chat_id, "user_id": user_id} + + return await self._post( + "approveChatJoinRequest", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def decline_chat_join_request( + self, + chat_id: Union[str, int], + 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: + """Use this method to decline a chat join request. + + The bot must be an administrator in the chat for this to work and must have the + :attr:`telegram.ChatPermissions.can_invite_users` administrator right. + + .. versionadded:: 13.8 + + Args: + chat_id (:obj:`int` | :obj:`str`): |chat_id_channel| + 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 = {"chat_id": chat_id, "user_id": user_id} + + return await self._post( + "declineChatJoinRequest", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def set_chat_photo( + self, + chat_id: Union[str, int], + photo: FileInput, + *, + 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: + """Use this method to set a new profile photo for the chat. + + Photos can't be changed for private chats. The bot must be an administrator in the chat + for this to work and must have the appropriate admin rights. + + Args: + chat_id (:obj:`int` | :obj:`str`): |chat_id_channel| + photo (:term:`file object` | :obj:`bytes` | :class:`pathlib.Path`): New chat photo. + |uploadinput| + + .. versionchanged:: 13.2 + Accept :obj:`bytes` as input. + + .. versionchanged:: 20.0 + File paths as input is also accepted for bots *not* running in + :paramref:`~telegram.Bot.local_mode`. + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + Raises: + :class:`telegram.error.TelegramError` + + """ + data: JSONDict = {"chat_id": chat_id, "photo": self._parse_file_input(photo)} + return await self._post( + "setChatPhoto", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def delete_chat_photo( + self, + chat_id: Union[str, 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: + """ + Use this method to delete a chat photo. Photos can't be changed for private chats. The bot + must be an administrator in the chat for this to work and must have the appropriate admin + rights. + + 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( + "deleteChatPhoto", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def set_chat_title( + self, + chat_id: Union[str, int], + title: 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: + """ + Use this method to change the title of a chat. Titles can't be changed for private chats. + The bot must be an administrator in the chat for this to work and must have the appropriate + admin rights. + + Args: + chat_id (:obj:`int` | :obj:`str`): |chat_id_channel| + title (:obj:`str`): New chat title, + :tg-const:`telegram.constants.ChatLimit.MIN_CHAT_TITLE_LENGTH`- + :tg-const:`telegram.constants.ChatLimit.MAX_CHAT_TITLE_LENGTH` characters. + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + Raises: + :class:`telegram.error.TelegramError` + + """ + data: JSONDict = {"chat_id": chat_id, "title": title} + return await self._post( + "setChatTitle", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def set_chat_description( + self, + chat_id: Union[str, int], + 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: + """ + Use this method to change the description of a group, a supergroup or a channel. The bot + must be an administrator in the chat for this to work and must have the appropriate admin + rights. + + Args: + chat_id (:obj:`int` | :obj:`str`): |chat_id_channel| + description (:obj:`str`, optional): New chat description, + 0-:tg-const:`telegram.constants.ChatLimit.CHAT_DESCRIPTION_LENGTH` + characters. + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + Raises: + :class:`telegram.error.TelegramError` + + """ + data: JSONDict = {"chat_id": chat_id, "description": description} + + return await self._post( + "setChatDescription", + 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, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: Optional[JSONDict] = None, + ) -> bool: + """ + Use this method to add a message to the list of pinned messages in a chat. If the + chat is not a private chat, the bot must be an administrator in the chat for this to work + and must have the :paramref:`~telegram.ChatAdministratorRights.can_pin_messages` admin + right in a supergroup or :attr:`~telegram.ChatMemberAdministrator.can_edit_messages` admin + right in a channel. + + Args: + chat_id (:obj:`int` | :obj:`str`): |chat_id_channel| + message_id (:obj:`int`): Identifier of a message to pin. + 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. + + Raises: + :class:`telegram.error.TelegramError` + + """ + data: JSONDict = { + "chat_id": chat_id, + "message_id": message_id, + "disable_notification": disable_notification, + "business_connection_id": business_connection_id, + } + + return await self._post( + "pinChatMessage", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + 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, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: Optional[JSONDict] = None, + ) -> bool: + """ + Use this method to remove a message from the list of pinned messages in a chat. If the + chat is not a private chat, the bot must be an administrator in the chat for this to work + and must have the :paramref:`~telegram.ChatAdministratorRights.can_pin_messages` admin + right in a supergroup or :attr:`~telegram.ChatMemberAdministrator.can_edit_messages` admin + right in a channel. + + Args: + chat_id (:obj:`int` | :obj:`str`): |chat_id_channel| + 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. + + Raises: + :class:`telegram.error.TelegramError` + + """ + data: JSONDict = { + "chat_id": chat_id, + "message_id": message_id, + "business_connection_id": business_connection_id, + } + + return await self._post( + "unpinChatMessage", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def unpin_all_chat_messages( + self, + chat_id: Union[str, 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: + """ + Use this method to clear the list of pinned messages in a chat. If the + chat is not a private chat, the bot must be an administrator in the chat for this + to work and must have the :paramref:`~telegram.ChatAdministratorRights.can_pin_messages` + admin right in a supergroup or :attr:`~telegram.ChatMemberAdministrator.can_edit_messages` + admin right in a channel. + + 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( + "unpinAllChatMessages", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def get_sticker_set( + self, + name: 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, + ) -> StickerSet: + """Use this method to get a sticker set. + + Args: + name (:obj:`str`): Name of the sticker set. + + Returns: + :class:`telegram.StickerSet` + + Raises: + :class:`telegram.error.TelegramError` + + """ + data: JSONDict = {"name": name} + result = await self._post( + "getStickerSet", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + return StickerSet.de_json(result, self) # type: ignore[return-value] + + async def get_custom_emoji_stickers( + self, + custom_emoji_ids: Sequence[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, + ) -> Tuple[Sticker, ...]: + """ + Use this method to get information about emoji stickers by their identifiers. + + .. versionchanged:: 20.0 + Returns a tuple instead of a list. + + Args: + custom_emoji_ids (Sequence[:obj:`str`]): Sequence of custom emoji identifiers. + At most :tg-const:`telegram.constants.CustomEmojiStickerLimit.\ +CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified. + + .. versionchanged:: 20.0 + |sequenceargs| + + Returns: + Tuple[:class:`telegram.Sticker`] + + Raises: + :class:`telegram.error.TelegramError` + + """ + data: JSONDict = {"custom_emoji_ids": custom_emoji_ids} + result = await self._post( + "getCustomEmojiStickers", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + return Sticker.de_list(result, self) + + async def upload_sticker_file( + self, + user_id: int, + sticker: FileInput, + sticker_format: 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, + ) -> File: + """ + Use this method to upload a file with a sticker for later use in the + :meth:`create_new_sticker_set` and :meth:`add_sticker_to_set` methods (can be used multiple + times). + + .. versionchanged:: 20.5 + Removed deprecated parameter ``png_sticker``. + + Args: + user_id (:obj:`int`): User identifier of sticker file owner. + 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| + + .. versionadded:: 20.2 + + sticker_format (:obj:`str`): Format of the sticker. Must be one of + :attr:`telegram.constants.StickerFormat.STATIC`, + :attr:`telegram.constants.StickerFormat.ANIMATED`, + :attr:`telegram.constants.StickerFormat.VIDEO`. + + .. versionadded:: 20.2 + + Returns: + :class:`telegram.File`: On success, the uploaded File is returned. + + Raises: + :class:`telegram.error.TelegramError` + + """ + data: JSONDict = { + "user_id": user_id, + "sticker": self._parse_file_input(sticker), + "sticker_format": sticker_format, + } + result = await self._post( + "uploadStickerFile", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + return File.de_json(result, self) # type: ignore[return-value] + + async def add_sticker_to_set( + self, + user_id: int, + name: str, + sticker: "InputSticker", + *, + 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: + """ + Use this method to add a new sticker to a set created by the bot. The format of the added + sticker must match the format of the other stickers in the set. Emoji sticker sets can have + up to :tg-const:`telegram.constants.StickerSetLimit.MAX_EMOJI_STICKERS` stickers. Other + sticker sets can have up to + :tg-const:`telegram.constants.StickerSetLimit.MAX_STATIC_STICKERS` stickers. + + .. versionchanged:: 20.2 + Since Bot API 6.6, the parameter :paramref:`sticker` replace the parameters + ``png_sticker``, ``tgs_sticker``, ``webm_sticker``, ``emojis``, and ``mask_position``. + + .. versionchanged:: 20.5 + Removed deprecated parameters ``png_sticker``, ``tgs_sticker``, ``webm_sticker``, + ``emojis``, and ``mask_position``. + + Args: + user_id (:obj:`int`): User identifier of created sticker set owner. + name (:obj:`str`): Sticker set name. + 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 isn't changed. + + .. versionadded:: 20.2 + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + Raises: + :class:`telegram.error.TelegramError` + + """ + data: JSONDict = { + "user_id": user_id, + "name": name, + "sticker": sticker, + } + + return await self._post( + "addStickerToSet", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def set_sticker_position_in_set( + self, + sticker: str, + position: 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: + """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. + position (:obj:`int`): New sticker position in the set, zero-based. + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + Raises: + :class:`telegram.error.TelegramError` + + """ + data: JSONDict = {"sticker": sticker, "position": position} + return await self._post( + "setStickerPositionInSet", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def create_new_sticker_set( + self, + user_id: int, + name: str, + title: str, + stickers: Sequence["InputSticker"], + sticker_type: Optional[str] = None, + needs_repainting: 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: + """ + Use this method to create new sticker set owned by a user. + The bot will be able to edit the created sticker set thus created. + + .. versionchanged:: 20.0 + The parameter ``contains_masks`` has been removed. Use :paramref:`sticker_type` + instead. + + .. versionchanged:: 20.2 + Since Bot API 6.6, the parameters :paramref:`stickers` and :paramref:`sticker_format` + replace the parameters ``png_sticker``, ``tgs_sticker``,``webm_sticker``, ``emojis``, + and ``mask_position``. + + .. versionchanged:: 20.5 + Removed the deprecated parameters mentioned above and adjusted the order of the + parameters. + + .. versionremoved:: 21.2 + Removed the deprecated parameter ``sticker_format``. + + Args: + user_id (:obj:`int`): User identifier of created sticker set owner. + name (:obj:`str`): Short name of sticker set, to be used in t.me/addstickers/ URLs + (e.g., animals). Can contain only english letters, digits and underscores. + Must begin with a letter, can't contain consecutive underscores and + must end in "_by_". is case insensitive. + :tg-const:`telegram.constants.StickerLimit.MIN_NAME_AND_TITLE`- + :tg-const:`telegram.constants.StickerLimit.MAX_NAME_AND_TITLE` characters. + title (:obj:`str`): Sticker set title, + :tg-const:`telegram.constants.StickerLimit.MIN_NAME_AND_TITLE`- + :tg-const:`telegram.constants.StickerLimit.MAX_NAME_AND_TITLE` characters. + + stickers (Sequence[:class:`telegram.InputSticker`]): A sequence of + :tg-const:`telegram.constants.StickerSetLimit.MIN_INITIAL_STICKERS`- + :tg-const:`telegram.constants.StickerSetLimit.MAX_INITIAL_STICKERS` initial + stickers to be added to the sticker set. + + .. versionadded:: 20.2 + + sticker_type (:obj:`str`, optional): Type of stickers in the set, pass + :attr:`telegram.Sticker.REGULAR` or :attr:`telegram.Sticker.MASK`, or + :attr:`telegram.Sticker.CUSTOM_EMOJI`. By default, a regular sticker set is created + + .. versionadded:: 20.0 + + needs_repainting (:obj:`bool`, optional): Pass :obj:`True` if stickers in the sticker + set must be repainted to the color of text when used in messages, the accent color + if used as emoji status, white on chat photos, or another appropriate color based + on context; for custom emoji sticker sets only. + + .. versionadded:: 20.2 + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + Raises: + :class:`telegram.error.TelegramError` + """ + data: JSONDict = { + "user_id": user_id, + "name": name, + "title": title, + "stickers": stickers, + "sticker_type": sticker_type, + "needs_repainting": needs_repainting, + } + + return await self._post( + "createNewStickerSet", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def delete_sticker_from_set( + self, + sticker: 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: + """Use this method to delete a sticker from a set created by the bot. + + Args: + sticker (:obj:`str`): File identifier of the sticker. + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + Raises: + :class:`telegram.error.TelegramError` + + """ + data: JSONDict = {"sticker": sticker} + return await self._post( + "deleteStickerFromSet", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def delete_sticker_set( + self, + name: 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: + """ + Use this method to delete a sticker set that was created by the bot. + + .. versionadded:: 20.2 + + Args: + name (:obj:`str`): Sticker set name. + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + Raises: + :class:`telegram.error.TelegramError` + + """ + data: JSONDict = {"name": name} + return await self._post( + "deleteStickerSet", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def set_sticker_set_thumbnail( + self, + name: str, + user_id: int, + format: str, # pylint: disable=redefined-builtin + thumbnail: Optional[FileInput] = 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: + """Use this method to set the thumbnail of a regular or mask sticker set. The format of the + thumbnail file must match the format of the stickers in the set. + + .. versionadded:: 20.2 + + .. versionchanged:: 21.1 + As per Bot API 7.2, the new argument :paramref:`format` will be required, and thus the + order of the arguments had to be changed. + + Args: + name (:obj:`str`): Sticker set name + user_id (:obj:`int`): User identifier of created sticker set owner. + 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. + + .. versionadded:: 21.1 + + 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 + **.TGS** animation with the thumbnail up to + :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 + to :tg-const:`telegram.constants.StickerSetLimit.MAX_ANIMATED_THUMBNAIL_SIZE` + kilobytes in size; see + `this `_ for video sticker + technical requirements. + + |fileinput| + + Animated and video sticker set thumbnails can't be uploaded via HTTP URL. If + omitted, then the thumbnail is dropped and the first sticker is used as the + thumbnail. + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + Raises: + :class:`telegram.error.TelegramError` + + """ + data: JSONDict = { + "name": name, + "user_id": user_id, + "thumbnail": self._parse_file_input(thumbnail) if thumbnail else None, + "format": format, + } + + return await self._post( + "setStickerSetThumbnail", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def set_sticker_set_title( + self, + name: str, + title: 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: + """ + Use this method to set the title of a created sticker set. + + .. versionadded:: 20.2 + + Args: + name (:obj:`str`): Sticker set name. + title (:obj:`str`): Sticker set title, + :tg-const:`telegram.constants.StickerLimit.MIN_NAME_AND_TITLE`- + :tg-const:`telegram.constants.StickerLimit.MAX_NAME_AND_TITLE` characters. + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + Raises: + :class:`telegram.error.TelegramError` + + """ + data: JSONDict = {"name": name, "title": title} + return await self._post( + "setStickerSetTitle", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def set_sticker_emoji_list( + self, + sticker: str, + emoji_list: Sequence[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: + """ + Use this method to change the list of emoji assigned to a regular or custom emoji sticker. + The sticker must belong to a sticker set created by the bot. + + .. versionadded:: 20.2 + + Args: + sticker (:obj:`str`): File identifier of the sticker. + 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 + the sticker. + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + Raises: + :class:`telegram.error.TelegramError` + """ + data: JSONDict = {"sticker": sticker, "emoji_list": emoji_list} + return await self._post( + "setStickerEmojiList", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def set_sticker_keywords( + self, + sticker: str, + keywords: Optional[Sequence[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: + """ + Use this method to change search keywords assigned to a regular or custom emoji sticker. + The sticker must belong to a sticker set created by the bot. + + .. versionadded:: 20.2 + + Args: + sticker (:obj:`str`): File identifier of the sticker. + 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 + :tg-const:`telegram.constants.StickerLimit.MAX_KEYWORD_LENGTH` characters. + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + Raises: + :class:`telegram.error.TelegramError` + """ + data: JSONDict = {"sticker": sticker, "keywords": keywords} + return await self._post( + "setStickerKeywords", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def set_sticker_mask_position( + self, + sticker: str, + mask_position: Optional[MaskPosition] = 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: + """ + Use this method to change the mask position of a mask sticker. + The sticker must belong to a sticker set that was created by the bot. + + .. versionadded:: 20.2 + + Args: + sticker (:obj:`str`): File identifier of the sticker. + 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. + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + Raises: + :class:`telegram.error.TelegramError` + """ + data: JSONDict = {"sticker": sticker, "mask_position": mask_position} + return await self._post( + "setStickerMaskPosition", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def set_custom_emoji_sticker_set_thumbnail( + self, + name: str, + custom_emoji_id: 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: + """ + Use this method to set the thumbnail of a custom emoji sticker set. + + .. versionadded:: 20.2 + + Args: + name (:obj:`str`): Sticker set name. + custom_emoji_id (:obj:`str`, optional): Custom emoji identifier of a sticker from the + sticker set; pass an empty string to drop the thumbnail and use the first sticker + as the thumbnail. + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + Raises: + :class:`telegram.error.TelegramError` + + """ + data: JSONDict = {"name": name, "custom_emoji_id": custom_emoji_id} + + return await self._post( + "setCustomEmojiStickerSetThumbnail", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def set_passport_data_errors( + self, + user_id: int, + errors: Sequence["PassportElementError"], + *, + 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: + """ + Informs a user that some of the Telegram Passport elements they provided contains errors. + The user will not be able to re-submit their Passport to you until the errors are fixed + (the contents of the field for which you returned the error must change). + + Use this if the data submitted by the user doesn't satisfy the standards your service + requires for any reason. For example, if a birthday date seems invalid, a submitted + document is blurry, a scan shows evidence of tampering, etc. Supply some details in the + error message to make sure the user knows how to correct the issues. + + Args: + user_id (:obj:`int`): User identifier + errors (Sequence[:class:`PassportElementError`]): A Sequence describing the errors. + + .. versionchanged:: 20.0 + |sequenceargs| + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + Raises: + :class:`telegram.error.TelegramError` + + """ + data: JSONDict = {"user_id": user_id, "errors": errors} + return await self._post( + "setPassportDataErrors", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def send_poll( + self, + chat_id: Union[int, str], + question: str, + options: Sequence[Union[str, "InputPollOption"]], + is_anonymous: Optional[bool] = None, + type: Optional[str] = None, # pylint: disable=redefined-builtin + allows_multiple_answers: Optional[bool] = None, + correct_option_id: Optional[CorrectOptionID] = None, + is_closed: Optional[bool] = None, + disable_notification: ODVInput[bool] = DEFAULT_NONE, + 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, + explanation_entities: Optional[Sequence["MessageEntity"]] = None, + protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: Optional[int] = None, + reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, + question_parse_mode: ODVInput[str] = DEFAULT_NONE, + question_entities: Optional[Sequence["MessageEntity"]] = None, + message_effect_id: Optional[str] = None, + *, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + reply_to_message_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, + ) -> Message: + """ + Use this method to send a native poll. + + Args: + chat_id (:obj:`int` | :obj:`str`): |chat_id_channel| + question (:obj:`str`): Poll question, :tg-const:`telegram.Poll.MIN_QUESTION_LENGTH`- + :tg-const:`telegram.Poll.MAX_QUESTION_LENGTH` characters. + options (Sequence[:obj:`str` | :class:`telegram.InputPollOption`]): Sequence of + :tg-const:`telegram.Poll.MIN_OPTION_NUMBER`- + :tg-const:`telegram.Poll.MAX_OPTION_NUMBER` answer options. Each option may either + be a string with + :tg-const:`telegram.Poll.MIN_OPTION_LENGTH`- + :tg-const:`telegram.Poll.MAX_OPTION_LENGTH` characters or an + :class:`~telegram.InputPollOption` object. Strings are converted to + :class:`~telegram.InputPollOption` objects automatically. + + .. versionchanged:: 20.0 + |sequenceargs| + + .. versionchanged:: 21.2 + Bot API 7.3 adds support for :class:`~telegram.InputPollOption` objects. + is_anonymous (:obj:`bool`, optional): :obj:`True`, if the poll needs to be anonymous, + defaults to :obj:`True`. + type (:obj:`str`, optional): Poll type, :tg-const:`telegram.Poll.QUIZ` or + :tg-const:`telegram.Poll.REGULAR`, defaults to :tg-const:`telegram.Poll.REGULAR`. + allows_multiple_answers (:obj:`bool`, optional): :obj:`True`, if the poll allows + multiple answers, ignored for polls in quiz mode, defaults to :obj:`False`. + correct_option_id (:obj:`int`, optional): 0-based identifier of the correct answer + option, required for polls in quiz mode. + explanation (:obj:`str`, optional): Text that is shown when a user chooses an incorrect + answer or taps on the lamp icon in a quiz-style poll, + 0-:tg-const:`telegram.Poll.MAX_EXPLANATION_LENGTH` characters with at most + :tg-const:`telegram.Poll.MAX_EXPLANATION_LINE_FEEDS` line feeds after entities + parsing. + explanation_parse_mode (:obj:`str`, optional): Mode for parsing entities in the + explanation. See the constants in :class:`telegram.constants.ParseMode` for the + available modes. + explanation_entities (Sequence[:class:`telegram.MessageEntity`], optional): Sequence of + special entities that appear in message text, which can be specified instead of + :paramref:`explanation_parse_mode`. + + .. versionchanged:: 20.0 + |sequenceargs| + open_period (:obj:`int`, 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`. + 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. + 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| + protect_content (:obj:`bool`, optional): |protect_content| + + .. versionadded:: 13.10 + message_thread_id (:obj:`int`, optional): |message_thread_id_arg| + + .. versionadded:: 20.0 + + reply_markup (:class:`InlineKeyboardMarkup` | :class:`ReplyKeyboardMarkup` | \ + :class:`ReplyKeyboardRemove` | :class:`ForceReply`, optional): + Additional interface options. An object for an inline keyboard, custom reply + keyboard, instructions to remove reply keyboard or to force a reply from the user. + reply_parameters (:class:`telegram.ReplyParameters`, optional): |reply_parameters| + + .. versionadded:: 20.8 + business_connection_id (:obj:`str`, optional): |business_id_str| + + .. versionadded:: 21.1 + question_parse_mode (:obj:`str`, optional): Mode for parsing entities in the question. + See the constants in :class:`telegram.constants.ParseMode` for the available modes. + Currently, only custom emoji entities are allowed. + + .. versionadded:: 21.2 + question_entities (Sequence[:class:`telegram.Message`], optional): Special entities + that appear in the poll :paramref:`question`. It can be specified instead of + :paramref:`question_parse_mode`. + + .. versionadded:: 21.2 + message_effect_id (:obj:`str`, optional): |message_effect_id| + + .. versionadded:: 21.3 + + Keyword Args: + allow_sending_without_reply (:obj:`bool`, optional): |allow_sending_without_reply| + Mutually exclusive with :paramref:`reply_parameters`, which this is a convenience + parameter for + + .. versionchanged:: 20.8 + Bot API 7.0 introduced :paramref:`reply_parameters` |rtm_aswr_deprecated| + + .. versionchanged:: 21.0 + |keyword_only_arg| + reply_to_message_id (:obj:`int`, optional): |reply_to_msg_id| + Mutually exclusive with :paramref:`reply_parameters`, which this is a convenience + parameter for + + .. versionchanged:: 20.8 + Bot API 7.0 introduced :paramref:`reply_parameters` |rtm_aswr_deprecated| + + .. versionchanged:: 21.0 + |keyword_only_arg| + + Returns: + :class:`telegram.Message`: On success, the sent Message is returned. + + Raises: + :class:`telegram.error.TelegramError` + + """ + data: JSONDict = { + "chat_id": chat_id, + "question": question, + "options": [ + InputPollOption(option) if isinstance(option, str) else option + for option in options + ], + "explanation_parse_mode": explanation_parse_mode, + "is_anonymous": is_anonymous, + "type": type, + "allows_multiple_answers": allows_multiple_answers, + "correct_option_id": correct_option_id, + "is_closed": is_closed, + "explanation": explanation, + "explanation_entities": explanation_entities, + "open_period": open_period, + "close_date": close_date, + "question_parse_mode": question_parse_mode, + "question_entities": question_entities, + } + + return await self._send_message( + "sendPoll", + data, + reply_to_message_id=reply_to_message_id, + disable_notification=disable_notification, + reply_markup=reply_markup, + allow_sending_without_reply=allow_sending_without_reply, + protect_content=protect_content, + message_thread_id=message_thread_id, + reply_parameters=reply_parameters, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + business_connection_id=business_connection_id, + message_effect_id=message_effect_id, + ) + + async def stop_poll( + self, + chat_id: Union[int, str], + message_id: int, + reply_markup: Optional["InlineKeyboardMarkup"] = None, + business_connection_id: 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, + ) -> Poll: + """ + Use this method to stop a poll which was sent by the bot. + + Args: + chat_id (:obj:`int` | :obj:`str`): |chat_id_channel| + message_id (:obj:`int`): Identifier of the original message with the poll. + reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): An object for a new + message inline keyboard. + business_connection_id (:obj:`str`, optional): |business_id_str_edit| + + .. versionadded:: 21.4 + + Returns: + :class:`telegram.Poll`: On success, the stopped Poll is returned. + + Raises: + :class:`telegram.error.TelegramError` + + """ + data: JSONDict = { + "chat_id": chat_id, + "message_id": message_id, + "reply_markup": reply_markup, + "business_connection_id": business_connection_id, + } + + result = await self._post( + "stopPoll", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + return Poll.de_json(result, self) # type: ignore[return-value] + + async def send_dice( + self, + chat_id: Union[int, str], + disable_notification: ODVInput[bool] = DEFAULT_NONE, + reply_markup: Optional[ReplyMarkup] = None, + emoji: Optional[str] = None, + protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: Optional[int] = None, + reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, + message_effect_id: Optional[str] = None, + *, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + reply_to_message_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, + ) -> Message: + """ + Use this method to send an animated emoji that will display a random value. + + Args: + chat_id (:obj:`int` | :obj:`str`): |chat_id_channel| + disable_notification (:obj:`bool`, optional): |disable_notification| + reply_markup (:class:`InlineKeyboardMarkup` | :class:`ReplyKeyboardMarkup` | \ + :class:`ReplyKeyboardRemove` | :class:`ForceReply`, optional): + Additional interface options. An object for an inline keyboard, custom reply + keyboard, instructions to remove reply keyboard or to force a reply from the user + emoji (:obj:`str`, optional): Emoji on which the dice throw animation is based. + Currently, must be one of :class:`telegram.constants.DiceEmoji`. Dice can have + values + :tg-const:`telegram.Dice.MIN_VALUE`-:tg-const:`telegram.Dice.MAX_VALUE_BOWLING` + for :tg-const:`telegram.Dice.DICE`, :tg-const:`telegram.Dice.DARTS` and + :tg-const:`telegram.Dice.BOWLING`, values + :tg-const:`telegram.Dice.MIN_VALUE`-:tg-const:`telegram.Dice.MAX_VALUE_BASKETBALL` + for :tg-const:`telegram.Dice.BASKETBALL` and :tg-const:`telegram.Dice.FOOTBALL`, + and values :tg-const:`telegram.Dice.MIN_VALUE`- + :tg-const:`telegram.Dice.MAX_VALUE_SLOT_MACHINE` + for :tg-const:`telegram.Dice.SLOT_MACHINE`. Defaults to + :tg-const:`telegram.Dice.DICE`. + + .. versionchanged:: 13.4 + Added the :tg-const:`telegram.Dice.BOWLING` emoji. + protect_content (:obj:`bool`, optional): |protect_content| + + .. versionadded:: 13.10 + message_thread_id (:obj:`int`, optional): |message_thread_id_arg| + + .. versionadded:: 20.0 + reply_parameters (:class:`telegram.ReplyParameters`, optional): |reply_parameters| + + .. versionadded:: 20.8 + business_connection_id (:obj:`str`, optional): |business_id_str| + + .. versionadded:: 21.1 + message_effect_id (:obj:`str`, optional): |message_effect_id| + + .. versionadded:: 21.3 + + Keyword Args: + allow_sending_without_reply (:obj:`bool`, optional): |allow_sending_without_reply| + Mutually exclusive with :paramref:`reply_parameters`, which this is a convenience + parameter for + + .. versionchanged:: 20.8 + Bot API 7.0 introduced :paramref:`reply_parameters` |rtm_aswr_deprecated| + + .. versionchanged:: 21.0 + |keyword_only_arg| + reply_to_message_id (:obj:`int`, optional): |reply_to_msg_id| + Mutually exclusive with :paramref:`reply_parameters`, which this is a convenience + parameter for + + .. versionchanged:: 20.8 + Bot API 7.0 introduced :paramref:`reply_parameters` |rtm_aswr_deprecated| + + .. versionchanged:: 21.0 + |keyword_only_arg| + + + Returns: + :class:`telegram.Message`: On success, the sent Message is returned. + + Raises: + :class:`telegram.error.TelegramError` + + """ + data: JSONDict = {"chat_id": chat_id, "emoji": emoji} + + return await self._send_message( + "sendDice", + data, + reply_to_message_id=reply_to_message_id, + disable_notification=disable_notification, + reply_markup=reply_markup, + allow_sending_without_reply=allow_sending_without_reply, + protect_content=protect_content, + message_thread_id=message_thread_id, + reply_parameters=reply_parameters, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + business_connection_id=business_connection_id, + message_effect_id=message_effect_id, + ) + + async def get_my_default_administrator_rights( + self, + for_channels: 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, + ) -> ChatAdministratorRights: + """Use this method to get the current default administrator rights of the bot. + + .. seealso:: :meth:`set_my_default_administrator_rights` + + .. versionadded:: 20.0 + + Args: + for_channels (:obj:`bool`, optional): Pass :obj:`True` to get default administrator + rights of the bot in channels. Otherwise, default administrator rights of the bot + for groups and supergroups will be returned. + + Returns: + :class:`telegram.ChatAdministratorRights`: On success. + + Raises: + :class:`telegram.error.TelegramError` + """ + data: JSONDict = {"for_channels": for_channels} + + result = await self._post( + "getMyDefaultAdministratorRights", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + return ChatAdministratorRights.de_json(result, self) # type: ignore[return-value] + + async def set_my_default_administrator_rights( + self, + rights: Optional[ChatAdministratorRights] = None, + for_channels: 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: + """Use this method to change the default administrator rights requested by the bot when + it's added as an administrator to groups or channels. These rights will be suggested to + users, but they are free to modify the list before adding the bot. + + .. seealso:: :meth:`get_my_default_administrator_rights` + + .. versionadded:: 20.0 + + Args: + 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 + rights of the bot for groups and supergroups will be changed. + + Returns: + :obj:`bool`: Returns :obj:`True` on success. + + Raises: + :exc:`telegram.error.TelegramError` + """ + data: JSONDict = {"rights": rights, "for_channels": for_channels} + + return await self._post( + "setMyDefaultAdministratorRights", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def get_my_commands( + self, + scope: Optional[BotCommandScope] = None, + language_code: 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, + ) -> Tuple[BotCommand, ...]: + """ + Use this method to get the current list of the bot's commands for the given scope and user + language. + + .. seealso:: :meth:`set_my_commands`, :meth:`delete_my_commands` + + .. versionchanged:: 20.0 + Returns a tuple instead of a list. + + Args: + scope (:class:`telegram.BotCommandScope`, optional): An object, + describing scope of users. Defaults to :class:`telegram.BotCommandScopeDefault`. + + .. versionadded:: 13.7 + + language_code (:obj:`str`, optional): A two-letter ISO 639-1 language code or an empty + string. + + .. versionadded:: 13.7 + + Returns: + Tuple[:class:`telegram.BotCommand`]: On success, the commands set for the bot. An empty + tuple is returned if commands are not set. + + Raises: + :class:`telegram.error.TelegramError` + + """ + data: JSONDict = {"scope": scope, "language_code": language_code} + + result = await self._post( + "getMyCommands", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + return BotCommand.de_list(result, self) + + async def set_my_commands( + self, + commands: Sequence[Union[BotCommand, Tuple[str, str]]], + scope: Optional[BotCommandScope] = None, + language_code: 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: + """ + Use this method to change the list of the bot's commands. See the + `Telegram docs `_ for more details about + bot commands. + + .. seealso:: :meth:`get_my_commands`, :meth:`delete_my_commands` + + Args: + commands (Sequence[:class:`BotCommand` | (:obj:`str`, :obj:`str`)]): A sequence + of bot commands to be set as the list of the bot's commands. At most + :tg-const:`telegram.constants.BotCommandLimit.MAX_COMMAND_NUMBER` commands can be + specified. + + Note: + If you pass in a sequence of :obj:`tuple`, the order of elements in each + :obj:`tuple` must correspond to the order of positional arguments to create a + :class:`BotCommand` instance. + + .. versionchanged:: 20.0 + |sequenceargs| + scope (:class:`telegram.BotCommandScope`, optional): An object, + describing scope of users for which the commands are relevant. Defaults to + :class:`telegram.BotCommandScopeDefault`. + + .. versionadded:: 13.7 + + language_code (:obj:`str`, optional): A two-letter ISO 639-1 language code. If empty, + commands will be applied to all users from the given scope, for whose language + there are no dedicated commands. + + .. versionadded:: 13.7 + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + Raises: + :class:`telegram.error.TelegramError` + + """ + cmds = [c if isinstance(c, BotCommand) else BotCommand(c[0], c[1]) for c in commands] + data: JSONDict = {"commands": cmds, "scope": scope, "language_code": language_code} + + return await self._post( + "setMyCommands", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def delete_my_commands( + self, + scope: Optional[BotCommandScope] = None, + language_code: 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: + """ + Use this method to delete the list of the bot's commands for the given scope and user + language. After deletion, + `higher level commands `_ + will be shown to affected users. + + .. versionadded:: 13.7 + + .. seealso:: :meth:`get_my_commands`, :meth:`set_my_commands` + + Args: + scope (:class:`telegram.BotCommandScope`, optional): An object, + describing scope of users for which the commands are relevant. Defaults to + :class:`telegram.BotCommandScopeDefault`. + language_code (:obj:`str`, optional): A two-letter ISO 639-1 language code. If empty, + commands will be applied to all users from the given scope, for whose language + there are no dedicated commands. + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + Raises: + :class:`telegram.error.TelegramError` + """ + data: JSONDict = {"scope": scope, "language_code": language_code} + + return await self._post( + "deleteMyCommands", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def log_out( + 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: + """ + Use this method to log out from the cloud Bot API server before launching the bot locally. + You *must* log out the bot before running it locally, otherwise there is no guarantee that + the bot will receive updates. After a successful call, you can immediately log in on a + local server, but will not be able to log in back to the cloud Bot API server for 10 + minutes. + + Returns: + :obj:`True`: On success, :obj:`True` is returned. + + Raises: + :class:`telegram.error.TelegramError` + + """ + return await self._post( + "logOut", + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def close( + 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: + """ + Use this method to close the bot instance before moving it from one local server to + another. You need to delete the webhook before calling this method to ensure that the bot + isn't launched again after server restart. The method will return error 429 in the first + 10 minutes after the bot is launched. + + Returns: + :obj:`True`: On success, :obj:`True` is returned. + + Raises: + :class:`telegram.error.TelegramError` + + """ + return await self._post( + "close", + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def copy_message( + self, + chat_id: Union[int, str], + from_chat_id: Union[str, int], + message_id: int, + caption: Optional[str] = None, + parse_mode: ODVInput[str] = DEFAULT_NONE, + caption_entities: Optional[Sequence["MessageEntity"]] = None, + disable_notification: ODVInput[bool] = DEFAULT_NONE, + reply_markup: Optional[ReplyMarkup] = None, + protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: Optional[int] = None, + reply_parameters: Optional["ReplyParameters"] = None, + show_caption_above_media: Optional[bool] = None, + *, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + reply_to_message_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, + ) -> MessageId: + """Use this method to copy messages of any kind. Service messages, paid media messages, + giveaway messages, giveaway winners messages, and invoice messages + can't be copied. The method is analogous to the method :meth:`forward_message`, but the + copied message doesn't have a link to the original message. + + Args: + chat_id (:obj:`int` | :obj:`str`): |chat_id_channel| + 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. + 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. + parse_mode (:obj:`str`, optional): Mode for parsing entities in the new caption. See + the constants in :class:`telegram.constants.ParseMode` for the available modes. + caption_entities (Sequence[:class:`telegram.MessageEntity`], optional): + |caption_entities| + + .. versionchanged:: 20.0 + |sequenceargs| + disable_notification (:obj:`bool`, optional): |disable_notification| + protect_content (:obj:`bool`, optional): |protect_content| + + .. versionadded:: 13.10 + message_thread_id (:obj:`int`, optional): |message_thread_id_arg| + + .. versionadded:: 20.0 + + reply_markup (:class:`InlineKeyboardMarkup` | :class:`ReplyKeyboardMarkup` | \ + :class:`ReplyKeyboardRemove` | :class:`ForceReply`, optional): + Additional interface options. An object for an inline keyboard, custom reply + keyboard, instructions to remove reply keyboard or to force a reply from the user. + reply_parameters (:class:`telegram.ReplyParameters`, optional): |reply_parameters| + + .. versionadded:: 20.8 + show_caption_above_media (:obj:`bool`, optional): Pass |show_cap_above_med| + + .. versionadded:: 21.3 + + Keyword Args: + allow_sending_without_reply (:obj:`bool`, optional): |allow_sending_without_reply| + Mutually exclusive with :paramref:`reply_parameters`, which this is a convenience + parameter for + + .. versionchanged:: 20.8 + Bot API 7.0 introduced :paramref:`reply_parameters` |rtm_aswr_deprecated| + + .. versionchanged:: 21.0 + |keyword_only_arg| + reply_to_message_id (:obj:`int`, optional): |reply_to_msg_id| + Mutually exclusive with :paramref:`reply_parameters`, which this is a convenience + parameter for + + .. versionchanged:: 20.8 + Bot API 7.0 introduced :paramref:`reply_parameters` |rtm_aswr_deprecated| + + .. versionchanged:: 21.0 + |keyword_only_arg| + + Returns: + :class:`telegram.MessageId`: On success, the :class:`telegram.MessageId` of the sent + message is returned. + + Raises: + :class:`telegram.error.TelegramError` + + """ + if allow_sending_without_reply is not DEFAULT_NONE and reply_parameters is not None: + raise ValueError( + "`allow_sending_without_reply` and `reply_parameters` are mutually exclusive." + ) + + if reply_to_message_id is not None and reply_parameters is not None: + raise ValueError( + "`reply_to_message_id` and `reply_parameters` are mutually exclusive." + ) + + if reply_to_message_id is not None: + reply_parameters = ReplyParameters( + message_id=reply_to_message_id, + allow_sending_without_reply=allow_sending_without_reply, + ) + + data: JSONDict = { + "chat_id": chat_id, + "from_chat_id": from_chat_id, + "message_id": message_id, + "parse_mode": parse_mode, + "disable_notification": disable_notification, + "protect_content": protect_content, + "caption": caption, + "caption_entities": caption_entities, + "reply_markup": reply_markup, + "message_thread_id": message_thread_id, + "reply_parameters": reply_parameters, + "show_caption_above_media": show_caption_above_media, + } + + result = await self._post( + "copyMessage", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + return MessageId.de_json(result, self) # type: ignore[return-value] + + async def copy_messages( + self, + chat_id: Union[int, str], + from_chat_id: Union[str, int], + message_ids: Sequence[int], + disable_notification: ODVInput[bool] = DEFAULT_NONE, + protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: Optional[int] = None, + remove_caption: 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, + ) -> 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 + messages, giveaway winners messages, and invoice messages can't be copied. A quiz poll can + be copied only if the value + of the field :attr:`telegram.Poll.correct_option_id` is known to the bot. The method is + analogous to the method :meth:`forward_messages`, but the copied messages don't have a + link to the original message. Album grouping is kept for copied messages. + + .. versionadded:: 20.8 + + Args: + chat_id (:obj:`int` | :obj:`str`): |chat_id_channel| + 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_ids (Sequence[:obj:`int`]): A list of + :tg-const:`telegram.constants.BulkRequestLimit.MIN_LIMIT` - + :tg-const:`telegram.constants.BulkRequestLimit.MAX_LIMIT` identifiers of messages + in the chat :paramref:`from_chat_id` to copy. The identifiers must be + specified in a strictly increasing order. + disable_notification (:obj:`bool`, optional): |disable_notification| + protect_content (:obj:`bool`, optional): |protect_content| + message_thread_id (:obj:`int`, optional): |message_thread_id_arg| + remove_caption (:obj:`bool`, optional): Pass :obj:`True` to copy the messages without + their captions. + + Returns: + Tuple[:class:`telegram.MessageId`]: On success, a tuple of :class:`~telegram.MessageId` + of the sent messages is returned. + + Raises: + :class:`telegram.error.TelegramError` + + """ + data: JSONDict = { + "chat_id": chat_id, + "from_chat_id": from_chat_id, + "message_ids": message_ids, + "disable_notification": disable_notification, + "protect_content": protect_content, + "message_thread_id": message_thread_id, + "remove_caption": remove_caption, + } + + result = await self._post( + "copyMessages", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + return MessageId.de_list(result, self) + + async def set_chat_menu_button( + self, + chat_id: Optional[int] = None, + menu_button: Optional[MenuButton] = 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: + """Use this method to change the bot's menu button in a private chat, or the default menu + button. + + .. seealso:: :meth:`get_chat_menu_button`, :meth:`telegram.Chat.get_menu_button` + :meth:`telegram.User.get_menu_button` + + .. versionadded:: 20.0 + + Args: + chat_id (:obj:`int`, optional): Unique identifier for the target private chat. If not + specified, default bot's menu button will be changed + menu_button (:class:`telegram.MenuButton`, optional): An object for the new bot's menu + button. Defaults to :class:`telegram.MenuButtonDefault`. + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + """ + data: JSONDict = {"chat_id": chat_id, "menu_button": menu_button} + + return await self._post( + "setChatMenuButton", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def get_chat_menu_button( + self, + chat_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, + ) -> MenuButton: + """Use this method to get the current value of the bot's menu button in a private chat, or + the default menu button. + + .. seealso:: :meth:`set_chat_menu_button`, :meth:`telegram.Chat.set_menu_button`, + :meth:`telegram.User.set_menu_button` + + .. versionadded:: 20.0 + + Args: + chat_id (:obj:`int`, optional): Unique identifier for the target private chat. If not + specified, default bot's menu button will be returned. + + Returns: + :class:`telegram.MenuButton`: On success, the current menu button is returned. + + """ + data = {"chat_id": chat_id} + + result = await self._post( + "getChatMenuButton", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + return MenuButton.de_json(result, bot=self) # type: ignore[return-value] + + 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"], + max_tip_amount: Optional[int] = None, + suggested_tip_amounts: Optional[Sequence[int]] = None, + provider_data: Optional[Union[str, object]] = None, + photo_url: Optional[str] = None, + photo_size: Optional[int] = None, + photo_width: Optional[int] = None, + photo_height: Optional[int] = None, + need_name: Optional[bool] = None, + need_phone_number: Optional[bool] = None, + need_email: Optional[bool] = None, + need_shipping_address: Optional[bool] = None, + send_phone_number_to_provider: Optional[bool] = None, + send_email_to_provider: Optional[bool] = None, + is_flexible: 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, + ) -> str: + """Use this method to create a link for an invoice. + + .. versionadded:: 20.0 + + Args: + 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. + :tg-const:`telegram.Invoice.MIN_DESCRIPTION_LENGTH`- + :tg-const:`telegram.Invoice.MAX_DESCRIPTION_LENGTH` characters. + 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 it for your internal processes. + provider_token (:obj:`str`): 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. + + currency (:obj:`str`): Three-letter ISO 4217 currency code, see `more on currencies + `_. Pass ``XTR`` for + payments in |tg_stars|. + prices (Sequence[:class:`telegram.LabeledPrice`)]: Price breakdown, a sequence + of components (e.g. product price, tax, discount, delivery cost, delivery tax, + bonus, etc.). Must contain exactly one item for payments in |tg_stars|. + + .. versionchanged:: 20.0 + |sequenceargs| + 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`` + parameter in `currencies.json + `_, it shows the number of + digits past the decimal point for each currency (2 for the majority of currencies). + Defaults to ``0``. Not supported for payments in |tg_stars|. + suggested_tip_amounts (Sequence[:obj:`int`], optional): An array of + suggested amounts of tips in the *smallest units* of the currency (integer, **not** + float/double). At most :tg-const:`telegram.Invoice.MAX_TIP_AMOUNTS` suggested tip + amounts can be specified. The suggested tip amounts must be positive, passed in a + strictly increased order and must not exceed :paramref:`max_tip_amount`. + + .. versionchanged:: 20.0 + |sequenceargs| + provider_data (:obj:`str` | :obj:`object`, optional): Data about the + invoice, which will be shared with the payment provider. A detailed description of + required fields should be provided by the payment provider. When an object is + passed, it will be encoded as JSON. + photo_url (:obj:`str`, optional): URL of the product photo for the invoice. Can be a + photo of the goods or a marketing image for a service. + photo_size (:obj:`int`, optional): Photo size in bytes. + photo_width (:obj:`int`, optional): Photo width. + photo_height (:obj:`int`, optional): Photo height. + need_name (:obj:`bool`, optional): Pass :obj:`True`, if you require the user's full + name to complete the order. Ignored for payments in |tg_stars|. + need_phone_number (:obj:`bool`, optional): Pass :obj:`True`, if you require the user's + phone number to complete the order. Ignored for payments in |tg_stars|. + need_email (:obj:`bool`, optional): Pass :obj:`True`, if you require the user's email + address to complete the order. Ignored for payments in |tg_stars|. + need_shipping_address (:obj:`bool`, optional): Pass :obj:`True`, if you require the + user's shipping address to complete the order. Ignored for payments in + |tg_stars|. + send_phone_number_to_provider (:obj:`bool`, optional): Pass :obj:`True`, if user's + phone number should be sent to provider. Ignored for payments in |tg_stars|. + send_email_to_provider (:obj:`bool`, optional): Pass :obj:`True`, if user's email + address should be sent to provider. Ignored for payments in |tg_stars|. + is_flexible (:obj:`bool`, optional): Pass :obj:`True`, if the final price depends on + the shipping method. Ignored for payments in |tg_stars|. + + Returns: + :class:`str`: On success, the created invoice link is returned. + + """ + data: JSONDict = { + "title": title, + "description": description, + "payload": payload, + "provider_token": provider_token, + "currency": currency, + "prices": prices, + "max_tip_amount": max_tip_amount, + "suggested_tip_amounts": suggested_tip_amounts, + "provider_data": provider_data, + "photo_url": photo_url, + "photo_size": photo_size, + "photo_width": photo_width, + "photo_height": photo_height, + "need_name": need_name, + "need_phone_number": need_phone_number, + "need_email": need_email, + "need_shipping_address": need_shipping_address, + "is_flexible": is_flexible, + "send_phone_number_to_provider": send_phone_number_to_provider, + "send_email_to_provider": send_email_to_provider, + } + + return await self._post( + "createInvoiceLink", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def get_forum_topic_icon_stickers( + 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, + ) -> 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`] + + Raises: + :class:`telegram.error.TelegramError` + + """ + result = await self._post( + "getForumTopicIconStickers", + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + return Sticker.de_list(result, self) + + async def create_forum_topic( + self, + chat_id: Union[str, int], + name: str, + icon_color: Optional[int] = None, + icon_custom_emoji_id: 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, + ) -> ForumTopic: + """ + Use this method to create a topic in a forum supergroup chat. The bot must be + an administrator in the chat for this to work and must have + :paramref:`~telegram.ChatAdministratorRights.can_manage_topics` administrator rights. + + .. versionadded:: 20.0 + + Args: + chat_id (:obj:`int` | :obj:`str`): |chat_id_group| + name (:obj:`str`): New topic name, + :tg-const:`telegram.constants.ForumTopicLimit.MIN_NAME_LENGTH`- + :tg-const:`telegram.constants.ForumTopicLimit.MAX_NAME_LENGTH` characters. + icon_color (:obj:`int`, optional): Color of the topic icon in RGB format. Currently, + must be one of :attr:`telegram.constants.ForumIconColor.BLUE`, + :attr:`telegram.constants.ForumIconColor.YELLOW`, + :attr:`telegram.constants.ForumIconColor.PURPLE`, + :attr:`telegram.constants.ForumIconColor.GREEN`, + :attr:`telegram.constants.ForumIconColor.PINK`, or + :attr:`telegram.constants.ForumIconColor.RED`. + icon_custom_emoji_id (:obj:`str`, optional): New unique identifier of the custom emoji + shown as the topic icon. Use :meth:`~telegram.Bot.get_forum_topic_icon_stickers` + to get all allowed custom emoji identifiers. + + Returns: + :class:`telegram.ForumTopic` + + Raises: + :class:`telegram.error.TelegramError` + """ + data: JSONDict = { + "chat_id": chat_id, + "name": name, + "icon_color": icon_color, + "icon_custom_emoji_id": icon_custom_emoji_id, + } + result = await self._post( + "createForumTopic", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + return ForumTopic.de_json(result, self) # type: ignore[return-value] + + async def edit_forum_topic( + self, + chat_id: Union[str, int], + message_thread_id: int, + name: Optional[str] = None, + icon_custom_emoji_id: 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: + """ + 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 the + :paramref:`~telegram.ChatAdministratorRights.can_manage_topics` administrator rights, + unless it is the creator of the topic. + + .. versionadded:: 20.0 + + Args: + chat_id (:obj:`int` | :obj:`str`): |chat_id_group| + message_thread_id (:obj:`int`): |message_thread_id| + name (:obj:`str`, optional): New topic name, + :tg-const:`telegram.constants.ForumTopicLimit.MIN_NAME_LENGTH`- + :tg-const:`telegram.constants.ForumTopicLimit.MAX_NAME_LENGTH` characters. If + not specified or empty, the current name of the topic will be kept. + icon_custom_emoji_id (:obj:`str`, optional): New unique identifier of the custom emoji + shown as the topic icon. Use :meth:`~telegram.Bot.get_forum_topic_icon_stickers` + to get all allowed custom emoji identifiers.Pass an empty string to remove the + icon. If not specified, the current icon will be kept. + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + Raises: + :class:`telegram.error.TelegramError` + + """ + data: JSONDict = { + "chat_id": chat_id, + "message_thread_id": message_thread_id, + "name": name, + "icon_custom_emoji_id": icon_custom_emoji_id, + } + return await self._post( + "editForumTopic", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def close_forum_topic( + self, + chat_id: Union[str, int], + message_thread_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: + """ + Use this method to close an open topic in a forum supergroup chat. The bot must + be an administrator in the chat for this to work and must have + :paramref:`~telegram.ChatAdministratorRights.can_manage_topics` administrator rights, + unless it is the creator of the topic. + + .. versionadded:: 20.0 + + Args: + chat_id (:obj:`int` | :obj:`str`): |chat_id_group| + message_thread_id (:obj:`int`): |message_thread_id| + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + Raises: + :class:`telegram.error.TelegramError` + + """ + data: JSONDict = { + "chat_id": chat_id, + "message_thread_id": message_thread_id, + } + return await self._post( + "closeForumTopic", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def reopen_forum_topic( + self, + chat_id: Union[str, int], + message_thread_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: + """ + Use this method to reopen a closed topic in a forum supergroup chat. The bot must + be an administrator in the chat for this to work and must have + :paramref:`~telegram.ChatAdministratorRights.can_manage_topics` administrator rights, + unless it is the creator of the topic. + + .. versionadded:: 20.0 + + Args: + chat_id (:obj:`int` | :obj:`str`): |chat_id_group| + message_thread_id (:obj:`int`): |message_thread_id| + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + Raises: + :class:`telegram.error.TelegramError` + + """ + data: JSONDict = { + "chat_id": chat_id, + "message_thread_id": message_thread_id, + } + return await self._post( + "reopenForumTopic", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def delete_forum_topic( + self, + chat_id: Union[str, int], + message_thread_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: + """ + Use this method to delete a forum topic along with all its messages in a forum supergroup + chat. The bot must be an administrator in the chat for this to work and must have + :paramref:`~telegram.ChatAdministratorRights.can_delete_messages` administrator rights. + + .. versionadded:: 20.0 + + Args: + chat_id (:obj:`int` | :obj:`str`): |chat_id_group| + message_thread_id (:obj:`int`): |message_thread_id| + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + Raises: + :class:`telegram.error.TelegramError` + + """ + data: JSONDict = { + "chat_id": chat_id, + "message_thread_id": message_thread_id, + } + return await self._post( + "deleteForumTopic", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def unpin_all_forum_topic_messages( + self, + chat_id: Union[str, int], + message_thread_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: + """ + Use this method to clear the list of pinned messages in a forum topic. The bot must + be an administrator in the chat for this to work and must have + :paramref:`~telegram.ChatAdministratorRights.can_pin_messages` administrator rights + in the supergroup. + + .. versionadded:: 20.0 + + Args: + chat_id (:obj:`int` | :obj:`str`): |chat_id_group| + message_thread_id (:obj:`int`): |message_thread_id| + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + Raises: + :class:`telegram.error.TelegramError` + + """ + data: JSONDict = { + "chat_id": chat_id, + "message_thread_id": message_thread_id, + } + return await self._post( + "unpinAllForumTopicMessages", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def unpin_all_general_forum_topic_messages( + self, + chat_id: Union[str, 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: + """ + Use this method to clear the list of pinned messages in a General forum topic. The bot must + be an administrator in the chat for this to work and must have + :paramref:`~telegram.ChatAdministratorRights.can_pin_messages` administrator rights in the + supergroup. + + .. versionadded:: 20.5 + + Args: + chat_id (:obj:`int` | :obj:`str`): |chat_id_group| + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + Raises: + :class:`telegram.error.TelegramError` + """ + data: JSONDict = {"chat_id": chat_id} + + return await self._post( + "unpinAllGeneralForumTopicMessages", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def edit_general_forum_topic( + self, + chat_id: Union[str, int], + name: 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: + """ + 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 the + :attr:`~telegram.ChatAdministratorRights.can_manage_topics` administrator rights. + + .. versionadded:: 20.0 + + Args: + chat_id (:obj:`int` | :obj:`str`): |chat_id_group| + name (:obj:`str`): New topic name, + :tg-const:`telegram.constants.ForumTopicLimit.MIN_NAME_LENGTH`- + :tg-const:`telegram.constants.ForumTopicLimit.MAX_NAME_LENGTH` characters. + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + Raises: + :class:`telegram.error.TelegramError` + + """ + data: JSONDict = {"chat_id": chat_id, "name": name} + + return await self._post( + "editGeneralForumTopic", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def close_general_forum_topic( + self, + chat_id: Union[str, 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: + """ + Use this method to close an open 'General' topic in a forum supergroup chat. The bot must + be an administrator in the chat for this to work and must have + :attr:`~telegram.ChatAdministratorRights.can_manage_topics` administrator rights. + + .. versionadded:: 20.0 + + Args: + chat_id (:obj:`int` | :obj:`str`): |chat_id_group| + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + Raises: + :class:`telegram.error.TelegramError` + + """ + data: JSONDict = {"chat_id": chat_id} + + return await self._post( + "closeGeneralForumTopic", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def reopen_general_forum_topic( + self, + chat_id: Union[str, 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: + """ + Use this method to reopen a closed 'General' topic in a forum supergroup chat. The bot must + be an administrator in the chat for this to work and must have + :attr:`~telegram.ChatAdministratorRights.can_manage_topics` administrator rights. + The topic will be automatically unhidden if it was hidden. + + .. versionadded:: 20.0 + + Args: + chat_id (:obj:`int` | :obj:`str`): |chat_id_group| + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + Raises: + :class:`telegram.error.TelegramError` + + """ + data: JSONDict = {"chat_id": chat_id} + + return await self._post( + "reopenGeneralForumTopic", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def hide_general_forum_topic( + self, + chat_id: Union[str, 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: + """ + Use this method to hide the 'General' topic in a forum supergroup chat. The bot must + be an administrator in the chat for this to work and must have + :attr:`~telegram.ChatAdministratorRights.can_manage_topics` administrator rights. + The topic will be automatically closed if it was open. + + .. versionadded:: 20.0 + + Args: + chat_id (:obj:`int` | :obj:`str`): |chat_id_group| + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + Raises: + :class:`telegram.error.TelegramError` + + """ + data: JSONDict = {"chat_id": chat_id} + + return await self._post( + "hideGeneralForumTopic", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def unhide_general_forum_topic( + self, + chat_id: Union[str, 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: + """ + Use this method to unhide the 'General' topic in a forum supergroup chat. The bot must + be an administrator in the chat for this to work and must have + :attr:`~telegram.ChatAdministratorRights.can_manage_topics` administrator rights. + + .. versionadded:: 20.0 + + Args: + chat_id (:obj:`int` | :obj:`str`): |chat_id_group| + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + Raises: + :class:`telegram.error.TelegramError` + + """ + data: JSONDict = {"chat_id": chat_id} + + return await self._post( + "unhideGeneralForumTopic", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def set_my_description( + self, + description: Optional[str] = None, + language_code: 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: + """ + Use this method to change the bot's description, which is shown in the chat with the bot + if the chat is empty. + + .. versionadded:: 20.2 + + Args: + description (:obj:`str`, optional): New bot description; + 0-:tg-const:`telegram.constants.BotDescriptionLimit.MAX_DESCRIPTION_LENGTH` + characters. Pass an empty string to remove the dedicated description for the given + language. + language_code (:obj:`str`, optional): A two-letter ISO 639-1 language code. If empty, + the description will be applied to all users for whose language there is no + dedicated description. + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + Raises: + :class:`telegram.error.TelegramError` + + """ + data: JSONDict = {"description": description, "language_code": language_code} + + return await self._post( + "setMyDescription", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def set_my_short_description( + self, + short_description: Optional[str] = None, + language_code: 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: + """ + Use this method to change the bot's short description, which is shown on the bot's profile + page and is sent together with the link when users share the bot. + + .. versionadded:: 20.2 + + Args: + short_description (:obj:`str`, optional): New short description for the bot; + 0-:tg-const:`telegram.constants.BotDescriptionLimit.MAX_SHORT_DESCRIPTION_LENGTH` + characters. Pass an empty string to remove the dedicated description for the given + language. + language_code (:obj:`str`, optional): A two-letter ISO 639-1 language code. If empty, + the description will be applied to all users for whose language there is no + dedicated description. + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + Raises: + :class:`telegram.error.TelegramError` + + """ + data: JSONDict = {"short_description": short_description, "language_code": language_code} + + return await self._post( + "setMyShortDescription", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def get_my_description( + self, + language_code: 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, + ) -> BotDescription: + """ + Use this method to get the current bot description for the given user language. + + Args: + language_code (:obj:`str`, optional): A two-letter ISO 639-1 language code or an empty + string. + + Returns: + :class:`telegram.BotDescription`: On success, the bot description is returned. + + Raises: + :class:`telegram.error.TelegramError` + + """ + data = {"language_code": language_code} + return BotDescription.de_json( # type: ignore[return-value] + await self._post( + "getMyDescription", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ), + bot=self, + ) + + async def get_my_short_description( + self, + language_code: 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, + ) -> BotShortDescription: + """ + Use this method to get the current bot short description for the given user language. + + Args: + language_code (:obj:`str`, optional): A two-letter ISO 639-1 language code or an empty + string. + + Returns: + :class:`telegram.BotShortDescription`: On success, the bot short description is + returned. + + Raises: + :class:`telegram.error.TelegramError` + + """ + data = {"language_code": language_code} + return BotShortDescription.de_json( # type: ignore[return-value] + await self._post( + "getMyShortDescription", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ), + bot=self, + ) + + async def set_my_name( + self, + name: Optional[str] = None, + language_code: 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: + """ + Use this method to change the bot's name. + + .. versionadded:: 20.3 + + Args: + name (:obj:`str`, optional): New bot name; + 0-:tg-const:`telegram.constants.BotNameLimit.MAX_NAME_LENGTH` + characters. Pass an empty string to remove the dedicated name for the given + language. + + Caution: + If :paramref:`language_code` is not specified, a :paramref:`name` *must* + be specified. + language_code (:obj:`str`, optional): A two-letter ISO 639-1 language code. If empty, + the name will be applied to all users for whose language there is no + dedicated name. + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + Raises: + :class:`telegram.error.TelegramError` + + """ + data: JSONDict = {"name": name, "language_code": language_code} + + return await self._post( + "setMyName", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def get_my_name( + self, + language_code: 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, + ) -> BotName: + """ + Use this method to get the current bot name for the given user language. + + Args: + language_code (:obj:`str`, optional): A two-letter ISO 639-1 language code or an empty + string. + + Returns: + :class:`telegram.BotName`: On success, the bot name is returned. + + Raises: + :class:`telegram.error.TelegramError` + + """ + data = {"language_code": language_code} + return BotName.de_json( # type: ignore[return-value] + await self._post( + "getMyName", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ), + bot=self, + ) + + async def get_user_chat_boosts( + self, + chat_id: Union[str, int], + 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, + ) -> UserChatBoosts: + """ + Use this method to get the list of boosts added to a chat by a user. Requires + administrator rights in the chat. + + .. versionadded:: 20.8 + + Args: + chat_id (:obj:`int` | :obj:`str`): |chat_id_channel| + user_id (:obj:`int`): Unique identifier of the target user. + + Returns: + :class:`telegram.UserChatBoosts`: On success, the object containing the list of boosts + is returned. + + Raises: + :class:`telegram.error.TelegramError` + """ + data: JSONDict = {"chat_id": chat_id, "user_id": user_id} + return UserChatBoosts.de_json( # type: ignore[return-value] + await self._post( + "getUserChatBoosts", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ), + bot=self, + ) + + async def set_message_reaction( + self, + chat_id: Union[str, int], + message_id: int, + reaction: Optional[Union[Sequence[Union[ReactionType, str]], ReactionType, str]] = None, + is_big: 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: + """ + Use this method to change the chosen reactions on a message. Service messages can't be + reacted to. Automatically forwarded messages from a channel to its discussion group have + the same available reactions as messages in the channel. Bots can't use paid reactions. + + .. versionadded:: 20.8 + + Args: + chat_id (:obj:`int` | :obj:`str`): |chat_id_channel| + message_id (:obj:`int`): Identifier of the target message. If the message belongs to a + media group, the reaction is set to the first non-deleted message in the group + instead. + reaction (Sequence[:class:`telegram.ReactionType` | :obj:`str`] | \ + :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. Paid + reactions can't be used by bots. + + Tip: + Passed :obj:`str` values will be converted to either + :class:`telegram.ReactionTypeEmoji` or + :class:`telegram.ReactionTypeCustomEmoji` + depending on whether they are listed in + :class:`~telegram.constants.ReactionEmoji`. + + is_big (:obj:`bool`, optional): Pass :obj:`True` to set the reaction with a big + animation. + + Returns: + :obj:`bool` On success, :obj:`True` is returned. + + Raises: + :class:`telegram.error.TelegramError` + """ + allowed_reactions: Set[str] = set(ReactionEmoji) + parsed_reaction = ( + [ + ( + entry + if isinstance(entry, ReactionType) + else ( + ReactionTypeEmoji(emoji=entry) + if entry in allowed_reactions + else ReactionTypeCustomEmoji(custom_emoji_id=entry) + ) + ) + for entry in ( + [reaction] if isinstance(reaction, (ReactionType, str)) else reaction + ) + ] + if reaction is not None + else None + ) + + data: JSONDict = { + "chat_id": chat_id, + "message_id": message_id, + "reaction": parsed_reaction, + "is_big": is_big, + } + + return await self._post( + "setMessageReaction", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def get_business_connection( + self, + business_connection_id: 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, + ) -> BusinessConnection: + """ + Use this method to get information about the connection of the bot with a business account. + + .. versionadded:: 21.1 + + Args: + business_connection_id (:obj:`str`): Unique identifier of the business connection. + + Returns: + :class:`telegram.BusinessConnection`: On success, the object containing the business + connection information is returned. + + Raises: + :class:`telegram.error.TelegramError` + """ + data: JSONDict = {"business_connection_id": business_connection_id} + return BusinessConnection.de_json( # type: ignore[return-value] + await self._post( + "getBusinessConnection", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ), + bot=self, + ) + + async def replace_sticker_in_set( + self, + user_id: int, + name: str, + old_sticker: str, + sticker: "InputSticker", + *, + 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: + """Use this method to replace an existing sticker in a sticker set with a new one. + The method is equivalent to calling :meth:`delete_sticker_from_set`, + then :meth:`add_sticker_to_set`, then :meth:`set_sticker_position_in_set`. + + .. versionadded:: 21.1 + + 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 (: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. + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + Raises: + :class:`telegram.error.TelegramError` + """ + data: JSONDict = { + "user_id": user_id, + "name": name, + "old_sticker": old_sticker, + "sticker": sticker, + } + + return await self._post( + "replaceStickerInSet", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def refund_star_payment( + self, + user_id: int, + telegram_payment_charge_id: 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: + """Refunds a successful payment in |tg_stars|. + + .. versionadded:: 21.3 + + Args: + user_id (:obj:`int`): User identifier of the user whose payment will be refunded. + telegram_payment_charge_id (:obj:`str`): Telegram payment identifier. + + 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, + } + + return await self._post( + "refundStarPayment", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def get_star_transactions( + self, + offset: Optional[int] = None, + limit: 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, + ) -> StarTransactions: + """Returns the bot's Telegram Star transactions in chronological order. + + .. versionadded:: 21.4 + + Args: + offset (:obj:`int`, optional): Number of transactions to skip in the response. + limit (:obj:`int`, optional): The maximum number of transactions to be retrieved. + Values between :tg-const:`telegram.constants.StarTransactionsLimit.MIN_LIMIT`- + :tg-const:`telegram.constants.StarTransactionsLimit.MAX_LIMIT` are accepted. + Defaults to :tg-const:`telegram.constants.StarTransactionsLimit.MAX_LIMIT`. + + Returns: + :class:`telegram.StarTransactions`: On success. + + Raises: + :class:`telegram.error.TelegramError` + """ + + data: JSONDict = {"offset": offset, "limit": limit} + + return StarTransactions.de_json( # type: ignore[return-value] + await self._post( + "getStarTransactions", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ), + bot=self, + ) + + async def send_paid_media( + self, + chat_id: Union[str, int], + star_count: int, + media: Sequence["InputPaidMedia"], + caption: Optional[str] = None, + parse_mode: ODVInput[str] = DEFAULT_NONE, + caption_entities: Optional[Sequence["MessageEntity"]] = None, + show_caption_above_media: Optional[bool] = None, + disable_notification: ODVInput[bool] = DEFAULT_NONE, + protect_content: ODVInput[bool] = DEFAULT_NONE, + reply_parameters: Optional["ReplyParameters"] = None, + reply_markup: Optional[ReplyMarkup] = None, + business_connection_id: Optional[str] = None, + *, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + reply_to_message_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, + ) -> Message: + """Use this method to send paid media. + + .. versionadded:: 21.4 + + Args: + 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. + media (Sequence[:class:`telegram.InputPaidMedia`]): A list describing the media to be + sent; up to :tg-const:`telegram.constants.MediaGroupLimit.MAX_MEDIA_LENGTH` items. + 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| + caption_entities (Sequence[:class:`telegram.MessageEntity`], optional): + |caption_entities| + show_caption_above_media (:obj:`bool`, optional): Pass |show_cap_above_med| + disable_notification (:obj:`bool`, optional): |disable_notification| + protect_content (:obj:`bool`, optional): |protect_content| + reply_parameters (:class:`telegram.ReplyParameters`, optional): |reply_parameters| + reply_markup (:class:`InlineKeyboardMarkup` | :class:`ReplyKeyboardMarkup` | \ + :class:`ReplyKeyboardRemove` | :class:`ForceReply`, optional): + Additional interface options. An object for an inline keyboard, custom reply + keyboard, instructions to remove reply keyboard or to force a reply from the user. + business_connection_id (:obj:`str`, optional): |business_id_str| + + .. versionadded:: 21.5 + + Keyword Args: + allow_sending_without_reply (:obj:`bool`, optional): |allow_sending_without_reply| + Mutually exclusive with :paramref:`reply_parameters`, which this is a convenience + parameter for + + reply_to_message_id (:obj:`int`, optional): |reply_to_msg_id| + Mutually exclusive with :paramref:`reply_parameters`, which this is a convenience + parameter for + + Returns: + :class:`telegram.Message`: On success, the sent message is returned. + + Raises: + :class:`telegram.error.TelegramError` + """ + + data: JSONDict = { + "chat_id": chat_id, + "star_count": star_count, + "media": media, + "show_caption_above_media": show_caption_above_media, + } + + return await self._send_message( + "sendPaidMedia", + data, + caption=caption, + parse_mode=parse_mode, + caption_entities=caption_entities, + disable_notification=disable_notification, + protect_content=protect_content, + reply_parameters=reply_parameters, + reply_markup=reply_markup, + allow_sending_without_reply=allow_sending_without_reply, + reply_to_message_id=reply_to_message_id, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + business_connection_id=business_connection_id, + ) + + async def create_chat_subscription_invite_link( + self, + chat_id: Union[str, int], + subscription_period: int, + 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`): 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). + 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) # type: ignore[return-value] + + 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) # type: ignore[return-value] + + def to_dict(self, recursive: bool = True) -> JSONDict: # noqa: ARG002 + """See :meth:`telegram.TelegramObject.to_dict`.""" + data: JSONDict = {"id": self.id, "username": self.username, "first_name": self.first_name} + + if self.last_name: + data["last_name"] = self.last_name + + return data + + # camelCase aliases + getMe = get_me + """Alias for :meth:`get_me`""" + sendMessage = send_message + """Alias for :meth:`send_message`""" + deleteMessage = delete_message + """Alias for :meth:`delete_message`""" + deleteMessages = delete_messages + """Alias for :meth:`delete_messages`""" + forwardMessage = forward_message + """Alias for :meth:`forward_message`""" + forwardMessages = forward_messages + """Alias for :meth:`forward_messages`""" + sendPhoto = send_photo + """Alias for :meth:`send_photo`""" + sendAudio = send_audio + """Alias for :meth:`send_audio`""" + sendDocument = send_document + """Alias for :meth:`send_document`""" + sendSticker = send_sticker + """Alias for :meth:`send_sticker`""" + sendVideo = send_video + """Alias for :meth:`send_video`""" + sendAnimation = send_animation + """Alias for :meth:`send_animation`""" + sendVoice = send_voice + """Alias for :meth:`send_voice`""" + sendVideoNote = send_video_note + """Alias for :meth:`send_video_note`""" + sendMediaGroup = send_media_group + """Alias for :meth:`send_media_group`""" + sendLocation = send_location + """Alias for :meth:`send_location`""" + editMessageLiveLocation = edit_message_live_location + """Alias for :meth:`edit_message_live_location`""" + stopMessageLiveLocation = stop_message_live_location + """Alias for :meth:`stop_message_live_location`""" + sendVenue = send_venue + """Alias for :meth:`send_venue`""" + sendContact = send_contact + """Alias for :meth:`send_contact`""" + sendGame = send_game + """Alias for :meth:`send_game`""" + sendChatAction = send_chat_action + """Alias for :meth:`send_chat_action`""" + answerInlineQuery = answer_inline_query + """Alias for :meth:`answer_inline_query`""" + getUserProfilePhotos = get_user_profile_photos + """Alias for :meth:`get_user_profile_photos`""" + getFile = get_file + """Alias for :meth:`get_file`""" + banChatMember = ban_chat_member + """Alias for :meth:`ban_chat_member`""" + banChatSenderChat = ban_chat_sender_chat + """Alias for :meth:`ban_chat_sender_chat`""" + unbanChatMember = unban_chat_member + """Alias for :meth:`unban_chat_member`""" + unbanChatSenderChat = unban_chat_sender_chat + """Alias for :meth:`unban_chat_sender_chat`""" + answerCallbackQuery = answer_callback_query + """Alias for :meth:`answer_callback_query`""" + editMessageText = edit_message_text + """Alias for :meth:`edit_message_text`""" + editMessageCaption = edit_message_caption + """Alias for :meth:`edit_message_caption`""" + editMessageMedia = edit_message_media + """Alias for :meth:`edit_message_media`""" + editMessageReplyMarkup = edit_message_reply_markup + """Alias for :meth:`edit_message_reply_markup`""" + getUpdates = get_updates + """Alias for :meth:`get_updates`""" + setWebhook = set_webhook + """Alias for :meth:`set_webhook`""" + deleteWebhook = delete_webhook + """Alias for :meth:`delete_webhook`""" + leaveChat = leave_chat + """Alias for :meth:`leave_chat`""" + getChat = get_chat + """Alias for :meth:`get_chat`""" + getChatAdministrators = get_chat_administrators + """Alias for :meth:`get_chat_administrators`""" + getChatMember = get_chat_member + """Alias for :meth:`get_chat_member`""" + setChatStickerSet = set_chat_sticker_set + """Alias for :meth:`set_chat_sticker_set`""" + deleteChatStickerSet = delete_chat_sticker_set + """Alias for :meth:`delete_chat_sticker_set`""" + getChatMemberCount = get_chat_member_count + """Alias for :meth:`get_chat_member_count`""" + getWebhookInfo = get_webhook_info + """Alias for :meth:`get_webhook_info`""" + setGameScore = set_game_score + """Alias for :meth:`set_game_score`""" + getGameHighScores = get_game_high_scores + """Alias for :meth:`get_game_high_scores`""" + sendInvoice = send_invoice + """Alias for :meth:`send_invoice`""" + answerShippingQuery = answer_shipping_query + """Alias for :meth:`answer_shipping_query`""" + answerPreCheckoutQuery = answer_pre_checkout_query + """Alias for :meth:`answer_pre_checkout_query`""" + answerWebAppQuery = answer_web_app_query + """Alias for :meth:`answer_web_app_query`""" + restrictChatMember = restrict_chat_member + """Alias for :meth:`restrict_chat_member`""" + promoteChatMember = promote_chat_member + """Alias for :meth:`promote_chat_member`""" + setChatPermissions = set_chat_permissions + """Alias for :meth:`set_chat_permissions`""" + setChatAdministratorCustomTitle = set_chat_administrator_custom_title + """Alias for :meth:`set_chat_administrator_custom_title`""" + exportChatInviteLink = export_chat_invite_link + """Alias for :meth:`export_chat_invite_link`""" + createChatInviteLink = create_chat_invite_link + """Alias for :meth:`create_chat_invite_link`""" + editChatInviteLink = edit_chat_invite_link + """Alias for :meth:`edit_chat_invite_link`""" + revokeChatInviteLink = revoke_chat_invite_link + """Alias for :meth:`revoke_chat_invite_link`""" + approveChatJoinRequest = approve_chat_join_request + """Alias for :meth:`approve_chat_join_request`""" + declineChatJoinRequest = decline_chat_join_request + """Alias for :meth:`decline_chat_join_request`""" + setChatPhoto = set_chat_photo + """Alias for :meth:`set_chat_photo`""" + deleteChatPhoto = delete_chat_photo + """Alias for :meth:`delete_chat_photo`""" + setChatTitle = set_chat_title + """Alias for :meth:`set_chat_title`""" + setChatDescription = set_chat_description + """Alias for :meth:`set_chat_description`""" + pinChatMessage = pin_chat_message + """Alias for :meth:`pin_chat_message`""" + unpinChatMessage = unpin_chat_message + """Alias for :meth:`unpin_chat_message`""" + unpinAllChatMessages = unpin_all_chat_messages + """Alias for :meth:`unpin_all_chat_messages`""" + getCustomEmojiStickers = get_custom_emoji_stickers + """Alias for :meth:`get_custom_emoji_stickers`""" + getStickerSet = get_sticker_set + """Alias for :meth:`get_sticker_set`""" + uploadStickerFile = upload_sticker_file + """Alias for :meth:`upload_sticker_file`""" + createNewStickerSet = create_new_sticker_set + """Alias for :meth:`create_new_sticker_set`""" + addStickerToSet = add_sticker_to_set + """Alias for :meth:`add_sticker_to_set`""" + setStickerPositionInSet = set_sticker_position_in_set + """Alias for :meth:`set_sticker_position_in_set`""" + deleteStickerFromSet = delete_sticker_from_set + """Alias for :meth:`delete_sticker_from_set`""" + setStickerSetThumbnail = set_sticker_set_thumbnail + """Alias for :meth:`set_sticker_set_thumbnail`""" + setPassportDataErrors = set_passport_data_errors + """Alias for :meth:`set_passport_data_errors`""" + sendPoll = send_poll + """Alias for :meth:`send_poll`""" + stopPoll = stop_poll + """Alias for :meth:`stop_poll`""" + sendDice = send_dice + """Alias for :meth:`send_dice`""" + getMyCommands = get_my_commands + """Alias for :meth:`get_my_commands`""" + setMyCommands = set_my_commands + """Alias for :meth:`set_my_commands`""" + deleteMyCommands = delete_my_commands + """Alias for :meth:`delete_my_commands`""" + logOut = log_out + """Alias for :meth:`log_out`""" + copyMessage = copy_message + """Alias for :meth:`copy_message`""" + copyMessages = copy_messages + """Alias for :meth:`copy_messages`""" + getChatMenuButton = get_chat_menu_button + """Alias for :meth:`get_chat_menu_button`""" + setChatMenuButton = set_chat_menu_button + """Alias for :meth:`set_chat_menu_button`""" + getMyDefaultAdministratorRights = get_my_default_administrator_rights + """Alias for :meth:`get_my_default_administrator_rights`""" + setMyDefaultAdministratorRights = set_my_default_administrator_rights + """Alias for :meth:`set_my_default_administrator_rights`""" + createInvoiceLink = create_invoice_link + """Alias for :meth:`create_invoice_link`""" + getForumTopicIconStickers = get_forum_topic_icon_stickers + """Alias for :meth:`get_forum_topic_icon_stickers`""" + createForumTopic = create_forum_topic + """Alias for :meth:`create_forum_topic`""" + editForumTopic = edit_forum_topic + """Alias for :meth:`edit_forum_topic`""" + closeForumTopic = close_forum_topic + """Alias for :meth:`close_forum_topic`""" + reopenForumTopic = reopen_forum_topic + """Alias for :meth:`reopen_forum_topic`""" + deleteForumTopic = delete_forum_topic + """Alias for :meth:`delete_forum_topic`""" + unpinAllForumTopicMessages = unpin_all_forum_topic_messages + """Alias for :meth:`unpin_all_forum_topic_messages`""" + editGeneralForumTopic = edit_general_forum_topic + """Alias for :meth:`edit_general_forum_topic`""" + closeGeneralForumTopic = close_general_forum_topic + """Alias for :meth:`close_general_forum_topic`""" + reopenGeneralForumTopic = reopen_general_forum_topic + """Alias for :meth:`reopen_general_forum_topic`""" + hideGeneralForumTopic = hide_general_forum_topic + """Alias for :meth:`hide_general_forum_topic`""" + unhideGeneralForumTopic = unhide_general_forum_topic + """Alias for :meth:`unhide_general_forum_topic`""" + setMyDescription = set_my_description + """Alias for :meth:`set_my_description`""" + setMyShortDescription = set_my_short_description + """Alias for :meth:`set_my_short_description`""" + getMyDescription = get_my_description + """Alias for :meth:`get_my_description`""" + getMyShortDescription = get_my_short_description + """Alias for :meth:`get_my_short_description`""" + setCustomEmojiStickerSetThumbnail = set_custom_emoji_sticker_set_thumbnail + """Alias for :meth:`set_custom_emoji_sticker_set_thumbnail`""" + setStickerSetTitle = set_sticker_set_title + """Alias for :meth:`set_sticker_set_title`""" + deleteStickerSet = delete_sticker_set + """Alias for :meth:`delete_sticker_set`""" + setStickerEmojiList = set_sticker_emoji_list + """Alias for :meth:`set_sticker_emoji_list`""" + setStickerKeywords = set_sticker_keywords + """Alias for :meth:`set_sticker_keywords`""" + setStickerMaskPosition = set_sticker_mask_position + """Alias for :meth:`set_sticker_mask_position`""" + setMyName = set_my_name + """Alias for :meth:`set_my_name`""" + getMyName = get_my_name + """Alias for :meth:`get_my_name`""" + unpinAllGeneralForumTopicMessages = unpin_all_general_forum_topic_messages + """Alias for :meth:`unpin_all_general_forum_topic_messages`""" + getUserChatBoosts = get_user_chat_boosts + """Alias for :meth:`get_user_chat_boosts`""" + setMessageReaction = set_message_reaction + """Alias for :meth:`set_message_reaction`""" + getBusinessConnection = get_business_connection + """Alias for :meth:`get_business_connection`""" + replaceStickerInSet = replace_sticker_in_set + """Alias for :meth:`replace_sticker_in_set`""" + refundStarPayment = refund_star_payment + """Alias for :meth:`refund_star_payment`""" + getStarTransactions = get_star_transactions + """Alias for :meth:`get_star_transactions`""" + 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`""" diff --git a/_botcommand.py b/_botcommand.py new file mode 100644 index 0000000000000000000000000000000000000000..972db7c240202040dae9c24daad6fe915b96d884 --- /dev/null +++ b/_botcommand.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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 Bot Command.""" + +from typing import Final, Optional + +from telegram import constants +from telegram._telegramobject import TelegramObject +from telegram._utils.types import JSONDict + + +class BotCommand(TelegramObject): + """ + This object represents a bot command. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`command` and :attr:`description` are equal. + + Args: + command (:obj:`str`): Text of the command; :tg-const:`telegram.BotCommand.MIN_COMMAND`- + :tg-const:`telegram.BotCommand.MAX_COMMAND` characters. Can contain only lowercase + English letters, digits and underscores. + description (:obj:`str`): Description of the command; + :tg-const:`telegram.BotCommand.MIN_DESCRIPTION`- + :tg-const:`telegram.BotCommand.MAX_DESCRIPTION` characters. + + Attributes: + command (:obj:`str`): Text of the command; :tg-const:`telegram.BotCommand.MIN_COMMAND`- + :tg-const:`telegram.BotCommand.MAX_COMMAND` characters. Can contain only lowercase + English letters, digits and underscores. + description (:obj:`str`): Description of the command; + :tg-const:`telegram.BotCommand.MIN_DESCRIPTION`- + :tg-const:`telegram.BotCommand.MAX_DESCRIPTION` characters. + + """ + + __slots__ = ("command", "description") + + def __init__(self, command: str, description: str, *, api_kwargs: Optional[JSONDict] = None): + super().__init__(api_kwargs=api_kwargs) + self.command: str = command + self.description: str = description + + self._id_attrs = (self.command, self.description) + + self._freeze() + + MIN_COMMAND: Final[int] = constants.BotCommandLimit.MIN_COMMAND + """:const:`telegram.constants.BotCommandLimit.MIN_COMMAND` + + .. versionadded:: 20.0 + """ + MAX_COMMAND: Final[int] = constants.BotCommandLimit.MAX_COMMAND + """:const:`telegram.constants.BotCommandLimit.MAX_COMMAND` + + .. versionadded:: 20.0 + """ + MIN_DESCRIPTION: Final[int] = constants.BotCommandLimit.MIN_DESCRIPTION + """:const:`telegram.constants.BotCommandLimit.MIN_DESCRIPTION` + + .. versionadded:: 20.0 + """ + MAX_DESCRIPTION: Final[int] = constants.BotCommandLimit.MAX_DESCRIPTION + """:const:`telegram.constants.BotCommandLimit.MAX_DESCRIPTION` + + .. versionadded:: 20.0 + """ diff --git a/_botcommandscope.py b/_botcommandscope.py new file mode 100644 index 0000000000000000000000000000000000000000..73cafd175998c144fcf3a22b2f3954ba24975cd7 --- /dev/null +++ b/_botcommandscope.py @@ -0,0 +1,273 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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/]. +# pylint: disable=redefined-builtin +"""This module contains objects representing Telegram bot command scopes.""" +from typing import TYPE_CHECKING, Dict, Final, Optional, Type, Union + +from telegram import constants +from telegram._telegramobject import TelegramObject +from telegram._utils import enum +from telegram._utils.types import JSONDict + +if TYPE_CHECKING: + from telegram import Bot + + +class BotCommandScope(TelegramObject): + """Base class for objects that represent the scope to which bot commands are applied. + Currently, the following 7 scopes are supported: + + * :class:`telegram.BotCommandScopeDefault` + * :class:`telegram.BotCommandScopeAllPrivateChats` + * :class:`telegram.BotCommandScopeAllGroupChats` + * :class:`telegram.BotCommandScopeAllChatAdministrators` + * :class:`telegram.BotCommandScopeChat` + * :class:`telegram.BotCommandScopeChatAdministrators` + * :class:`telegram.BotCommandScopeChatMember` + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`type` is equal. For subclasses with additional attributes, + the notion of equality is overridden. + + Note: + Please see the `official docs`_ on how Telegram determines which commands to display. + + .. _`official docs`: https://core.telegram.org/bots/api#determining-list-of-commands + + .. versionadded:: 13.7 + + Args: + type (:obj:`str`): Scope type. + + Attributes: + type (:obj:`str`): Scope type. + """ + + __slots__ = ("type",) + + DEFAULT: Final[str] = constants.BotCommandScopeType.DEFAULT + """:const:`telegram.constants.BotCommandScopeType.DEFAULT`""" + ALL_PRIVATE_CHATS: Final[str] = constants.BotCommandScopeType.ALL_PRIVATE_CHATS + """:const:`telegram.constants.BotCommandScopeType.ALL_PRIVATE_CHATS`""" + ALL_GROUP_CHATS: Final[str] = constants.BotCommandScopeType.ALL_GROUP_CHATS + """:const:`telegram.constants.BotCommandScopeType.ALL_GROUP_CHATS`""" + ALL_CHAT_ADMINISTRATORS: Final[str] = constants.BotCommandScopeType.ALL_CHAT_ADMINISTRATORS + """:const:`telegram.constants.BotCommandScopeType.ALL_CHAT_ADMINISTRATORS`""" + CHAT: Final[str] = constants.BotCommandScopeType.CHAT + """:const:`telegram.constants.BotCommandScopeType.CHAT`""" + CHAT_ADMINISTRATORS: Final[str] = constants.BotCommandScopeType.CHAT_ADMINISTRATORS + """:const:`telegram.constants.BotCommandScopeType.CHAT_ADMINISTRATORS`""" + CHAT_MEMBER: Final[str] = constants.BotCommandScopeType.CHAT_MEMBER + """:const:`telegram.constants.BotCommandScopeType.CHAT_MEMBER`""" + + def __init__(self, type: str, *, api_kwargs: Optional[JSONDict] = None): + super().__init__(api_kwargs=api_kwargs) + self.type: str = enum.get_member(constants.BotCommandScopeType, type, type) + self._id_attrs = (self.type,) + + self._freeze() + + @classmethod + def de_json( + cls, data: Optional[JSONDict], bot: Optional["Bot"] = None + ) -> Optional["BotCommandScope"]: + """Converts JSON data to the appropriate :class:`BotCommandScope` object, i.e. takes + care of selecting the correct subclass. + + Args: + data (Dict[:obj:`str`, ...]): The JSON data. + bot (:class:`telegram.Bot`, optional): The bot associated with this object. Defaults to + :obj:`None`, in which case shortcut methods will not be available. + + .. versionchanged:: 21.4 + :paramref:`bot` is now optional and defaults to :obj:`None` + + Returns: + The Telegram object. + + """ + data = cls._parse_data(data) + + if not data: + return None + + _class_mapping: Dict[str, Type[BotCommandScope]] = { + cls.DEFAULT: BotCommandScopeDefault, + cls.ALL_PRIVATE_CHATS: BotCommandScopeAllPrivateChats, + cls.ALL_GROUP_CHATS: BotCommandScopeAllGroupChats, + cls.ALL_CHAT_ADMINISTRATORS: BotCommandScopeAllChatAdministrators, + cls.CHAT: BotCommandScopeChat, + cls.CHAT_ADMINISTRATORS: BotCommandScopeChatAdministrators, + cls.CHAT_MEMBER: BotCommandScopeChatMember, + } + + if cls is BotCommandScope and data.get("type") in _class_mapping: + return _class_mapping[data.pop("type")].de_json(data=data, bot=bot) + return super().de_json(data=data, bot=bot) + + +class BotCommandScopeDefault(BotCommandScope): + """Represents the default scope of bot commands. Default commands are used if no commands with + a `narrower scope`_ are specified for the user. + + .. _`narrower scope`: https://core.telegram.org/bots/api#determining-list-of-commands + + .. versionadded:: 13.7 + Attributes: + type (:obj:`str`): Scope type :tg-const:`telegram.BotCommandScope.DEFAULT`. + """ + + __slots__ = () + + def __init__(self, *, api_kwargs: Optional[JSONDict] = None): + super().__init__(type=BotCommandScope.DEFAULT, api_kwargs=api_kwargs) + self._freeze() + + +class BotCommandScopeAllPrivateChats(BotCommandScope): + """Represents the scope of bot commands, covering all private chats. + + .. versionadded:: 13.7 + + Attributes: + type (:obj:`str`): Scope type :tg-const:`telegram.BotCommandScope.ALL_PRIVATE_CHATS`. + """ + + __slots__ = () + + def __init__(self, *, api_kwargs: Optional[JSONDict] = None): + super().__init__(type=BotCommandScope.ALL_PRIVATE_CHATS, api_kwargs=api_kwargs) + self._freeze() + + +class BotCommandScopeAllGroupChats(BotCommandScope): + """Represents the scope of bot commands, covering all group and supergroup chats. + + .. versionadded:: 13.7 + Attributes: + type (:obj:`str`): Scope type :tg-const:`telegram.BotCommandScope.ALL_GROUP_CHATS`. + """ + + __slots__ = () + + def __init__(self, *, api_kwargs: Optional[JSONDict] = None): + super().__init__(type=BotCommandScope.ALL_GROUP_CHATS, api_kwargs=api_kwargs) + self._freeze() + + +class BotCommandScopeAllChatAdministrators(BotCommandScope): + """Represents the scope of bot commands, covering all group and supergroup chat administrators. + + .. versionadded:: 13.7 + Attributes: + type (:obj:`str`): Scope type :tg-const:`telegram.BotCommandScope.ALL_CHAT_ADMINISTRATORS`. + """ + + __slots__ = () + + def __init__(self, *, api_kwargs: Optional[JSONDict] = None): + super().__init__(type=BotCommandScope.ALL_CHAT_ADMINISTRATORS, api_kwargs=api_kwargs) + self._freeze() + + +class BotCommandScopeChat(BotCommandScope): + """Represents the scope of bot commands, covering a specific chat. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`type` and :attr:`chat_id` are equal. + + .. versionadded:: 13.7 + + Args: + chat_id (:obj:`str` | :obj:`int`): |chat_id_group| + + Attributes: + type (:obj:`str`): Scope type :tg-const:`telegram.BotCommandScope.CHAT`. + chat_id (:obj:`str` | :obj:`int`): |chat_id_group| + """ + + __slots__ = ("chat_id",) + + def __init__(self, chat_id: Union[str, int], *, api_kwargs: Optional[JSONDict] = None): + super().__init__(type=BotCommandScope.CHAT, api_kwargs=api_kwargs) + with self._unfrozen(): + self.chat_id: Union[str, int] = ( + chat_id if isinstance(chat_id, str) and chat_id.startswith("@") else int(chat_id) + ) + self._id_attrs = (self.type, self.chat_id) + + +class BotCommandScopeChatAdministrators(BotCommandScope): + """Represents the scope of bot commands, covering all administrators of a specific group or + supergroup chat. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`type` and :attr:`chat_id` are equal. + + .. versionadded:: 13.7 + + Args: + chat_id (:obj:`str` | :obj:`int`): |chat_id_group| + Attributes: + type (:obj:`str`): Scope type :tg-const:`telegram.BotCommandScope.CHAT_ADMINISTRATORS`. + chat_id (:obj:`str` | :obj:`int`): |chat_id_group| + """ + + __slots__ = ("chat_id",) + + def __init__(self, chat_id: Union[str, int], *, api_kwargs: Optional[JSONDict] = None): + super().__init__(type=BotCommandScope.CHAT_ADMINISTRATORS, api_kwargs=api_kwargs) + with self._unfrozen(): + self.chat_id: Union[str, int] = ( + chat_id if isinstance(chat_id, str) and chat_id.startswith("@") else int(chat_id) + ) + self._id_attrs = (self.type, self.chat_id) + + +class BotCommandScopeChatMember(BotCommandScope): + """Represents the scope of bot commands, covering a specific member of a group or supergroup + chat. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`type`, :attr:`chat_id` and :attr:`user_id` are equal. + + .. versionadded:: 13.7 + + Args: + chat_id (:obj:`str` | :obj:`int`): |chat_id_group| + user_id (:obj:`int`): Unique identifier of the target user. + + Attributes: + type (:obj:`str`): Scope type :tg-const:`telegram.BotCommandScope.CHAT_MEMBER`. + chat_id (:obj:`str` | :obj:`int`): |chat_id_group| + user_id (:obj:`int`): Unique identifier of the target user. + """ + + __slots__ = ("chat_id", "user_id") + + def __init__( + self, chat_id: Union[str, int], user_id: int, *, api_kwargs: Optional[JSONDict] = None + ): + super().__init__(type=BotCommandScope.CHAT_MEMBER, api_kwargs=api_kwargs) + with self._unfrozen(): + self.chat_id: Union[str, int] = ( + chat_id if isinstance(chat_id, str) and chat_id.startswith("@") else int(chat_id) + ) + self.user_id: int = user_id + self._id_attrs = (self.type, self.chat_id, self.user_id) diff --git a/_botdescription.py b/_botdescription.py new file mode 100644 index 0000000000000000000000000000000000000000..e2a9d36df1d5f099327cae91f125e307e024114a --- /dev/null +++ b/_botdescription.py @@ -0,0 +1,77 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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 two objects that represent a Telegram bots (short) description.""" +from typing import Optional + +from telegram._telegramobject import TelegramObject +from telegram._utils.types import JSONDict + + +class BotDescription(TelegramObject): + """This object represents the bot's description. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`description` is equal. + + .. versionadded:: 20.2 + + Args: + description (:obj:`str`): The bot's description. + + Attributes: + description (:obj:`str`): The bot's description. + + """ + + __slots__ = ("description",) + + def __init__(self, description: str, *, api_kwargs: Optional[JSONDict] = None): + super().__init__(api_kwargs=api_kwargs) + self.description: str = description + + self._id_attrs = (self.description,) + + self._freeze() + + +class BotShortDescription(TelegramObject): + """This object represents the bot's short description. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`short_description` is equal. + + .. versionadded:: 20.2 + + Args: + short_description (:obj:`str`): The bot's short description. + + Attributes: + short_description (:obj:`str`): The bot's short description. + + """ + + __slots__ = ("short_description",) + + def __init__(self, short_description: str, *, api_kwargs: Optional[JSONDict] = None): + super().__init__(api_kwargs=api_kwargs) + self.short_description: str = short_description + + self._id_attrs = (self.short_description,) + + self._freeze() diff --git a/_botname.py b/_botname.py new file mode 100644 index 0000000000000000000000000000000000000000..2a57ea39f0d8770198db1404b3252700dc51ea3c --- /dev/null +++ b/_botname.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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 represent a Telegram bots name.""" +from typing import Final, Optional + +from telegram import constants +from telegram._telegramobject import TelegramObject +from telegram._utils.types import JSONDict + + +class BotName(TelegramObject): + """This object represents the bot's name. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`name` is equal. + + .. versionadded:: 20.3 + + Args: + name (:obj:`str`): The bot's name. + + Attributes: + name (:obj:`str`): The bot's name. + + """ + + __slots__ = ("name",) + + def __init__(self, name: str, *, api_kwargs: Optional[JSONDict] = None): + super().__init__(api_kwargs=api_kwargs) + self.name: str = name + + self._id_attrs = (self.name,) + + self._freeze() + + MAX_LENGTH: Final[int] = constants.BotNameLimit.MAX_NAME_LENGTH + """:const:`telegram.constants.BotNameLimit.MAX_NAME_LENGTH`""" diff --git a/_business.py b/_business.py new file mode 100644 index 0000000000000000000000000000000000000000..22c89e024b4dc8967b5237baadc5d478c5eb58a7 --- /dev/null +++ b/_business.py @@ -0,0 +1,455 @@ +#!/usr/bin/env python +# pylint: disable=redefined-builtin +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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 the Telegram Business related classes.""" + +from datetime import datetime +from typing import TYPE_CHECKING, Optional, Sequence, Tuple + +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.datetime import extract_tzinfo_from_defaults, from_timestamp +from telegram._utils.types import JSONDict + +if TYPE_CHECKING: + from telegram import Bot + + +class BusinessConnection(TelegramObject): + """ + Describes the connection of the bot with a business account. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal if their :attr:`id`, :attr:`user`, :attr:`user_chat_id`, :attr:`date`, + :attr:`can_reply`, and :attr:`is_enabled` are equal. + + .. versionadded:: 21.1 + + Args: + id (:obj:`str`): Unique identifier of the business connection. + user (:class:`telegram.User`): Business account user that created the business connection. + user_chat_id (:obj:`int`): Identifier of a private chat with the user who created the + business connection. + date (:obj:`datetime.datetime`): Date the connection was established in Unix time. + can_reply (:obj:`bool`): True, if the bot can act on behalf of the business account in + chats that were active in the last 24 hours. + is_enabled (:obj:`bool`): True, if the connection is active. + + Attributes: + id (:obj:`str`): Unique identifier of the business connection. + user (:class:`telegram.User`): Business account user that created the business connection. + user_chat_id (:obj:`int`): Identifier of a private chat with the user who created the + business connection. + date (:obj:`datetime.datetime`): Date the connection was established in Unix time. + can_reply (:obj:`bool`): True, if the bot can act on behalf of the business account in + chats that were active in the last 24 hours. + is_enabled (:obj:`bool`): True, if the connection is active. + """ + + __slots__ = ( + "can_reply", + "date", + "id", + "is_enabled", + "user", + "user_chat_id", + ) + + def __init__( + self, + id: str, + user: "User", + user_chat_id: int, + date: datetime, + can_reply: bool, + is_enabled: bool, + *, + api_kwargs: Optional[JSONDict] = None, + ): + super().__init__(api_kwargs=api_kwargs) + self.id: str = id + self.user: User = user + self.user_chat_id: int = user_chat_id + self.date: datetime = date + self.can_reply: bool = can_reply + self.is_enabled: bool = is_enabled + + self._id_attrs = ( + self.id, + self.user, + self.user_chat_id, + self.date, + self.can_reply, + self.is_enabled, + ) + + self._freeze() + + @classmethod + def de_json( + cls, data: Optional[JSONDict], bot: Optional["Bot"] = None + ) -> Optional["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) + + return super().de_json(data=data, bot=bot) + + +class BusinessMessagesDeleted(TelegramObject): + """ + This object is received when messages are deleted from a connected business account. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal if their :attr:`business_connection_id`, :attr:`message_ids`, and + :attr:`chat` are equal. + + .. versionadded:: 21.1 + + Args: + 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 (Sequence[:obj:`int`]): A list of identifiers of the deleted messages in the + chat of the business account. + + Attributes: + 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 + chat of the business account. + """ + + __slots__ = ( + "business_connection_id", + "chat", + "message_ids", + ) + + def __init__( + self, + business_connection_id: str, + chat: Chat, + message_ids: Sequence[int], + *, + api_kwargs: Optional[JSONDict] = None, + ): + 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._id_attrs = ( + self.business_connection_id, + self.chat, + self.message_ids, + ) + + self._freeze() + + @classmethod + def de_json( + cls, data: Optional[JSONDict], bot: Optional["Bot"] = None + ) -> Optional["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) + + return super().de_json(data=data, bot=bot) + + +class BusinessIntro(TelegramObject): + """ + This object contains information about the start page settings of a Telegram Business account. + + Objects of this class are comparable in terms of equality. + Two objects of this class are considered equal, if their + :attr:`title`, :attr:`message` and :attr:`sticker` are equal. + + .. versionadded:: 21.1 + + Args: + title (:obj:`str`, optional): Title text of the business intro. + message (:obj:`str`, optional): Message text of the business intro. + sticker (:class:`telegram.Sticker`, optional): Sticker of the business intro. + + Attributes: + title (:obj:`str`): Optional. Title text of the business intro. + message (:obj:`str`): Optional. Message text of the business intro. + sticker (:class:`telegram.Sticker`): Optional. Sticker of the business intro. + """ + + __slots__ = ( + "message", + "sticker", + "title", + ) + + def __init__( + self, + title: Optional[str] = None, + message: Optional[str] = None, + sticker: Optional[Sticker] = None, + *, + api_kwargs: Optional[JSONDict] = None, + ): + super().__init__(api_kwargs=api_kwargs) + self.title: Optional[str] = title + self.message: Optional[str] = message + self.sticker: Optional[Sticker] = sticker + + self._id_attrs = (self.title, self.message, self.sticker) + + self._freeze() + + @classmethod + def de_json( + cls, data: Optional[JSONDict], bot: Optional["Bot"] = None + ) -> Optional["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) + + return super().de_json(data=data, bot=bot) + + +class BusinessLocation(TelegramObject): + """ + This object contains information about the location of a Telegram Business account. + + Objects of this class are comparable in terms of equality. + Two objects of this class are considered equal, if their + :attr:`address` is equal. + + .. versionadded:: 21.1 + + Args: + address (:obj:`str`): Address of the business. + location (:class:`telegram.Location`, optional): Location of the business. + + Attributes: + address (:obj:`str`): Address of the business. + location (:class:`telegram.Location`): Optional. Location of the business. + """ + + __slots__ = ( + "address", + "location", + ) + + def __init__( + self, + address: str, + location: Optional[Location] = None, + *, + api_kwargs: Optional[JSONDict] = None, + ): + super().__init__(api_kwargs=api_kwargs) + self.address: str = address + self.location: Optional[Location] = location + + self._id_attrs = (self.address,) + + self._freeze() + + @classmethod + def de_json( + cls, data: Optional[JSONDict], bot: Optional["Bot"] = None + ) -> Optional["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) + + return super().de_json(data=data, bot=bot) + + +class BusinessOpeningHoursInterval(TelegramObject): + """ + This object describes an interval of time during which a business is open. + + Objects of this class are comparable in terms of equality. + Two objects of this class are considered equal, if their + :attr:`opening_minute` and :attr:`closing_minute` are equal. + + .. versionadded:: 21.1 + + Examples: + A day has (24 * 60 =) 1440 minutes, a week has (7 * 1440 =) 10080 minutes. + Starting the the minute's sequence from Monday, example values of + :attr:`opening_minute`, :attr:`closing_minute` will map to the following day times: + + * Monday - 8am to 8:30pm: + - ``opening_minute = 480`` :guilabel:`8 * 60` + - ``closing_minute = 1230`` :guilabel:`20 * 60 + 30` + * Tuesday - 24 hours: + - ``opening_minute = 1440`` :guilabel:`24 * 60` + - ``closing_minute = 2879`` :guilabel:`2 * 24 * 60 - 1` + * Sunday - 12am - 11:58pm: + - ``opening_minute = 8640`` :guilabel:`6 * 24 * 60` + - ``closing_minute = 10078`` :guilabel:`7 * 24 * 60 - 2` + + Args: + opening_minute (:obj:`int`): The minute's sequence number in a week, starting on Monday, + marking the start of the time interval during which the business is open; + 0 - 7 * 24 * 60. + closing_minute (:obj:`int`): The minute's + sequence number in a week, starting on Monday, marking the end of the time interval + during which the business is open; 0 - 8 * 24 * 60 + + Attributes: + opening_minute (:obj:`int`): The minute's sequence number in a week, starting on Monday, + marking the start of the time interval during which the business is open; + 0 - 7 * 24 * 60. + closing_minute (:obj:`int`): The minute's + sequence number in a week, starting on Monday, marking the end of the time interval + during which the business is open; 0 - 8 * 24 * 60 + """ + + __slots__ = ("_closing_time", "_opening_time", "closing_minute", "opening_minute") + + def __init__( + self, + opening_minute: int, + closing_minute: int, + *, + api_kwargs: Optional[JSONDict] = None, + ): + super().__init__(api_kwargs=api_kwargs) + 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._id_attrs = (self.opening_minute, self.closing_minute) + + self._freeze() + + 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]: + """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`]: + """ + 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]: + """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`]: + """ + if self._closing_time is None: + self._closing_time = self._parse_minute(self.closing_minute) + return self._closing_time + + +class BusinessOpeningHours(TelegramObject): + """ + This object describes the opening hours of a business. + + Objects of this class are comparable in terms of equality. + Two objects of this class are considered equal, if their + :attr:`time_zone_name` and :attr:`opening_hours` are equal. + + .. versionadded:: 21.1 + + Args: + time_zone_name (:obj:`str`): Unique name of the time zone for which the opening + hours are defined. + opening_hours (Sequence[:class:`telegram.BusinessOpeningHoursInterval`]): List of + time intervals describing business opening hours. + + Attributes: + time_zone_name (:obj:`str`): Unique name of the time zone for which the opening + hours are defined. + opening_hours (Sequence[:class:`telegram.BusinessOpeningHoursInterval`]): List of + time intervals describing business opening hours. + """ + + __slots__ = ("opening_hours", "time_zone_name") + + def __init__( + self, + time_zone_name: str, + opening_hours: Sequence[BusinessOpeningHoursInterval], + *, + api_kwargs: Optional[JSONDict] = None, + ): + super().__init__(api_kwargs=api_kwargs) + self.time_zone_name: str = time_zone_name + self.opening_hours: Sequence[BusinessOpeningHoursInterval] = parse_sequence_arg( + opening_hours + ) + + self._id_attrs = (self.time_zone_name, self.opening_hours) + + self._freeze() + + @classmethod + def de_json( + cls, data: Optional[JSONDict], bot: Optional["Bot"] = None + ) -> Optional["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 + ) + + return super().de_json(data=data, bot=bot) diff --git a/_callbackquery.py b/_callbackquery.py new file mode 100644 index 0000000000000000000000000000000000000000..bdfa569dbfd04384d62ef756d8506c1359f05100 --- /dev/null +++ b/_callbackquery.py @@ -0,0 +1,892 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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/]. +# 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 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.defaultvalue import DEFAULT_NONE +from telegram._utils.types import JSONDict, ODVInput, ReplyMarkup + +if TYPE_CHECKING: + from telegram import ( + Bot, + GameHighScore, + InlineKeyboardMarkup, + InputMedia, + LinkPreviewOptions, + MessageEntity, + MessageId, + ReplyParameters, + ) + + +class CallbackQuery(TelegramObject): + """ + This object represents an incoming callback query from a callback button in an inline keyboard. + + If the button that originated the query was attached to a message sent by the bot, the field + :attr:`message` will be present. If the button was attached to a message sent via the bot (in + inline mode), the field :attr:`inline_message_id` will be present. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`id` is equal. + + Note: + * In Python :keyword:`from` is a reserved word. Use :paramref:`from_user` instead. + * Exactly one of the fields :attr:`data` or :attr:`game_short_name` will be present. + * After the user presses an inline button, Telegram clients will display a progress bar + until you call :attr:`answer`. It is, therefore, necessary to react + by calling :attr:`telegram.Bot.answer_callback_query` even if no notification to the user + is needed (e.g., without specifying any of the optional parameters). + * If you're using :attr:`telegram.ext.ExtBot.callback_data_cache`, :attr:`data` may be + an instance + of :class:`telegram.ext.InvalidCallbackData`. This will be the case, if the data + associated with the button triggering the :class:`telegram.CallbackQuery` was already + deleted or if :attr:`data` was manipulated by a malicious client. + + .. versionadded:: 13.6 + + Args: + id (:obj:`str`): Unique identifier for this query. + from_user (:class:`telegram.User`): Sender. + chat_instance (:obj:`str`): Global identifier, uniquely corresponding to the chat to which + the message with the callback button was sent. Useful for high scores in games. + message (:class:`telegram.MaybeInaccessibleMessage`, optional): Message sent by the bot + with the callback button that originated the query. + + .. versionchanged:: 20.8 + Accept objects of type :class:`telegram.MaybeInaccessibleMessage` since Bot API 7.0. + data (:obj:`str`, optional): Data associated with the callback button. Be aware that the + message, which originated the query, can contain no callback buttons with this data. + inline_message_id (:obj:`str`, optional): Identifier of the message sent via the bot in + inline mode, that originated the query. + game_short_name (:obj:`str`, optional): Short name of a Game to be returned, serves as + the unique identifier for the game. + + Attributes: + id (:obj:`str`): Unique identifier for this query. + from_user (:class:`telegram.User`): Sender. + chat_instance (:obj:`str`): Global identifier, uniquely corresponding to the chat to which + the message with the callback button was sent. Useful for high scores in games. + message (:class:`telegram.MaybeInaccessibleMessage`): Optional. Message sent by the bot + with the callback button that originated the query. + + .. versionchanged:: 20.8 + Objects may be of type :class:`telegram.MaybeInaccessibleMessage` since Bot API + 7.0. + data (:obj:`str` | :obj:`object`): Optional. Data associated with the callback button. + Be aware that the message, which originated the query, can contain no callback buttons + with this data. + + Tip: + The value here is the same as the value passed in + :paramref:`telegram.InlineKeyboardButton.callback_data`. + inline_message_id (:obj:`str`): Optional. Identifier of the message sent via the bot in + inline mode, that originated the query. + game_short_name (:obj:`str`): Optional. Short name of a Game to be returned, serves as + the unique identifier for the game. + + + """ + + __slots__ = ( + "chat_instance", + "data", + "from_user", + "game_short_name", + "id", + "inline_message_id", + "message", + ) + + def __init__( + self, + id: str, + from_user: User, + chat_instance: str, + message: Optional[MaybeInaccessibleMessage] = None, + data: Optional[str] = None, + inline_message_id: Optional[str] = None, + game_short_name: Optional[str] = None, + *, + api_kwargs: Optional[JSONDict] = None, + ): + super().__init__(api_kwargs=api_kwargs) + # Required + self.id: str = id + self.from_user: User = from_user + self.chat_instance: str = chat_instance + # Optionals + self.message: Optional[MaybeInaccessibleMessage] = message + self.data: Optional[str] = data + self.inline_message_id: Optional[str] = inline_message_id + self.game_short_name: Optional[str] = game_short_name + + self._id_attrs = (self.id,) + + self._freeze() + + @classmethod + def de_json( + cls, data: Optional[JSONDict], bot: Optional["Bot"] = None + ) -> Optional["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) + + return super().de_json(data=data, bot=bot) + + async def answer( + self, + text: Optional[str] = None, + show_alert: Optional[bool] = None, + url: Optional[str] = None, + cache_time: 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: + """Shortcut for:: + + await bot.answer_callback_query(update.callback_query.id, *args, **kwargs) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.answer_callback_query`. + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + """ + return await self.get_bot().answer_callback_query( + callback_query_id=self.id, + text=text, + show_alert=show_alert, + url=url, + cache_time=cache_time, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + def _get_message(self, action: str = "edit") -> Message: + """Helper method to get the message for the shortcut methods. Must be called only + if :attr:`inline_message_id` is *not* set. + """ + if not isinstance(self.message, Message): + raise TypeError(f"Cannot {action} an inaccessible message") + return self.message + + async def edit_message_text( + self, + text: str, + parse_mode: ODVInput[str] = DEFAULT_NONE, + reply_markup: Optional["InlineKeyboardMarkup"] = None, + entities: Optional[Sequence["MessageEntity"]] = None, + link_preview_options: ODVInput["LinkPreviewOptions"] = DEFAULT_NONE, + *, + disable_web_page_preview: 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, + ) -> Union[Message, bool]: + """Shortcut for either:: + + await update.callback_query.message.edit_text(*args, **kwargs) + + or:: + + await bot.edit_message_text( + inline_message_id=update.callback_query.inline_message_id, *args, **kwargs, + ) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.edit_message_text` and :meth:`telegram.Message.edit_text`. + + .. versionchanged:: 20.8 + Raises :exc:`TypeError` if :attr:`message` is not accessible. + + Returns: + :class:`telegram.Message`: On success, if edited message is sent by the bot, the + edited Message is returned, otherwise :obj:`True` is returned. + + Raises: + :exc:`TypeError` if :attr:`message` is not accessible. + + """ + if self.inline_message_id: + return await self.get_bot().edit_message_text( + inline_message_id=self.inline_message_id, + text=text, + parse_mode=parse_mode, + disable_web_page_preview=disable_web_page_preview, + link_preview_options=link_preview_options, + reply_markup=reply_markup, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + entities=entities, + chat_id=None, + message_id=None, + # inline messages can not be sent on behalf of a bcid + business_connection_id=None, + ) + return await self._get_message().edit_text( + text=text, + parse_mode=parse_mode, + disable_web_page_preview=disable_web_page_preview, + link_preview_options=link_preview_options, + reply_markup=reply_markup, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + entities=entities, + ) + + async def edit_message_caption( + self, + caption: Optional[str] = None, + reply_markup: Optional["InlineKeyboardMarkup"] = None, + parse_mode: ODVInput[str] = DEFAULT_NONE, + caption_entities: Optional[Sequence["MessageEntity"]] = None, + show_caption_above_media: 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, + ) -> Union[Message, bool]: + """Shortcut for either:: + + await update.callback_query.message.edit_caption(*args, **kwargs) + + or:: + + await bot.edit_message_caption( + inline_message_id=update.callback_query.inline_message_id, *args, **kwargs, + ) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.edit_message_caption` and :meth:`telegram.Message.edit_caption`. + + .. versionchanged:: 20.8 + Raises :exc:`TypeError` if :attr:`message` is not accessible. + + Returns: + :class:`telegram.Message`: On success, if edited message is sent by the bot, the + edited Message is returned, otherwise :obj:`True` is returned. + + Raises: + :exc:`TypeError` if :attr:`message` is not accessible. + + """ + if self.inline_message_id: + return await self.get_bot().edit_message_caption( + caption=caption, + inline_message_id=self.inline_message_id, + reply_markup=reply_markup, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + parse_mode=parse_mode, + api_kwargs=api_kwargs, + caption_entities=caption_entities, + chat_id=None, + message_id=None, + show_caption_above_media=show_caption_above_media, + # inline messages can not be sent on behalf of a bcid + business_connection_id=None, + ) + return await self._get_message().edit_caption( + caption=caption, + reply_markup=reply_markup, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + parse_mode=parse_mode, + api_kwargs=api_kwargs, + caption_entities=caption_entities, + show_caption_above_media=show_caption_above_media, + ) + + async def edit_message_reply_markup( + self, + reply_markup: Optional["InlineKeyboardMarkup"] = 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, + ) -> Union[Message, bool]: + """Shortcut for either:: + + await update.callback_query.message.edit_reply_markup(*args, **kwargs) + + or:: + + await bot.edit_message_reply_markup( + inline_message_id=update.callback_query.inline_message_id, *args, **kwargs + ) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.edit_message_reply_markup` and + :meth:`telegram.Message.edit_reply_markup`. + + .. versionchanged:: 20.8 + Raises :exc:`TypeError` if :attr:`message` is not accessible. + + Returns: + :class:`telegram.Message`: On success, if edited message is sent by the bot, the + edited Message is returned, otherwise :obj:`True` is returned. + + Raises: + :exc:`TypeError` if :attr:`message` is not accessible. + + """ + if self.inline_message_id: + return await self.get_bot().edit_message_reply_markup( + reply_markup=reply_markup, + inline_message_id=self.inline_message_id, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + chat_id=None, + message_id=None, + # inline messages can not be sent on behalf of a bcid + business_connection_id=None, + ) + return await self._get_message().edit_reply_markup( + reply_markup=reply_markup, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def edit_message_media( + self, + media: "InputMedia", + reply_markup: Optional["InlineKeyboardMarkup"] = 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, + ) -> Union[Message, bool]: + """Shortcut for either:: + + await update.callback_query.message.edit_media(*args, **kwargs) + + or:: + + await bot.edit_message_media( + inline_message_id=update.callback_query.inline_message_id, *args, **kwargs + ) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.edit_message_media` and :meth:`telegram.Message.edit_media`. + + .. versionchanged:: 20.8 + Raises :exc:`TypeError` if :attr:`message` is not accessible. + + Returns: + :class:`telegram.Message`: On success, if edited message is not an inline message, the + edited Message is returned, otherwise :obj:`True` is returned. + + Raises: + :exc:`TypeError` if :attr:`message` is not accessible. + + """ + if self.inline_message_id: + return await self.get_bot().edit_message_media( + inline_message_id=self.inline_message_id, + media=media, + reply_markup=reply_markup, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + chat_id=None, + message_id=None, + # inline messages can not be sent on behalf of a bcid + business_connection_id=None, + ) + return await self._get_message().edit_media( + media=media, + reply_markup=reply_markup, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def edit_message_live_location( + self, + latitude: Optional[float] = None, + longitude: Optional[float] = None, + reply_markup: Optional["InlineKeyboardMarkup"] = None, + horizontal_accuracy: Optional[float] = None, + heading: Optional[int] = None, + proximity_alert_radius: Optional[int] = None, + live_period: Optional[int] = None, + *, + location: Optional[Location] = 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, + ) -> Union[Message, bool]: + """Shortcut for either:: + + await update.callback_query.message.edit_live_location(*args, **kwargs) + + or:: + + await bot.edit_message_live_location( + inline_message_id=update.callback_query.inline_message_id, *args, **kwargs + ) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.edit_message_live_location` and + :meth:`telegram.Message.edit_live_location`. + + .. versionchanged:: 20.8 + Raises :exc:`TypeError` if :attr:`message` is not accessible. + + Returns: + :class:`telegram.Message`: On success, if edited message is sent by the bot, the + edited Message is returned, otherwise :obj:`True` is returned. + + Raises: + :exc:`TypeError` if :attr:`message` is not accessible. + + """ + if self.inline_message_id: + return await self.get_bot().edit_message_live_location( + inline_message_id=self.inline_message_id, + latitude=latitude, + longitude=longitude, + location=location, + reply_markup=reply_markup, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + horizontal_accuracy=horizontal_accuracy, + heading=heading, + proximity_alert_radius=proximity_alert_radius, + live_period=live_period, + chat_id=None, + message_id=None, + # inline messages can not be sent on behalf of a bcid + business_connection_id=None, + ) + return await self._get_message().edit_live_location( + latitude=latitude, + longitude=longitude, + location=location, + reply_markup=reply_markup, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + horizontal_accuracy=horizontal_accuracy, + heading=heading, + proximity_alert_radius=proximity_alert_radius, + live_period=live_period, + ) + + async def stop_message_live_location( + self, + reply_markup: Optional["InlineKeyboardMarkup"] = 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, + ) -> Union[Message, bool]: + """Shortcut for either:: + + await update.callback_query.message.stop_live_location(*args, **kwargs) + + or:: + + await bot.stop_message_live_location( + inline_message_id=update.callback_query.inline_message_id, *args, **kwargs + ) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.stop_message_live_location` and + :meth:`telegram.Message.stop_live_location`. + + .. versionchanged:: 20.8 + Raises :exc:`TypeError` if :attr:`message` is not accessible. + + Returns: + :class:`telegram.Message`: On success, if edited message is sent by the bot, the + edited Message is returned, otherwise :obj:`True` is returned. + + Raises: + :exc:`TypeError` if :attr:`message` is not accessible. + + """ + if self.inline_message_id: + return await self.get_bot().stop_message_live_location( + inline_message_id=self.inline_message_id, + reply_markup=reply_markup, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + chat_id=None, + message_id=None, + # inline messages can not be sent on behalf of a bcid + business_connection_id=None, + ) + return await self._get_message().stop_live_location( + reply_markup=reply_markup, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def set_game_score( + self, + user_id: int, + score: int, + force: Optional[bool] = None, + disable_edit_message: 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, + ) -> Union[Message, bool]: + """Shortcut for either:: + + await update.callback_query.message.set_game_score(*args, **kwargs) + + or:: + + await bot.set_game_score( + inline_message_id=update.callback_query.inline_message_id, *args, **kwargs + ) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.set_game_score` and :meth:`telegram.Message.set_game_score`. + + .. versionchanged:: 20.8 + Raises :exc:`TypeError` if :attr:`message` is not accessible. + + Returns: + :class:`telegram.Message`: On success, if edited message is sent by the bot, the + edited Message is returned, otherwise :obj:`True` is returned. + + Raises: + :exc:`TypeError` if :attr:`message` is not accessible. + + """ + if self.inline_message_id: + return await self.get_bot().set_game_score( + inline_message_id=self.inline_message_id, + user_id=user_id, + score=score, + force=force, + disable_edit_message=disable_edit_message, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + chat_id=None, + message_id=None, + ) + return await self._get_message().set_game_score( + user_id=user_id, + score=score, + force=force, + disable_edit_message=disable_edit_message, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def get_game_high_scores( + 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, + ) -> Tuple["GameHighScore", ...]: + """Shortcut for either:: + + await update.callback_query.message.get_game_high_score(*args, **kwargs) + + or:: + + await bot.get_game_high_scores( + inline_message_id=update.callback_query.inline_message_id, *args, **kwargs + ) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.get_game_high_scores` and + :meth:`telegram.Message.get_game_high_scores`. + + .. versionchanged:: 20.8 + Raises :exc:`TypeError` if :attr:`message` is not accessible. + + Returns: + Tuple[:class:`telegram.GameHighScore`] + + Raises: + :exc:`TypeError` if :attr:`message` is not accessible. + + """ + if self.inline_message_id: + return await self.get_bot().get_game_high_scores( + inline_message_id=self.inline_message_id, + user_id=user_id, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + chat_id=None, + message_id=None, + ) + return await self._get_message().get_game_high_scores( + user_id=user_id, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def delete_message( + 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 update.callback_query.message.delete(*args, **kwargs) + + For the documentation of the arguments, please see :meth:`telegram.Message.delete`. + + .. versionchanged:: 20.8 + Raises :exc:`TypeError` if :attr:`message` is not accessible. + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + Raises: + :exc:`TypeError` if :attr:`message` is not accessible. + + """ + return await self._get_message(action="delete").delete( + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def pin_message( + self, + disable_notification: ODVInput[bool] = DEFAULT_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 update.callback_query.message.pin(*args, **kwargs) + + For the documentation of the arguments, please see :meth:`telegram.Message.pin`. + + .. versionchanged:: 20.8 + Raises :exc:`TypeError` if :attr:`message` is not accessible. + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + Raises: + :exc:`TypeError` if :attr:`message` is not accessible. + """ + return await self._get_message(action="pin").pin( + disable_notification=disable_notification, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def unpin_message( + 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 update.callback_query.message.unpin(*args, **kwargs) + + For the documentation of the arguments, please see :meth:`telegram.Message.unpin`. + + .. versionchanged:: 20.8 + Raises :exc:`TypeError` if :attr:`message` is not accessible. + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + Raises: + :exc:`TypeError` if :attr:`message` is not accessible. + """ + return await self._get_message(action="unpin").unpin( + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def copy_message( + self, + chat_id: Union[int, str], + caption: Optional[str] = None, + parse_mode: ODVInput[str] = DEFAULT_NONE, + caption_entities: Optional[Sequence["MessageEntity"]] = None, + disable_notification: ODVInput[bool] = DEFAULT_NONE, + reply_markup: Optional[ReplyMarkup] = None, + protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: Optional[int] = None, + reply_parameters: Optional["ReplyParameters"] = None, + show_caption_above_media: Optional[bool] = None, + *, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + reply_to_message_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, + ) -> "MessageId": + """Shortcut for:: + + await update.callback_query.message.copy( + from_chat_id=update.message.chat_id, + message_id=update.message.message_id, + *args, + **kwargs + ) + + For the documentation of the arguments, please see :meth:`telegram.Message.copy`. + + .. versionchanged:: 20.8 + Raises :exc:`TypeError` if :attr:`message` is not accessible. + + Returns: + :class:`telegram.MessageId`: On success, returns the MessageId of the sent message. + + Raises: + :exc:`TypeError` if :attr:`message` is not accessible. + """ + return await self._get_message(action="copy").copy( + chat_id=chat_id, + caption=caption, + parse_mode=parse_mode, + caption_entities=caption_entities, + disable_notification=disable_notification, + reply_to_message_id=reply_to_message_id, + allow_sending_without_reply=allow_sending_without_reply, + reply_markup=reply_markup, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + protect_content=protect_content, + message_thread_id=message_thread_id, + reply_parameters=reply_parameters, + show_caption_above_media=show_caption_above_media, + ) + + MAX_ANSWER_TEXT_LENGTH: Final[int] = ( + constants.CallbackQueryLimit.ANSWER_CALLBACK_QUERY_TEXT_LENGTH + ) + """ + :const:`telegram.constants.CallbackQueryLimit.ANSWER_CALLBACK_QUERY_TEXT_LENGTH` + + .. versionadded:: 13.2 + """ diff --git a/_chat.py b/_chat.py new file mode 100644 index 0000000000000000000000000000000000000000..6eb78978596d34df3c5fc66b3c64e9d70db3e661 --- /dev/null +++ b/_chat.py @@ -0,0 +1,3451 @@ +#!/usr/bin/env python +# pylint: disable=redefined-builtin +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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 Chat.""" +from datetime import datetime +from html import escape +from typing import TYPE_CHECKING, Final, Optional, Sequence, Tuple, Union + +from telegram import constants +from telegram._chatpermissions import ChatPermissions +from telegram._forumtopic import ForumTopic +from telegram._menubutton import MenuButton +from telegram._reaction import ReactionType +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.helpers import escape_markdown +from telegram.helpers import mention_html as helpers_mention_html +from telegram.helpers import mention_markdown as helpers_mention_markdown + +if TYPE_CHECKING: + from telegram import ( + Animation, + Audio, + ChatInviteLink, + ChatMember, + Contact, + Document, + InlineKeyboardMarkup, + InputMediaAudio, + InputMediaDocument, + InputMediaPhoto, + InputMediaVideo, + InputPaidMedia, + InputPollOption, + LabeledPrice, + LinkPreviewOptions, + Location, + Message, + MessageEntity, + MessageId, + PhotoSize, + ReplyParameters, + Sticker, + UserChatBoosts, + Venue, + Video, + VideoNote, + Voice, + ) + + +class _ChatBase(TelegramObject): + """Base class for :class:`telegram.Chat` and :class:`telegram.ChatFullInfo`. + + .. versionadded:: 21.3 + """ + + __slots__ = ("first_name", "id", "is_forum", "last_name", "title", "type", "username") + + def __init__( + self, + id: int, + type: str, + title: Optional[str] = None, + username: Optional[str] = None, + first_name: Optional[str] = None, + last_name: Optional[str] = None, + is_forum: Optional[bool] = None, + *, + api_kwargs: Optional[JSONDict] = None, + ): + super().__init__(api_kwargs=api_kwargs) + # Required + self.id: int = id + self.type: str = enum.get_member(constants.ChatType, type, type) + # Optionals + self.title: Optional[str] = title + self.username: Optional[str] = username + self.first_name: Optional[str] = first_name + self.last_name: Optional[str] = last_name + self.is_forum: Optional[bool] = is_forum + + self._id_attrs = (self.id,) + + self._freeze() + + SENDER: Final[str] = constants.ChatType.SENDER + """:const:`telegram.constants.ChatType.SENDER` + + .. versionadded:: 13.5 + """ + PRIVATE: Final[str] = constants.ChatType.PRIVATE + """:const:`telegram.constants.ChatType.PRIVATE`""" + GROUP: Final[str] = constants.ChatType.GROUP + """:const:`telegram.constants.ChatType.GROUP`""" + SUPERGROUP: Final[str] = constants.ChatType.SUPERGROUP + """:const:`telegram.constants.ChatType.SUPERGROUP`""" + CHANNEL: Final[str] = constants.ChatType.CHANNEL + """:const:`telegram.constants.ChatType.CHANNEL`""" + + @property + def effective_name(self) -> Optional[str]: + """ + :obj:`str`: Convenience property. Gives :attr:`~Chat.title` if not :obj:`None`, + else :attr:`~Chat.full_name` if not :obj:`None`. + + .. versionadded:: 20.1 + """ + if self.title is not None: + return self.title + if self.full_name is not None: + return self.full_name + return None + + @property + def full_name(self) -> Optional[str]: + """ + :obj:`str`: Convenience property. If :attr:`~Chat.first_name` is not :obj:`None`, gives + :attr:`~Chat.first_name` followed by (if available) :attr:`~Chat.last_name`. + + Note: + :attr:`full_name` will always be :obj:`None`, if the chat is a (super)group or + channel. + + .. versionadded:: 13.2 + """ + if not self.first_name: + return None + if self.last_name: + return f"{self.first_name} {self.last_name}" + return self.first_name + + @property + def link(self) -> Optional[str]: + """:obj:`str`: Convenience property. If the chat has a :attr:`~Chat.username`, returns a + t.me link of the chat. + """ + if self.username: + return f"https://t.me/{self.username}" + return None + + def mention_markdown(self, name: Optional[str] = None) -> str: + """ + Note: + :tg-const:`telegram.constants.ParseMode.MARKDOWN` is a legacy mode, retained by + Telegram for backward compatibility. You should use :meth:`mention_markdown_v2` + instead. + + .. versionadded:: 20.0 + + Args: + name (:obj:`str`): The name used as a link for the chat. Defaults to + :attr:`~Chat.full_name`. + + Returns: + :obj:`str`: The inline mention for the chat as markdown (version 1). + + Raises: + :exc:`TypeError`: If the chat is a private chat and neither the :paramref:`name` + nor the :attr:`~Chat.first_name` is set, then throw an :exc:`TypeError`. + If the chat is a public chat and neither the :paramref:`name` nor the + :attr:`~Chat.title` is set, then throw an :exc:`TypeError`. If chat is a + private group chat, then throw an :exc:`TypeError`. + + """ + if self.type == self.PRIVATE: + if name: + return helpers_mention_markdown(self.id, name) + if self.full_name: + return helpers_mention_markdown(self.id, self.full_name) + raise TypeError("Can not create a mention to a private chat without first name") + if self.username: + if name: + return f"[{name}]({self.link})" + if self.title: + return f"[{self.title}]({self.link})" + raise TypeError("Can not create a mention to a public chat without title") + raise TypeError("Can not create a mention to a private group chat") + + def mention_markdown_v2(self, name: Optional[str] = None) -> str: + """ + .. versionadded:: 20.0 + + Args: + name (:obj:`str`): The name used as a link for the chat. Defaults to + :attr:`~Chat.full_name`. + + Returns: + :obj:`str`: The inline mention for the chat as markdown (version 2). + + Raises: + :exc:`TypeError`: If the chat is a private chat and neither the :paramref:`name` + nor the :attr:`~Chat.first_name` is set, then throw an :exc:`TypeError`. + If the chat is a public chat and neither the :paramref:`name` nor the + :attr:`~Chat.title` is set, then throw an :exc:`TypeError`. If chat is a + private group chat, then throw an :exc:`TypeError`. + + """ + if self.type == self.PRIVATE: + if name: + return helpers_mention_markdown(self.id, name, version=2) + if self.full_name: + return helpers_mention_markdown(self.id, self.full_name, version=2) + raise TypeError("Can not create a mention to a private chat without first name") + if self.username: + if name: + return f"[{escape_markdown(name, version=2)}]({self.link})" + if self.title: + return f"[{escape_markdown(self.title, version=2)}]({self.link})" + raise TypeError("Can not create a mention to a public chat without title") + raise TypeError("Can not create a mention to a private group chat") + + def mention_html(self, name: Optional[str] = None) -> str: + """ + .. versionadded:: 20.0 + + Args: + name (:obj:`str`): The name used as a link for the chat. Defaults to :attr:`full_name`. + + Returns: + :obj:`str`: The inline mention for the chat as HTML. + + Raises: + :exc:`TypeError`: If the chat is a private chat and neither the :paramref:`name` + nor the :attr:`~Chat.first_name` is set, then throw an :exc:`TypeError`. + If the chat is a public chat and neither the :paramref:`name` nor the + :attr:`~Chat.title` is set, then throw an :exc:`TypeError`. + If chat is a private group chat, then throw an :exc:`TypeError`. + + """ + if self.type == self.PRIVATE: + if name: + return helpers_mention_html(self.id, name) + if self.full_name: + return helpers_mention_html(self.id, self.full_name) + raise TypeError("Can not create a mention to a private chat without first name") + if self.username: + if name: + return f'{escape(name)}' + if self.title: + return f'{escape(self.title)}' + raise TypeError("Can not create a mention to a public chat without title") + raise TypeError("Can not create a mention to a private group chat") + + async def leave( + 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.leave_chat(update.effective_chat.id, *args, **kwargs) + + For the documentation of the arguments, please see :meth:`telegram.Bot.leave_chat`. + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + """ + return await self.get_bot().leave_chat( + chat_id=self.id, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def get_administrators( + 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, + ) -> Tuple["ChatMember", ...]: + """Shortcut for:: + + await bot.get_chat_administrators(update.effective_chat.id, *args, **kwargs) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.get_chat_administrators`. + + Returns: + 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. + + """ + return await self.get_bot().get_chat_administrators( + chat_id=self.id, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def get_member_count( + 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, + ) -> int: + """Shortcut for:: + + await bot.get_chat_member_count(update.effective_chat.id, *args, **kwargs) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.get_chat_member_count`. + + Returns: + :obj:`int` + """ + return await self.get_bot().get_chat_member_count( + chat_id=self.id, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def get_member( + 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, + ) -> "ChatMember": + """Shortcut for:: + + await bot.get_chat_member(update.effective_chat.id, *args, **kwargs) + + For the documentation of the arguments, please see :meth:`telegram.Bot.get_chat_member`. + + Returns: + :class:`telegram.ChatMember` + + """ + return await self.get_bot().get_chat_member( + chat_id=self.id, + user_id=user_id, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def ban_member( + self, + user_id: int, + revoke_messages: Optional[bool] = None, + until_date: Optional[Union[int, 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: + """Shortcut for:: + + await bot.ban_chat_member(update.effective_chat.id, *args, **kwargs) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.ban_chat_member`. + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + """ + return await self.get_bot().ban_chat_member( + chat_id=self.id, + user_id=user_id, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + until_date=until_date, + api_kwargs=api_kwargs, + revoke_messages=revoke_messages, + ) + + async def ban_sender_chat( + self, + sender_chat_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: + """Shortcut for:: + + await bot.ban_chat_sender_chat(chat_id=update.effective_chat.id, *args, **kwargs) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.ban_chat_sender_chat`. + + .. versionadded:: 13.9 + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + """ + return await self.get_bot().ban_chat_sender_chat( + chat_id=self.id, + sender_chat_id=sender_chat_id, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def ban_chat( + self, + chat_id: Union[str, 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: + """Shortcut for:: + + await bot.ban_chat_sender_chat( + sender_chat_id=update.effective_chat.id, *args, **kwargs + ) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.ban_chat_sender_chat`. + + .. versionadded:: 13.9 + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + """ + return await self.get_bot().ban_chat_sender_chat( + chat_id=chat_id, + sender_chat_id=self.id, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def unban_sender_chat( + self, + sender_chat_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: + """Shortcut for:: + + await bot.unban_chat_sender_chat(chat_id=update.effective_chat.id, *args, **kwargs) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.unban_chat_sender_chat`. + + .. versionadded:: 13.9 + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + """ + return await self.get_bot().unban_chat_sender_chat( + chat_id=self.id, + sender_chat_id=sender_chat_id, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def unban_chat( + self, + chat_id: Union[str, 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: + """Shortcut for:: + + await bot.unban_chat_sender_chat( + sender_chat_id=update.effective_chat.id, *args, **kwargs + ) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.unban_chat_sender_chat`. + + .. versionadded:: 13.9 + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + """ + return await self.get_bot().unban_chat_sender_chat( + chat_id=chat_id, + sender_chat_id=self.id, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def unban_member( + self, + user_id: int, + only_if_banned: 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.unban_chat_member(update.effective_chat.id, *args, **kwargs) + + For the documentation of the arguments, please see :meth:`telegram.Bot.unban_chat_member`. + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + """ + return await self.get_bot().unban_chat_member( + chat_id=self.id, + user_id=user_id, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + only_if_banned=only_if_banned, + ) + + async def promote_member( + self, + user_id: int, + can_change_info: Optional[bool] = None, + can_post_messages: Optional[bool] = None, + can_edit_messages: Optional[bool] = None, + can_delete_messages: Optional[bool] = None, + can_invite_users: Optional[bool] = None, + can_restrict_members: Optional[bool] = None, + can_pin_messages: Optional[bool] = None, + can_promote_members: Optional[bool] = None, + is_anonymous: Optional[bool] = None, + can_manage_chat: Optional[bool] = None, + can_manage_video_chats: Optional[bool] = None, + can_manage_topics: Optional[bool] = None, + can_post_stories: Optional[bool] = None, + can_edit_stories: Optional[bool] = None, + can_delete_stories: 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.promote_chat_member(update.effective_chat.id, *args, **kwargs) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.promote_chat_member`. + + .. versionadded:: 13.2 + .. versionchanged:: 20.0 + The argument ``can_manage_voice_chats`` was renamed to + :paramref:`~telegram.Bot.promote_chat_member.can_manage_video_chats` in accordance to + Bot API 6.0. + .. versionchanged:: 20.6 + The arguments `can_post_stories`, `can_edit_stories` and `can_delete_stories` were + added. + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + """ + return await self.get_bot().promote_chat_member( + chat_id=self.id, + user_id=user_id, + can_change_info=can_change_info, + can_post_messages=can_post_messages, + can_edit_messages=can_edit_messages, + can_delete_messages=can_delete_messages, + can_invite_users=can_invite_users, + can_restrict_members=can_restrict_members, + can_pin_messages=can_pin_messages, + can_promote_members=can_promote_members, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + is_anonymous=is_anonymous, + can_manage_chat=can_manage_chat, + can_manage_video_chats=can_manage_video_chats, + can_manage_topics=can_manage_topics, + can_post_stories=can_post_stories, + can_edit_stories=can_edit_stories, + can_delete_stories=can_delete_stories, + ) + + async def restrict_member( + self, + user_id: int, + permissions: ChatPermissions, + until_date: Optional[Union[int, datetime]] = None, + use_independent_chat_permissions: 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.restrict_chat_member(update.effective_chat.id, *args, **kwargs) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.restrict_chat_member`. + + .. versionadded:: 13.2 + + .. versionadded:: 20.1 + Added :paramref:`~telegram.Bot.restrict_chat_member.use_independent_chat_permissions`. + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + """ + return await self.get_bot().restrict_chat_member( + chat_id=self.id, + user_id=user_id, + permissions=permissions, + until_date=until_date, + use_independent_chat_permissions=use_independent_chat_permissions, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def set_permissions( + self, + permissions: ChatPermissions, + use_independent_chat_permissions: 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.set_chat_permissions(update.effective_chat.id, *args, **kwargs) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.set_chat_permissions`. + + .. versionadded:: 20.1 + Added :paramref:`~telegram.Bot.set_chat_permissions.use_independent_chat_permissions`. + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + """ + return await self.get_bot().set_chat_permissions( + chat_id=self.id, + permissions=permissions, + use_independent_chat_permissions=use_independent_chat_permissions, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def set_administrator_custom_title( + self, + user_id: int, + custom_title: 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: + """Shortcut for:: + + await bot.set_chat_administrator_custom_title( + update.effective_chat.id, *args, **kwargs + ) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.set_chat_administrator_custom_title`. + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + """ + return await self.get_bot().set_chat_administrator_custom_title( + chat_id=self.id, + user_id=user_id, + custom_title=custom_title, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def set_photo( + self, + photo: FileInput, + *, + 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.set_chat_photo( + chat_id=update.effective_chat.id, *args, **kwargs + ) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.set_chat_photo`. + + .. versionadded:: 20.0 + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + """ + return await self.get_bot().set_chat_photo( + chat_id=self.id, + photo=photo, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def delete_photo( + 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.delete_chat_photo( + chat_id=update.effective_chat.id, *args, **kwargs + ) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.delete_chat_photo`. + + .. versionadded:: 20.0 + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + """ + return await self.get_bot().delete_chat_photo( + chat_id=self.id, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def set_title( + self, + title: 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: + """Shortcut for:: + + await bot.set_chat_title( + chat_id=update.effective_chat.id, *args, **kwargs + ) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.set_chat_title`. + + .. versionadded:: 20.0 + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + """ + return await self.get_bot().set_chat_title( + chat_id=self.id, + title=title, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def set_description( + self, + 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.set_chat_description( + chat_id=update.effective_chat.id, *args, **kwargs + ) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.set_chat_description`. + + .. versionadded:: 20.0 + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + """ + return await self.get_bot().set_chat_description( + chat_id=self.id, + description=description, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + 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, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: Optional[JSONDict] = None, + ) -> bool: + """Shortcut for:: + + await bot.pin_chat_message(chat_id=update.effective_chat.id, *args, **kwargs) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.pin_chat_message`. + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + """ + return await self.get_bot().pin_chat_message( + chat_id=self.id, + message_id=message_id, + disable_notification=disable_notification, + read_timeout=read_timeout, + write_timeout=write_timeout, + 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, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: Optional[JSONDict] = None, + ) -> bool: + """Shortcut for:: + + await bot.unpin_chat_message(chat_id=update.effective_chat.id, *args, **kwargs) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.unpin_chat_message`. + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + """ + return await self.get_bot().unpin_chat_message( + chat_id=self.id, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + message_id=message_id, + business_connection_id=business_connection_id, + ) + + async def unpin_all_messages( + 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.unpin_all_chat_messages(chat_id=update.effective_chat.id, *args, **kwargs) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.unpin_all_chat_messages`. + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + """ + return await self.get_bot().unpin_all_chat_messages( + chat_id=self.id, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def send_message( + self, + text: str, + parse_mode: ODVInput[str] = DEFAULT_NONE, + disable_notification: ODVInput[bool] = DEFAULT_NONE, + reply_markup: Optional[ReplyMarkup] = None, + entities: Optional[Sequence["MessageEntity"]] = None, + protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: Optional[int] = None, + link_preview_options: ODVInput["LinkPreviewOptions"] = DEFAULT_NONE, + reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, + message_effect_id: Optional[str] = None, + *, + reply_to_message_id: Optional[int] = None, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + disable_web_page_preview: 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, + ) -> "Message": + """Shortcut for:: + + await bot.send_message(update.effective_chat.id, *args, **kwargs) + + For the documentation of the arguments, please see :meth:`telegram.Bot.send_message`. + + Returns: + :class:`telegram.Message`: On success, instance representing the message posted. + + """ + return await self.get_bot().send_message( + chat_id=self.id, + text=text, + parse_mode=parse_mode, + disable_web_page_preview=disable_web_page_preview, + disable_notification=disable_notification, + reply_to_message_id=reply_to_message_id, + link_preview_options=link_preview_options, + reply_parameters=reply_parameters, + reply_markup=reply_markup, + allow_sending_without_reply=allow_sending_without_reply, + entities=entities, + protect_content=protect_content, + message_thread_id=message_thread_id, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + business_connection_id=business_connection_id, + message_effect_id=message_effect_id, + ) + + async def delete_message( + self, + message_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: + """Shortcut for:: + + await bot.delete_message(update.effective_chat.id, *args, **kwargs) + + For the documentation of the arguments, please see :meth:`telegram.Bot.delete_message`. + + .. versionadded:: 20.8 + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + """ + return await self.get_bot().delete_message( + chat_id=self.id, + message_id=message_id, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def delete_messages( + self, + message_ids: Sequence[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: + """Shortcut for:: + + await bot.delete_messages(update.effective_chat.id, *args, **kwargs) + + For the documentation of the arguments, please see :meth:`telegram.Bot.delete_messages`. + + .. versionadded:: 20.8 + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + """ + return await self.get_bot().delete_messages( + chat_id=self.id, + message_ids=message_ids, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def send_media_group( + self, + media: Sequence[ + Union["InputMediaAudio", "InputMediaDocument", "InputMediaPhoto", "InputMediaVideo"] + ], + disable_notification: ODVInput[bool] = DEFAULT_NONE, + protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: Optional[int] = None, + reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, + message_effect_id: Optional[str] = None, + *, + reply_to_message_id: Optional[int] = None, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_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, + caption: Optional[str] = None, + parse_mode: ODVInput[str] = DEFAULT_NONE, + caption_entities: Optional[Sequence["MessageEntity"]] = None, + ) -> Tuple["Message", ...]: + """Shortcut for:: + + await bot.send_media_group(update.effective_chat.id, *args, **kwargs) + + 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` + instances that were sent is returned. + + """ + return await self.get_bot().send_media_group( + chat_id=self.id, + media=media, + disable_notification=disable_notification, + reply_to_message_id=reply_to_message_id, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + allow_sending_without_reply=allow_sending_without_reply, + protect_content=protect_content, + message_thread_id=message_thread_id, + caption=caption, + parse_mode=parse_mode, + caption_entities=caption_entities, + reply_parameters=reply_parameters, + business_connection_id=business_connection_id, + message_effect_id=message_effect_id, + ) + + async def send_chat_action( + self, + action: str, + message_thread_id: Optional[int] = None, + business_connection_id: 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.send_chat_action(update.effective_chat.id, *args, **kwargs) + + For the documentation of the arguments, please see :meth:`telegram.Bot.send_chat_action`. + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + """ + return await self.get_bot().send_chat_action( + chat_id=self.id, + action=action, + message_thread_id=message_thread_id, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + business_connection_id=business_connection_id, + ) + + send_action = send_chat_action + """Alias for :attr:`send_chat_action`""" + + async def send_photo( + self, + photo: Union[FileInput, "PhotoSize"], + caption: Optional[str] = None, + disable_notification: ODVInput[bool] = DEFAULT_NONE, + reply_markup: Optional[ReplyMarkup] = None, + parse_mode: ODVInput[str] = DEFAULT_NONE, + caption_entities: Optional[Sequence["MessageEntity"]] = None, + protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: Optional[int] = None, + has_spoiler: Optional[bool] = None, + reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, + message_effect_id: Optional[str] = None, + show_caption_above_media: Optional[bool] = None, + *, + reply_to_message_id: Optional[int] = None, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + filename: 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, + ) -> "Message": + """Shortcut for:: + + await bot.send_photo(update.effective_chat.id, *args, **kwargs) + + For the documentation of the arguments, please see :meth:`telegram.Bot.send_photo`. + + Returns: + :class:`telegram.Message`: On success, instance representing the message posted. + + """ + return await self.get_bot().send_photo( + chat_id=self.id, + photo=photo, + caption=caption, + disable_notification=disable_notification, + reply_to_message_id=reply_to_message_id, + reply_parameters=reply_parameters, + reply_markup=reply_markup, + parse_mode=parse_mode, + allow_sending_without_reply=allow_sending_without_reply, + caption_entities=caption_entities, + filename=filename, + protect_content=protect_content, + message_thread_id=message_thread_id, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + has_spoiler=has_spoiler, + business_connection_id=business_connection_id, + message_effect_id=message_effect_id, + show_caption_above_media=show_caption_above_media, + ) + + async def send_contact( + self, + phone_number: Optional[str] = None, + first_name: Optional[str] = None, + last_name: Optional[str] = None, + disable_notification: ODVInput[bool] = DEFAULT_NONE, + reply_markup: Optional[ReplyMarkup] = None, + vcard: Optional[str] = None, + protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: Optional[int] = None, + reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, + message_effect_id: Optional[str] = None, + *, + reply_to_message_id: Optional[int] = None, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + contact: Optional["Contact"] = 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, + ) -> "Message": + """Shortcut for:: + + await bot.send_contact(update.effective_chat.id, *args, **kwargs) + + For the documentation of the arguments, please see :meth:`telegram.Bot.send_contact`. + + Returns: + :class:`telegram.Message`: On success, instance representing the message posted. + + """ + return await self.get_bot().send_contact( + chat_id=self.id, + phone_number=phone_number, + first_name=first_name, + last_name=last_name, + disable_notification=disable_notification, + reply_to_message_id=reply_to_message_id, + reply_parameters=reply_parameters, + reply_markup=reply_markup, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + contact=contact, + vcard=vcard, + api_kwargs=api_kwargs, + allow_sending_without_reply=allow_sending_without_reply, + protect_content=protect_content, + message_thread_id=message_thread_id, + business_connection_id=business_connection_id, + message_effect_id=message_effect_id, + ) + + async def send_audio( + self, + audio: Union[FileInput, "Audio"], + duration: Optional[int] = None, + performer: Optional[str] = None, + title: Optional[str] = None, + caption: Optional[str] = None, + disable_notification: ODVInput[bool] = DEFAULT_NONE, + reply_markup: Optional[ReplyMarkup] = None, + parse_mode: ODVInput[str] = DEFAULT_NONE, + caption_entities: Optional[Sequence["MessageEntity"]] = None, + protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: Optional[int] = None, + thumbnail: Optional[FileInput] = None, + reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, + message_effect_id: Optional[str] = None, + *, + reply_to_message_id: Optional[int] = None, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + filename: 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, + ) -> "Message": + """Shortcut for:: + + await bot.send_audio(update.effective_chat.id, *args, **kwargs) + + For the documentation of the arguments, please see :meth:`telegram.Bot.send_audio`. + + Returns: + :class:`telegram.Message`: On success, instance representing the message posted. + + """ + return await self.get_bot().send_audio( + chat_id=self.id, + audio=audio, + duration=duration, + performer=performer, + title=title, + caption=caption, + disable_notification=disable_notification, + reply_to_message_id=reply_to_message_id, + reply_parameters=reply_parameters, + reply_markup=reply_markup, + parse_mode=parse_mode, + allow_sending_without_reply=allow_sending_without_reply, + caption_entities=caption_entities, + filename=filename, + protect_content=protect_content, + message_thread_id=message_thread_id, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + thumbnail=thumbnail, + business_connection_id=business_connection_id, + message_effect_id=message_effect_id, + ) + + async def send_document( + self, + document: Union[FileInput, "Document"], + caption: Optional[str] = None, + disable_notification: ODVInput[bool] = DEFAULT_NONE, + reply_markup: Optional[ReplyMarkup] = None, + parse_mode: ODVInput[str] = DEFAULT_NONE, + disable_content_type_detection: Optional[bool] = None, + caption_entities: Optional[Sequence["MessageEntity"]] = None, + protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: Optional[int] = None, + thumbnail: Optional[FileInput] = None, + reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, + message_effect_id: Optional[str] = None, + *, + reply_to_message_id: Optional[int] = None, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + filename: 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, + ) -> "Message": + """Shortcut for:: + + await bot.send_document(update.effective_chat.id, *args, **kwargs) + + For the documentation of the arguments, please see :meth:`telegram.Bot.send_document`. + + Returns: + :class:`telegram.Message`: On success, instance representing the message posted. + + """ + return await self.get_bot().send_document( + chat_id=self.id, + document=document, + filename=filename, + caption=caption, + disable_notification=disable_notification, + reply_to_message_id=reply_to_message_id, + reply_parameters=reply_parameters, + reply_markup=reply_markup, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + parse_mode=parse_mode, + thumbnail=thumbnail, + api_kwargs=api_kwargs, + disable_content_type_detection=disable_content_type_detection, + allow_sending_without_reply=allow_sending_without_reply, + caption_entities=caption_entities, + protect_content=protect_content, + message_thread_id=message_thread_id, + business_connection_id=business_connection_id, + message_effect_id=message_effect_id, + ) + + async def send_dice( + self, + disable_notification: ODVInput[bool] = DEFAULT_NONE, + reply_markup: Optional[ReplyMarkup] = None, + emoji: Optional[str] = None, + protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: Optional[int] = None, + reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, + message_effect_id: Optional[str] = None, + *, + reply_to_message_id: Optional[int] = None, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_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, + ) -> "Message": + """Shortcut for:: + + await bot.send_dice(update.effective_chat.id, *args, **kwargs) + + For the documentation of the arguments, please see :meth:`telegram.Bot.send_dice`. + + Returns: + :class:`telegram.Message`: On success, instance representing the message posted. + + """ + return await self.get_bot().send_dice( + chat_id=self.id, + disable_notification=disable_notification, + reply_to_message_id=reply_to_message_id, + reply_parameters=reply_parameters, + reply_markup=reply_markup, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + emoji=emoji, + api_kwargs=api_kwargs, + allow_sending_without_reply=allow_sending_without_reply, + protect_content=protect_content, + message_thread_id=message_thread_id, + business_connection_id=business_connection_id, + message_effect_id=message_effect_id, + ) + + async def send_game( + self, + game_short_name: str, + disable_notification: ODVInput[bool] = DEFAULT_NONE, + reply_markup: Optional["InlineKeyboardMarkup"] = None, + protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: Optional[int] = None, + reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, + message_effect_id: Optional[str] = None, + *, + reply_to_message_id: Optional[int] = None, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_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, + ) -> "Message": + """Shortcut for:: + + await bot.send_game(update.effective_chat.id, *args, **kwargs) + + For the documentation of the arguments, please see :meth:`telegram.Bot.send_game`. + + Returns: + :class:`telegram.Message`: On success, instance representing the message posted. + + """ + return await self.get_bot().send_game( + chat_id=self.id, + game_short_name=game_short_name, + disable_notification=disable_notification, + reply_to_message_id=reply_to_message_id, + reply_parameters=reply_parameters, + reply_markup=reply_markup, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + allow_sending_without_reply=allow_sending_without_reply, + protect_content=protect_content, + message_thread_id=message_thread_id, + business_connection_id=business_connection_id, + message_effect_id=message_effect_id, + ) + + async def send_invoice( + self, + title: str, + description: str, + payload: str, + provider_token: Optional[str], + currency: str, + prices: Sequence["LabeledPrice"], + start_parameter: Optional[str] = None, + photo_url: Optional[str] = None, + photo_size: Optional[int] = None, + photo_width: Optional[int] = None, + photo_height: Optional[int] = None, + need_name: Optional[bool] = None, + need_phone_number: Optional[bool] = None, + need_email: Optional[bool] = None, + need_shipping_address: Optional[bool] = None, + is_flexible: Optional[bool] = None, + disable_notification: ODVInput[bool] = DEFAULT_NONE, + reply_markup: Optional["InlineKeyboardMarkup"] = None, + provider_data: Optional[Union[str, object]] = None, + send_phone_number_to_provider: Optional[bool] = None, + send_email_to_provider: Optional[bool] = None, + max_tip_amount: Optional[int] = None, + suggested_tip_amounts: Optional[Sequence[int]] = None, + protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: Optional[int] = None, + reply_parameters: Optional["ReplyParameters"] = None, + message_effect_id: Optional[str] = None, + *, + reply_to_message_id: Optional[int] = None, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_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, + ) -> "Message": + """Shortcut for:: + + await bot.send_invoice(update.effective_chat.id, *args, **kwargs) + + For the documentation of the arguments, please see :meth:`telegram.Bot.send_invoice`. + + Warning: + As of API 5.2 :paramref:`start_parameter ` + is an optional argument and therefore the + order of the arguments had to be changed. Use keyword arguments to make sure that the + arguments are passed correctly. + + .. versionchanged:: 13.5 + As of Bot API 5.2, the parameter + :paramref:`start_parameter ` is optional. + + Returns: + :class:`telegram.Message`: On success, instance representing the message posted. + + """ + return await self.get_bot().send_invoice( + chat_id=self.id, + title=title, + description=description, + payload=payload, + provider_token=provider_token, + currency=currency, + prices=prices, + start_parameter=start_parameter, + photo_url=photo_url, + photo_size=photo_size, + photo_width=photo_width, + photo_height=photo_height, + need_name=need_name, + need_phone_number=need_phone_number, + need_email=need_email, + need_shipping_address=need_shipping_address, + is_flexible=is_flexible, + disable_notification=disable_notification, + reply_to_message_id=reply_to_message_id, + reply_markup=reply_markup, + provider_data=provider_data, + send_phone_number_to_provider=send_phone_number_to_provider, + send_email_to_provider=send_email_to_provider, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + allow_sending_without_reply=allow_sending_without_reply, + max_tip_amount=max_tip_amount, + suggested_tip_amounts=suggested_tip_amounts, + protect_content=protect_content, + message_thread_id=message_thread_id, + reply_parameters=reply_parameters, + message_effect_id=message_effect_id, + ) + + async def send_location( + self, + latitude: Optional[float] = None, + longitude: Optional[float] = None, + disable_notification: ODVInput[bool] = DEFAULT_NONE, + reply_markup: Optional[ReplyMarkup] = None, + live_period: Optional[int] = None, + horizontal_accuracy: Optional[float] = None, + heading: Optional[int] = None, + proximity_alert_radius: Optional[int] = None, + protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: Optional[int] = None, + reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, + message_effect_id: Optional[str] = None, + *, + reply_to_message_id: Optional[int] = None, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + location: Optional["Location"] = 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, + ) -> "Message": + """Shortcut for:: + + await bot.send_location(update.effective_chat.id, *args, **kwargs) + + For the documentation of the arguments, please see :meth:`telegram.Bot.send_location`. + + Returns: + :class:`telegram.Message`: On success, instance representing the message posted. + + """ + return await self.get_bot().send_location( + chat_id=self.id, + latitude=latitude, + longitude=longitude, + disable_notification=disable_notification, + reply_to_message_id=reply_to_message_id, + reply_parameters=reply_parameters, + reply_markup=reply_markup, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + location=location, + live_period=live_period, + api_kwargs=api_kwargs, + horizontal_accuracy=horizontal_accuracy, + heading=heading, + proximity_alert_radius=proximity_alert_radius, + allow_sending_without_reply=allow_sending_without_reply, + protect_content=protect_content, + message_thread_id=message_thread_id, + business_connection_id=business_connection_id, + message_effect_id=message_effect_id, + ) + + async def send_animation( + self, + animation: Union[FileInput, "Animation"], + duration: Optional[int] = None, + width: Optional[int] = None, + height: Optional[int] = None, + caption: Optional[str] = None, + parse_mode: ODVInput[str] = DEFAULT_NONE, + disable_notification: ODVInput[bool] = DEFAULT_NONE, + reply_markup: Optional[ReplyMarkup] = None, + caption_entities: Optional[Sequence["MessageEntity"]] = None, + protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: Optional[int] = None, + has_spoiler: Optional[bool] = None, + thumbnail: Optional[FileInput] = None, + reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, + message_effect_id: Optional[str] = None, + show_caption_above_media: Optional[bool] = None, + *, + reply_to_message_id: Optional[int] = None, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + filename: 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, + ) -> "Message": + """Shortcut for:: + + await bot.send_animation(update.effective_chat.id, *args, **kwargs) + + For the documentation of the arguments, please see :meth:`telegram.Bot.send_animation`. + + Returns: + :class:`telegram.Message`: On success, instance representing the message posted. + + """ + return await self.get_bot().send_animation( + chat_id=self.id, + animation=animation, + duration=duration, + width=width, + height=height, + caption=caption, + parse_mode=parse_mode, + disable_notification=disable_notification, + reply_to_message_id=reply_to_message_id, + reply_parameters=reply_parameters, + reply_markup=reply_markup, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + allow_sending_without_reply=allow_sending_without_reply, + caption_entities=caption_entities, + filename=filename, + protect_content=protect_content, + message_thread_id=message_thread_id, + has_spoiler=has_spoiler, + thumbnail=thumbnail, + business_connection_id=business_connection_id, + message_effect_id=message_effect_id, + show_caption_above_media=show_caption_above_media, + ) + + async def send_sticker( + self, + sticker: Union[FileInput, "Sticker"], + disable_notification: ODVInput[bool] = DEFAULT_NONE, + reply_markup: Optional[ReplyMarkup] = None, + protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: Optional[int] = None, + emoji: Optional[str] = None, + reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, + message_effect_id: Optional[str] = None, + *, + reply_to_message_id: Optional[int] = None, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_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, + ) -> "Message": + """Shortcut for:: + + await bot.send_sticker(update.effective_chat.id, *args, **kwargs) + + For the documentation of the arguments, please see :meth:`telegram.Bot.send_sticker`. + + Returns: + :class:`telegram.Message`: On success, instance representing the message posted. + + """ + return await self.get_bot().send_sticker( + chat_id=self.id, + sticker=sticker, + disable_notification=disable_notification, + reply_to_message_id=reply_to_message_id, + reply_parameters=reply_parameters, + reply_markup=reply_markup, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + allow_sending_without_reply=allow_sending_without_reply, + protect_content=protect_content, + message_thread_id=message_thread_id, + emoji=emoji, + business_connection_id=business_connection_id, + message_effect_id=message_effect_id, + ) + + async def send_venue( + self, + latitude: Optional[float] = None, + longitude: Optional[float] = None, + title: Optional[str] = None, + address: Optional[str] = None, + foursquare_id: Optional[str] = None, + disable_notification: ODVInput[bool] = DEFAULT_NONE, + reply_markup: Optional[ReplyMarkup] = None, + foursquare_type: Optional[str] = None, + google_place_id: Optional[str] = None, + google_place_type: Optional[str] = None, + protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: Optional[int] = None, + reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, + message_effect_id: Optional[str] = None, + *, + reply_to_message_id: Optional[int] = None, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + venue: Optional["Venue"] = 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, + ) -> "Message": + """Shortcut for:: + + await bot.send_venue(update.effective_chat.id, *args, **kwargs) + + For the documentation of the arguments, please see :meth:`telegram.Bot.send_venue`. + + Returns: + :class:`telegram.Message`: On success, instance representing the message posted. + + """ + return await self.get_bot().send_venue( + chat_id=self.id, + latitude=latitude, + longitude=longitude, + title=title, + address=address, + foursquare_id=foursquare_id, + disable_notification=disable_notification, + reply_to_message_id=reply_to_message_id, + reply_parameters=reply_parameters, + reply_markup=reply_markup, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + venue=venue, + foursquare_type=foursquare_type, + api_kwargs=api_kwargs, + google_place_id=google_place_id, + google_place_type=google_place_type, + allow_sending_without_reply=allow_sending_without_reply, + protect_content=protect_content, + message_thread_id=message_thread_id, + business_connection_id=business_connection_id, + message_effect_id=message_effect_id, + ) + + async def send_video( + self, + video: Union[FileInput, "Video"], + duration: Optional[int] = None, + caption: Optional[str] = None, + disable_notification: ODVInput[bool] = DEFAULT_NONE, + reply_markup: Optional[ReplyMarkup] = None, + width: Optional[int] = None, + height: Optional[int] = None, + parse_mode: ODVInput[str] = DEFAULT_NONE, + supports_streaming: Optional[bool] = None, + caption_entities: Optional[Sequence["MessageEntity"]] = None, + protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: Optional[int] = None, + has_spoiler: Optional[bool] = None, + thumbnail: Optional[FileInput] = None, + reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, + message_effect_id: Optional[str] = None, + show_caption_above_media: Optional[bool] = None, + *, + reply_to_message_id: Optional[int] = None, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + filename: 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, + ) -> "Message": + """Shortcut for:: + + await bot.send_video(update.effective_chat.id, *args, **kwargs) + + For the documentation of the arguments, please see :meth:`telegram.Bot.send_video`. + + Returns: + :class:`telegram.Message`: On success, instance representing the message posted. + + """ + return await self.get_bot().send_video( + chat_id=self.id, + video=video, + duration=duration, + caption=caption, + disable_notification=disable_notification, + reply_to_message_id=reply_to_message_id, + reply_parameters=reply_parameters, + reply_markup=reply_markup, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + width=width, + height=height, + parse_mode=parse_mode, + supports_streaming=supports_streaming, + thumbnail=thumbnail, + api_kwargs=api_kwargs, + allow_sending_without_reply=allow_sending_without_reply, + caption_entities=caption_entities, + filename=filename, + protect_content=protect_content, + message_thread_id=message_thread_id, + has_spoiler=has_spoiler, + business_connection_id=business_connection_id, + message_effect_id=message_effect_id, + show_caption_above_media=show_caption_above_media, + ) + + async def send_video_note( + self, + video_note: Union[FileInput, "VideoNote"], + duration: Optional[int] = None, + length: Optional[int] = None, + disable_notification: ODVInput[bool] = DEFAULT_NONE, + reply_markup: Optional[ReplyMarkup] = None, + protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: Optional[int] = None, + thumbnail: Optional[FileInput] = None, + reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, + message_effect_id: Optional[str] = None, + *, + reply_to_message_id: Optional[int] = None, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + filename: 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, + ) -> "Message": + """Shortcut for:: + + await bot.send_video_note(update.effective_chat.id, *args, **kwargs) + + For the documentation of the arguments, please see :meth:`telegram.Bot.send_video_note`. + + Returns: + :class:`telegram.Message`: On success, instance representing the message posted. + + """ + return await self.get_bot().send_video_note( + chat_id=self.id, + video_note=video_note, + duration=duration, + length=length, + disable_notification=disable_notification, + reply_to_message_id=reply_to_message_id, + reply_parameters=reply_parameters, + reply_markup=reply_markup, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + thumbnail=thumbnail, + api_kwargs=api_kwargs, + allow_sending_without_reply=allow_sending_without_reply, + filename=filename, + protect_content=protect_content, + message_thread_id=message_thread_id, + business_connection_id=business_connection_id, + message_effect_id=message_effect_id, + ) + + async def send_voice( + self, + voice: Union[FileInput, "Voice"], + duration: Optional[int] = None, + caption: Optional[str] = None, + disable_notification: ODVInput[bool] = DEFAULT_NONE, + reply_markup: Optional[ReplyMarkup] = None, + parse_mode: ODVInput[str] = DEFAULT_NONE, + caption_entities: Optional[Sequence["MessageEntity"]] = None, + protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: Optional[int] = None, + reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, + message_effect_id: Optional[str] = None, + *, + reply_to_message_id: Optional[int] = None, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + filename: 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, + ) -> "Message": + """Shortcut for:: + + await bot.send_voice(update.effective_chat.id, *args, **kwargs) + + For the documentation of the arguments, please see :meth:`telegram.Bot.send_voice`. + + Returns: + :class:`telegram.Message`: On success, instance representing the message posted. + + """ + return await self.get_bot().send_voice( + chat_id=self.id, + voice=voice, + duration=duration, + caption=caption, + disable_notification=disable_notification, + reply_to_message_id=reply_to_message_id, + reply_parameters=reply_parameters, + reply_markup=reply_markup, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + parse_mode=parse_mode, + api_kwargs=api_kwargs, + allow_sending_without_reply=allow_sending_without_reply, + caption_entities=caption_entities, + filename=filename, + protect_content=protect_content, + message_thread_id=message_thread_id, + business_connection_id=business_connection_id, + message_effect_id=message_effect_id, + ) + + async def send_poll( + self, + question: str, + options: Sequence[Union[str, "InputPollOption"]], + is_anonymous: Optional[bool] = None, + type: Optional[str] = None, + allows_multiple_answers: Optional[bool] = None, + correct_option_id: Optional[CorrectOptionID] = None, + is_closed: Optional[bool] = None, + disable_notification: ODVInput[bool] = DEFAULT_NONE, + 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, + explanation_entities: Optional[Sequence["MessageEntity"]] = None, + protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: Optional[int] = None, + reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, + question_parse_mode: ODVInput[str] = DEFAULT_NONE, + question_entities: Optional[Sequence["MessageEntity"]] = None, + message_effect_id: Optional[str] = None, + *, + reply_to_message_id: Optional[int] = None, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_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, + ) -> "Message": + """Shortcut for:: + + await bot.send_poll(update.effective_chat.id, *args, **kwargs) + + For the documentation of the arguments, please see :meth:`telegram.Bot.send_poll`. + + Returns: + :class:`telegram.Message`: On success, instance representing the message posted. + + """ + return await self.get_bot().send_poll( + chat_id=self.id, + question=question, + options=options, + is_anonymous=is_anonymous, + type=type, # pylint=pylint, + allows_multiple_answers=allows_multiple_answers, + correct_option_id=correct_option_id, + is_closed=is_closed, + disable_notification=disable_notification, + reply_to_message_id=reply_to_message_id, + reply_parameters=reply_parameters, + reply_markup=reply_markup, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + message_effect_id=message_effect_id, + explanation=explanation, + explanation_parse_mode=explanation_parse_mode, + open_period=open_period, + close_date=close_date, + api_kwargs=api_kwargs, + allow_sending_without_reply=allow_sending_without_reply, + explanation_entities=explanation_entities, + protect_content=protect_content, + message_thread_id=message_thread_id, + business_connection_id=business_connection_id, + question_parse_mode=question_parse_mode, + question_entities=question_entities, + ) + + async def send_copy( + self, + from_chat_id: Union[str, int], + message_id: int, + caption: Optional[str] = None, + parse_mode: ODVInput[str] = DEFAULT_NONE, + caption_entities: Optional[Sequence["MessageEntity"]] = None, + disable_notification: ODVInput[bool] = DEFAULT_NONE, + reply_markup: Optional[ReplyMarkup] = None, + protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: Optional[int] = None, + reply_parameters: Optional["ReplyParameters"] = None, + show_caption_above_media: Optional[bool] = None, + *, + reply_to_message_id: Optional[int] = None, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_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, + ) -> "MessageId": + """Shortcut for:: + + await bot.copy_message(chat_id=update.effective_chat.id, *args, **kwargs) + + For the documentation of the arguments, please see :meth:`telegram.Bot.copy_message`. + + .. seealso:: :meth:`copy_message`, :meth:`send_copies`, :meth:`copy_messages`. + + Returns: + :class:`telegram.Message`: On success, instance representing the message posted. + + """ + return await self.get_bot().copy_message( + chat_id=self.id, + from_chat_id=from_chat_id, + message_id=message_id, + caption=caption, + parse_mode=parse_mode, + caption_entities=caption_entities, + disable_notification=disable_notification, + reply_to_message_id=reply_to_message_id, + reply_parameters=reply_parameters, + allow_sending_without_reply=allow_sending_without_reply, + reply_markup=reply_markup, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + protect_content=protect_content, + message_thread_id=message_thread_id, + show_caption_above_media=show_caption_above_media, + ) + + async def copy_message( + self, + chat_id: Union[int, str], + message_id: int, + caption: Optional[str] = None, + parse_mode: ODVInput[str] = DEFAULT_NONE, + caption_entities: Optional[Sequence["MessageEntity"]] = None, + disable_notification: ODVInput[bool] = DEFAULT_NONE, + reply_markup: Optional[ReplyMarkup] = None, + protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: Optional[int] = None, + reply_parameters: Optional["ReplyParameters"] = None, + show_caption_above_media: Optional[bool] = None, + *, + reply_to_message_id: Optional[int] = None, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_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, + ) -> "MessageId": + """Shortcut for:: + + await bot.copy_message(from_chat_id=update.effective_chat.id, *args, **kwargs) + + For the documentation of the arguments, please see :meth:`telegram.Bot.copy_message`. + + .. seealso:: :meth:`send_copy`, :meth:`send_copies`, :meth:`copy_messages`. + + Returns: + :class:`telegram.MessageId`: On success, returns the MessageId of the sent message. + + """ + return await self.get_bot().copy_message( + from_chat_id=self.id, + chat_id=chat_id, + message_id=message_id, + caption=caption, + parse_mode=parse_mode, + caption_entities=caption_entities, + disable_notification=disable_notification, + reply_to_message_id=reply_to_message_id, + reply_parameters=reply_parameters, + allow_sending_without_reply=allow_sending_without_reply, + reply_markup=reply_markup, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + protect_content=protect_content, + message_thread_id=message_thread_id, + show_caption_above_media=show_caption_above_media, + ) + + async def send_copies( + self, + from_chat_id: Union[str, int], + message_ids: Sequence[int], + disable_notification: ODVInput[bool] = DEFAULT_NONE, + protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: Optional[int] = None, + remove_caption: 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, + ) -> Tuple["MessageId", ...]: + """Shortcut for:: + + await bot.copy_messages(chat_id=update.effective_chat.id, *args, **kwargs) + + For the documentation of the arguments, please see :meth:`telegram.Bot.copy_messages`. + + .. seealso:: :meth:`copy_message`, :meth:`send_copy`, :meth:`copy_messages`. + + .. versionadded:: 20.8 + + Returns: + Tuple[:class:`telegram.MessageId`]: On success, a tuple of :class:`~telegram.MessageId` + of the sent messages is returned. + + """ + return await self.get_bot().copy_messages( + chat_id=self.id, + from_chat_id=from_chat_id, + message_ids=message_ids, + disable_notification=disable_notification, + protect_content=protect_content, + message_thread_id=message_thread_id, + remove_caption=remove_caption, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def copy_messages( + self, + chat_id: Union[str, int], + message_ids: Sequence[int], + disable_notification: ODVInput[bool] = DEFAULT_NONE, + protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: Optional[int] = None, + remove_caption: 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, + ) -> Tuple["MessageId", ...]: + """Shortcut for:: + + await bot.copy_messages(from_chat_id=update.effective_chat.id, *args, **kwargs) + + For the documentation of the arguments, please see :meth:`telegram.Bot.copy_messages`. + + .. seealso:: :meth:`copy_message`, :meth:`send_copy`, :meth:`send_copies`. + + .. versionadded:: 20.8 + + Returns: + Tuple[:class:`telegram.MessageId`]: On success, a tuple of :class:`~telegram.MessageId` + of the sent messages is returned. + + """ + return await self.get_bot().copy_messages( + from_chat_id=self.id, + chat_id=chat_id, + message_ids=message_ids, + disable_notification=disable_notification, + protect_content=protect_content, + message_thread_id=message_thread_id, + remove_caption=remove_caption, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def forward_from( + self, + from_chat_id: Union[str, int], + message_id: int, + disable_notification: ODVInput[bool] = DEFAULT_NONE, + protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_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, + ) -> "Message": + """Shortcut for:: + + await bot.forward_message(chat_id=update.effective_chat.id, *args, **kwargs) + + For the documentation of the arguments, please see :meth:`telegram.Bot.forward_message`. + + .. seealso:: :meth:`forward_to`, :meth:`forward_messages_from`, :meth:`forward_messages_to` + + .. versionadded:: 20.0 + + Returns: + :class:`telegram.Message`: On success, instance representing the message posted. + + """ + return await self.get_bot().forward_message( + chat_id=self.id, + from_chat_id=from_chat_id, + message_id=message_id, + disable_notification=disable_notification, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + protect_content=protect_content, + message_thread_id=message_thread_id, + ) + + async def forward_to( + self, + chat_id: Union[int, str], + message_id: int, + disable_notification: ODVInput[bool] = DEFAULT_NONE, + protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_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, + ) -> "Message": + """Shortcut for:: + + await bot.forward_message(from_chat_id=update.effective_chat.id, *args, **kwargs) + + For the documentation of the arguments, please see :meth:`telegram.Bot.forward_message`. + + .. seealso:: :meth:`forward_from`, :meth:`forward_messages_from`, + :meth:`forward_messages_to` + + .. versionadded:: 20.0 + + Returns: + :class:`telegram.Message`: On success, instance representing the message posted. + + """ + return await self.get_bot().forward_message( + from_chat_id=self.id, + chat_id=chat_id, + message_id=message_id, + disable_notification=disable_notification, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + protect_content=protect_content, + message_thread_id=message_thread_id, + ) + + async def forward_messages_from( + self, + from_chat_id: Union[str, int], + message_ids: Sequence[int], + disable_notification: ODVInput[bool] = DEFAULT_NONE, + protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_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, + ) -> Tuple["MessageId", ...]: + """Shortcut for:: + + await bot.forward_messages(chat_id=update.effective_chat.id, *args, **kwargs) + + For the documentation of the arguments, please see :meth:`telegram.Bot.forward_messages`. + + .. seealso:: :meth:`forward_to`, :meth:`forward_from`, :meth:`forward_messages_to`. + + .. versionadded:: 20.8 + + Returns: + Tuple[:class:`telegram.MessageId`]: On success, a tuple of :class:`~telegram.MessageId` + of sent messages is returned. + + """ + return await self.get_bot().forward_messages( + chat_id=self.id, + from_chat_id=from_chat_id, + message_ids=message_ids, + disable_notification=disable_notification, + protect_content=protect_content, + message_thread_id=message_thread_id, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def forward_messages_to( + self, + chat_id: Union[int, str], + message_ids: Sequence[int], + disable_notification: ODVInput[bool] = DEFAULT_NONE, + protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_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, + ) -> Tuple["MessageId", ...]: + """Shortcut for:: + + await bot.forward_messages(from_chat_id=update.effective_chat.id, *args, **kwargs) + + For the documentation of the arguments, please see :meth:`telegram.Bot.forward_messages`. + + .. seealso:: :meth:`forward_from`, :meth:`forward_to`, :meth:`forward_messages_from`. + + .. versionadded:: 20.8 + + Returns: + Tuple[:class:`telegram.MessageId`]: On success, a tuple of :class:`~telegram.MessageId` + of sent messages is returned. + + """ + return await self.get_bot().forward_messages( + from_chat_id=self.id, + chat_id=chat_id, + message_ids=message_ids, + disable_notification=disable_notification, + protect_content=protect_content, + message_thread_id=message_thread_id, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def export_invite_link( + 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, + ) -> str: + """Shortcut for:: + + await bot.export_chat_invite_link(chat_id=update.effective_chat.id, *args, **kwargs) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.export_chat_invite_link`. + + .. versionadded:: 13.4 + + Returns: + :obj:`str`: New invite link on success. + + """ + return await self.get_bot().export_chat_invite_link( + chat_id=self.id, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def create_invite_link( + self, + expire_date: Optional[Union[int, datetime]] = None, + member_limit: Optional[int] = None, + name: Optional[str] = None, + creates_join_request: 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, + ) -> "ChatInviteLink": + """Shortcut for:: + + await bot.create_chat_invite_link(chat_id=update.effective_chat.id, *args, **kwargs) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.create_chat_invite_link`. + + .. versionadded:: 13.4 + + .. versionchanged:: 13.8 + Edited signature according to the changes of + :meth:`telegram.Bot.create_chat_invite_link`. + + Returns: + :class:`telegram.ChatInviteLink` + + """ + return await self.get_bot().create_chat_invite_link( + chat_id=self.id, + expire_date=expire_date, + member_limit=member_limit, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + name=name, + creates_join_request=creates_join_request, + ) + + async def edit_invite_link( + self, + invite_link: Union[str, "ChatInviteLink"], + expire_date: Optional[Union[int, datetime]] = None, + member_limit: Optional[int] = None, + name: Optional[str] = None, + creates_join_request: 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, + ) -> "ChatInviteLink": + """Shortcut for:: + + await bot.edit_chat_invite_link(chat_id=update.effective_chat.id, *args, **kwargs) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.edit_chat_invite_link`. + + .. versionadded:: 13.4 + + .. versionchanged:: 13.8 + Edited signature according to the changes of :meth:`telegram.Bot.edit_chat_invite_link`. + + Returns: + :class:`telegram.ChatInviteLink` + + """ + return await self.get_bot().edit_chat_invite_link( + chat_id=self.id, + invite_link=invite_link, + expire_date=expire_date, + member_limit=member_limit, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + name=name, + creates_join_request=creates_join_request, + ) + + async def revoke_invite_link( + self, + invite_link: Union[str, "ChatInviteLink"], + *, + 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.revoke_chat_invite_link(chat_id=update.effective_chat.id, *args, **kwargs) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.revoke_chat_invite_link`. + + .. versionadded:: 13.4 + + Returns: + :class:`telegram.ChatInviteLink` + + """ + return await self.get_bot().revoke_chat_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, + ) + + async def create_subscription_invite_link( + self, + subscription_period: int, + 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, + *, + 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.approve_chat_join_request(chat_id=update.effective_chat.id, *args, **kwargs) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.approve_chat_join_request`. + + .. versionadded:: 13.8 + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + """ + return await self.get_bot().approve_chat_join_request( + chat_id=self.id, + user_id=user_id, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def decline_join_request( + 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: + """Shortcut for:: + + await bot.decline_chat_join_request(chat_id=update.effective_chat.id, *args, **kwargs) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.decline_chat_join_request`. + + .. versionadded:: 13.8 + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + """ + return await self.get_bot().decline_chat_join_request( + chat_id=self.id, + user_id=user_id, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def set_menu_button( + self, + menu_button: Optional[MenuButton] = 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.set_chat_menu_button(chat_id=update.effective_chat.id, *args, **kwargs) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.set_chat_menu_button`. + + Caution: + Can only work, if the chat is a private chat. + + .. seealso:: :meth:`get_menu_button` + + .. versionadded:: 20.0 + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + """ + return await self.get_bot().set_chat_menu_button( + chat_id=self.id, + menu_button=menu_button, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def create_forum_topic( + self, + name: str, + icon_color: Optional[int] = None, + icon_custom_emoji_id: 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, + ) -> ForumTopic: + """Shortcut for:: + + await bot.create_forum_topic(chat_id=update.effective_chat.id, *args, **kwargs) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.create_forum_topic`. + + .. versionadded:: 20.0 + + Returns: + :class:`telegram.ForumTopic` + """ + return await self.get_bot().create_forum_topic( + chat_id=self.id, + name=name, + icon_color=icon_color, + icon_custom_emoji_id=icon_custom_emoji_id, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def edit_forum_topic( + self, + message_thread_id: int, + name: Optional[str] = None, + icon_custom_emoji_id: 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.edit_forum_topic(chat_id=update.effective_chat.id, *args, **kwargs) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.edit_forum_topic`. + + .. versionadded:: 20.0 + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + """ + return await self.get_bot().edit_forum_topic( + chat_id=self.id, + message_thread_id=message_thread_id, + name=name, + icon_custom_emoji_id=icon_custom_emoji_id, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def close_forum_topic( + self, + message_thread_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: + """Shortcut for:: + + await bot.close_forum_topic(chat_id=update.effective_chat.id, *args, **kwargs) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.close_forum_topic`. + + .. versionadded:: 20.0 + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + """ + return await self.get_bot().close_forum_topic( + chat_id=self.id, + message_thread_id=message_thread_id, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def reopen_forum_topic( + self, + message_thread_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: + """Shortcut for:: + + await bot.reopen_forum_topic(chat_id=update.effective_chat.id, *args, **kwargs) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.reopen_forum_topic`. + + .. versionadded:: 20.0 + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + """ + return await self.get_bot().reopen_forum_topic( + chat_id=self.id, + message_thread_id=message_thread_id, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def delete_forum_topic( + self, + message_thread_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: + """Shortcut for:: + + await bot.delete_forum_topic(chat_id=update.effective_chat.id, *args, **kwargs) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.delete_forum_topic`. + + .. versionadded:: 20.0 + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + """ + return await self.get_bot().delete_forum_topic( + chat_id=self.id, + message_thread_id=message_thread_id, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def unpin_all_forum_topic_messages( + self, + message_thread_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: + """Shortcut for:: + + await bot.unpin_all_forum_topic_messages(chat_id=update.effective_chat.id, + *args, **kwargs) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.unpin_all_forum_topic_messages`. + + .. versionadded:: 20.0 + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + """ + return await self.get_bot().unpin_all_forum_topic_messages( + chat_id=self.id, + message_thread_id=message_thread_id, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def unpin_all_general_forum_topic_messages( + 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.unpin_all_general_forum_topic_messages(chat_id=update.effective_chat.id, + *args, **kwargs) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.unpin_all_general_forum_topic_messages`. + + .. versionadded:: 20.5 + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + """ + return await self.get_bot().unpin_all_general_forum_topic_messages( + chat_id=self.id, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def edit_general_forum_topic( + self, + name: 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: + """Shortcut for:: + + await bot.edit_general_forum_topic( + chat_id=update.effective_chat.id, *args, **kwargs + ) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.edit_general_forum_topic`. + + .. versionadded:: 20.0 + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + """ + return await self.get_bot().edit_general_forum_topic( + chat_id=self.id, + name=name, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def close_general_forum_topic( + 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.close_general_forum_topic(chat_id=update.effective_chat.id, *args, **kwargs) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.close_general_forum_topic`. + + .. versionadded:: 20.0 + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + """ + return await self.get_bot().close_general_forum_topic( + chat_id=self.id, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def reopen_general_forum_topic( + 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.reopen_general_forum_topic( + chat_id=update.effective_chat.id, *args, **kwargs + ) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.reopen_general_forum_topic`. + + .. versionadded:: 20.0 + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + """ + return await self.get_bot().reopen_general_forum_topic( + chat_id=self.id, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def hide_general_forum_topic( + 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.hide_general_forum_topic(chat_id=update.effective_chat.id, *args, **kwargs) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.hide_general_forum_topic`. + + .. versionadded:: 20.0 + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + """ + return await self.get_bot().hide_general_forum_topic( + chat_id=self.id, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def unhide_general_forum_topic( + 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.unhide_general_forum_topic ( + chat_id=update.effective_chat.id, *args, **kwargs + ) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.unhide_general_forum_topic`. + + .. versionadded:: 20.0 + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + """ + return await self.get_bot().unhide_general_forum_topic( + chat_id=self.id, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def get_menu_button( + 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, + ) -> MenuButton: + """Shortcut for:: + + await bot.get_chat_menu_button(chat_id=update.effective_chat.id, *args, **kwargs) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.get_chat_menu_button`. + + Caution: + Can only work, if the chat is a private chat. + + .. seealso:: :meth:`set_menu_button` + + .. versionadded:: 20.0 + + Returns: + :class:`telegram.MenuButton`: On success, the current menu button is returned. + """ + return await self.get_bot().get_chat_menu_button( + chat_id=self.id, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def get_user_chat_boosts( + 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, + ) -> "UserChatBoosts": + """Shortcut for:: + + await bot.get_user_chat_boosts(chat_id=update.effective_chat.id, *args, **kwargs) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.get_user_chat_boosts`. + + .. versionadded:: 20.8 + + Returns: + :class:`telegram.UserChatBoosts`: On success, returns the boosts applied in the chat. + """ + return await self.get_bot().get_user_chat_boosts( + chat_id=self.id, + user_id=user_id, + api_kwargs=api_kwargs, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + ) + + async def set_message_reaction( + self, + message_id: int, + reaction: Optional[Union[Sequence[Union[ReactionType, str]], ReactionType, str]] = None, + is_big: 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.set_message_reaction(chat_id=update.effective_chat.id, *args, **kwargs) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.set_message_reaction`. + + .. versionadded:: 20.8 + + Returns: + :obj:`bool` On success, :obj:`True` is returned. + """ + return await self.get_bot().set_message_reaction( + chat_id=self.id, + message_id=message_id, + reaction=reaction, + is_big=is_big, + 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, + star_count: int, + media: Sequence["InputPaidMedia"], + caption: Optional[str] = None, + parse_mode: ODVInput[str] = DEFAULT_NONE, + caption_entities: Optional[Sequence["MessageEntity"]] = None, + show_caption_above_media: Optional[bool] = None, + disable_notification: ODVInput[bool] = DEFAULT_NONE, + protect_content: ODVInput[bool] = DEFAULT_NONE, + reply_parameters: Optional["ReplyParameters"] = None, + reply_markup: Optional[ReplyMarkup] = None, + business_connection_id: Optional[str] = None, + *, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + reply_to_message_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, + ) -> "Message": + """Shortcut for:: + + await bot.send_paid_media(chat_id=update.effective_chat.id, *args, **kwargs) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.send_paid_media`. + + .. versionadded:: 21.4 + + Returns: + :class:`telegram.Message`: On success, instance representing the message posted. + """ + return await self.get_bot().send_paid_media( + chat_id=self.id, + star_count=star_count, + media=media, + caption=caption, + parse_mode=parse_mode, + caption_entities=caption_entities, + show_caption_above_media=show_caption_above_media, + disable_notification=disable_notification, + protect_content=protect_content, + reply_parameters=reply_parameters, + reply_markup=reply_markup, + allow_sending_without_reply=allow_sending_without_reply, + reply_to_message_id=reply_to_message_id, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + business_connection_id=business_connection_id, + ) + + +class Chat(_ChatBase): + """This object represents a chat. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`id` is equal. + + .. versionchanged:: 20.0 + + * Removed the deprecated methods ``kick_member`` and ``get_members_count``. + * The following are now keyword-only arguments in Bot methods: + ``location``, ``filename``, ``contact``, ``{read, write, connect, pool}_timeout``, + ``api_kwargs``. Use a named argument for those, + and notice that some positional arguments changed position as a result. + + .. versionchanged:: 20.0 + Removed the attribute ``all_members_are_administrators``. As long as Telegram provides + this field for backwards compatibility, it is available through + :attr:`~telegram.TelegramObject.api_kwargs`. + + .. versionchanged:: 21.3 + As per Bot API 7.3, most of the arguments and attributes of this class have now moved to + :class:`telegram.ChatFullInfo`. + + Args: + id (:obj:`int`): Unique identifier for this chat. + type (:obj:`str`): Type of chat, can be either :attr:`PRIVATE`, :attr:`GROUP`, + :attr:`SUPERGROUP` or :attr:`CHANNEL`. + title (:obj:`str`, optional): Title, for supergroups, channels and group chats. + username (:obj:`str`, optional): Username, for private chats, supergroups and channels if + available. + first_name (:obj:`str`, optional): First name of the other party in a private chat. + last_name (:obj:`str`, optional): Last name of the other party in a private chat. + is_forum (:obj:`bool`, optional): :obj:`True`, if the supergroup chat is a forum + (has topics_ enabled). + + .. versionadded:: 20.0 + + Attributes: + id (:obj:`int`): Unique identifier for this chat. + type (:obj:`str`): Type of chat, can be either :attr:`PRIVATE`, :attr:`GROUP`, + :attr:`SUPERGROUP` or :attr:`CHANNEL`. + title (:obj:`str`): Optional. Title, for supergroups, channels and group chats. + username (:obj:`str`): Optional. Username, for private chats, supergroups and channels if + available. + first_name (:obj:`str`): Optional. First name of the other party in a private chat. + last_name (:obj:`str`): Optional. Last name of the other party in a private chat. + is_forum (:obj:`bool`): Optional. :obj:`True`, if the supergroup chat is a forum + (has topics_ enabled). + + .. versionadded:: 20.0 + + .. _topics: https://telegram.org/blog/topics-in-groups-collectible-usernames#topics-in-groups + """ + + __slots__ = () diff --git a/_chatadministratorrights.py b/_chatadministratorrights.py new file mode 100644 index 0000000000000000000000000000000000000000..f0d0b033f62ff643bf5394879daf11519563748a --- /dev/null +++ b/_chatadministratorrights.py @@ -0,0 +1,247 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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 the class which represents a Telegram ChatAdministratorRights.""" +from typing import Optional + +from telegram._telegramobject import TelegramObject +from telegram._utils.types import JSONDict + + +class ChatAdministratorRights(TelegramObject): + """Represents the rights of an administrator in a chat. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`is_anonymous`, :attr:`can_manage_chat`, + :attr:`can_delete_messages`, :attr:`can_manage_video_chats`, :attr:`can_restrict_members`, + :attr:`can_promote_members`, :attr:`can_change_info`, :attr:`can_invite_users`, + :attr:`can_post_messages`, :attr:`can_edit_messages`, :attr:`can_pin_messages`, + :attr:`can_manage_topics`, :attr:`can_post_stories`, :attr:`can_delete_stories`, and + :attr:`can_edit_stories` are equal. + + .. versionadded:: 20.0 + + .. versionchanged:: 20.0 + :attr:`can_manage_topics` is considered as well when comparing objects of + this type in terms of equality. + + .. versionchanged:: 20.6 + :attr:`can_post_stories`, :attr:`can_edit_stories`, and :attr:`can_delete_stories` are + considered as well when comparing objects of this type in terms of equality. + + .. versionchanged:: 21.1 + As of this version, :attr:`can_post_stories`, :attr:`can_edit_stories`, + and :attr:`can_delete_stories` is now required. Thus, the order of arguments had to be + changed. + + Args: + is_anonymous (:obj:`bool`): :obj:`True`, if the user's presence in the chat is hidden. + can_manage_chat (:obj:`bool`): :obj:`True`, if the administrator can access the chat event + log, get boost list, see hidden supergroup and channel members, report spam messages + and ignore slow mode. Implied by any other administrator privilege. + can_delete_messages (:obj:`bool`): :obj:`True`, if the administrator can delete messages of + other users. + can_manage_video_chats (:obj:`bool`): :obj:`True`, if the administrator can manage video + chats. + can_restrict_members (:obj:`bool`): :obj:`True`, if the administrator can restrict, ban or + unban chat members, or access supergroup statistics. + can_promote_members (:obj:`bool`): :obj:`True`, if the administrator can add new + administrators with a subset of their own privileges or demote administrators + that they have promoted, directly or indirectly (promoted by administrators that + were appointed by the user). + can_change_info (:obj:`bool`): :obj:`True`, if the user is allowed to change the chat title + , photo and other settings. + can_invite_users (:obj:`bool`): :obj:`True`, if the user is allowed to invite new users to + the chat. + can_post_messages (:obj:`bool`, optional): :obj:`True`, if the administrator can post + messages in the channel, or access channel statistics; for channels only. + can_edit_messages (:obj:`bool`, optional): :obj:`True`, if the administrator can edit + messages of other users and can pin messages; for channels only. + can_pin_messages (:obj:`bool`, optional): :obj:`True`, if the user is allowed to pin + messages; for groups and supergroups only. + can_post_stories (:obj:`bool`): :obj:`True`, if the administrator can post + stories to the chat. + + .. versionadded:: 20.6 + .. versionchanged:: 21.0 + |non_optional_story_argument| + can_edit_stories (:obj:`bool`): :obj:`True`, if the administrator can edit stories posted + by other users, post stories to the chat page, pin chat stories, and access the chat's + story archive + + .. versionadded:: 20.6 + .. versionchanged:: 21.0 + |non_optional_story_argument| + can_delete_stories (:obj:`bool`): :obj:`True`, if the administrator can delete + stories posted by other users. + + .. versionadded:: 20.6 + .. versionchanged:: 21.0 + |non_optional_story_argument| + can_manage_topics (:obj:`bool`, optional): :obj:`True`, if the user is allowed + to create, rename, close, and reopen forum topics; for supergroups only. + + .. versionadded:: 20.0 + + Attributes: + is_anonymous (:obj:`bool`): :obj:`True`, if the user's presence in the chat is hidden. + can_manage_chat (:obj:`bool`): :obj:`True`, if the administrator can access the chat event + log, get boost list, see hidden supergroup and channel members, report spam messages + and ignore slow mode. Implied by any other administrator privilege. + can_delete_messages (:obj:`bool`): :obj:`True`, if the administrator can delete messages of + other users. + can_manage_video_chats (:obj:`bool`): :obj:`True`, if the administrator can manage video + chats. + can_restrict_members (:obj:`bool`): :obj:`True`, if the administrator can restrict, ban or + unban chat members, or access supergroup statistics. + can_promote_members (:obj:`bool`): :obj:`True`, if the administrator can add new + administrators with a subset of their own privileges or demote administrators that he + has promoted, directly or indirectly (promoted by administrators that were appointed by + the user.) + can_change_info (:obj:`bool`): :obj:`True`, if the user is allowed to change the chat title + ,photo and other settings. + can_invite_users (:obj:`bool`): :obj:`True`, if the user is allowed to invite new users to + the chat. + can_post_messages (:obj:`bool`): Optional. :obj:`True`, if the administrator can post + messages in the channel, or access channel statistics; for channels only. + can_edit_messages (:obj:`bool`): Optional. :obj:`True`, if the administrator can edit + messages of other users and can pin messages; for channels only. + can_pin_messages (:obj:`bool`): Optional. :obj:`True`, if the user is allowed to pin + messages; for groups and supergroups only. + can_post_stories (:obj:`bool`): :obj:`True`, if the administrator can post + stories to the chat. + + .. versionadded:: 20.6 + .. versionchanged:: 21.0 + |non_optional_story_argument| + can_edit_stories (:obj:`bool`): :obj:`True`, if the administrator can edit stories posted + by other users, post stories to the chat page, pin chat stories, and access the chat's + story archive + + .. versionadded:: 20.6 + .. versionchanged:: 21.0 + |non_optional_story_argument| + can_delete_stories (:obj:`bool`): :obj:`True`, if the administrator can delete + stories posted by other users. + + .. versionadded:: 20.6 + .. versionchanged:: 21.0 + |non_optional_story_argument| + can_manage_topics (:obj:`bool`): Optional. :obj:`True`, if the user is allowed + to create, rename, close, and reopen forum topics; for supergroups only. + + .. versionadded:: 20.0 + """ + + __slots__ = ( + "can_change_info", + "can_delete_messages", + "can_delete_stories", + "can_edit_messages", + "can_edit_stories", + "can_invite_users", + "can_manage_chat", + "can_manage_topics", + "can_manage_video_chats", + "can_pin_messages", + "can_post_messages", + "can_post_stories", + "can_promote_members", + "can_restrict_members", + "is_anonymous", + ) + + def __init__( + self, + is_anonymous: bool, + can_manage_chat: bool, + can_delete_messages: bool, + can_manage_video_chats: bool, + can_restrict_members: bool, + can_promote_members: bool, + can_change_info: bool, + can_invite_users: bool, + can_post_stories: bool, + can_edit_stories: bool, + can_delete_stories: bool, + can_post_messages: Optional[bool] = None, + can_edit_messages: Optional[bool] = None, + can_pin_messages: Optional[bool] = None, + can_manage_topics: Optional[bool] = None, + *, + api_kwargs: Optional[JSONDict] = None, + ) -> None: + super().__init__(api_kwargs=api_kwargs) + # Required + self.is_anonymous: bool = is_anonymous + self.can_manage_chat: bool = can_manage_chat + self.can_delete_messages: bool = can_delete_messages + self.can_manage_video_chats: bool = can_manage_video_chats + self.can_restrict_members: bool = can_restrict_members + self.can_promote_members: bool = can_promote_members + self.can_change_info: bool = can_change_info + self.can_invite_users: bool = can_invite_users + self.can_post_stories: bool = can_post_stories + self.can_edit_stories: bool = can_edit_stories + self.can_delete_stories: bool = can_delete_stories + # Optionals + self.can_post_messages: Optional[bool] = can_post_messages + self.can_edit_messages: Optional[bool] = can_edit_messages + self.can_pin_messages: Optional[bool] = can_pin_messages + self.can_manage_topics: Optional[bool] = can_manage_topics + + self._id_attrs = ( + self.is_anonymous, + self.can_manage_chat, + self.can_delete_messages, + self.can_manage_video_chats, + self.can_restrict_members, + self.can_promote_members, + self.can_change_info, + self.can_invite_users, + self.can_post_messages, + self.can_edit_messages, + self.can_pin_messages, + self.can_manage_topics, + self.can_post_stories, + self.can_edit_stories, + self.can_delete_stories, + ) + + self._freeze() + + @classmethod + def all_rights(cls) -> "ChatAdministratorRights": + """ + This method returns the :class:`ChatAdministratorRights` object with all attributes set to + :obj:`True`. This is e.g. useful when changing the bot's default administrator rights with + :meth:`telegram.Bot.set_my_default_administrator_rights`. + + .. versionadded:: 20.0 + """ + return cls(*(True,) * len(cls.__slots__)) + + @classmethod + def no_rights(cls) -> "ChatAdministratorRights": + """ + This method returns the :class:`ChatAdministratorRights` object with all attributes set to + :obj:`False`. + + .. versionadded:: 20.0 + """ + return cls(*(False,) * len(cls.__slots__)) diff --git a/_chatbackground.py b/_chatbackground.py new file mode 100644 index 0000000000000000000000000000000000000000..b33fd4d91aeffeaf43464dfa0da9e177bc631376 --- /dev/null +++ b/_chatbackground.py @@ -0,0 +1,546 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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 objects related to chat backgrounds.""" +from typing import TYPE_CHECKING, Dict, Final, Optional, Sequence, Tuple, Type + +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.types import JSONDict + +if TYPE_CHECKING: + from telegram import Bot + + +class BackgroundFill(TelegramObject): + """Base class for Telegram BackgroundFill Objects. It can be one of: + + * :class:`telegram.BackgroundFillSolid` + * :class:`telegram.BackgroundFillGradient` + * :class:`telegram.BackgroundFillFreeformGradient` + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`type` is equal. + + .. versionadded:: 21.2 + + Args: + type (:obj:`str`): Type of the background fill. Can be one of: + :attr:`~telegram.BackgroundFill.SOLID`, :attr:`~telegram.BackgroundFill.GRADIENT` + or :attr:`~telegram.BackgroundFill.FREEFORM_GRADIENT`. + + Attributes: + type (:obj:`str`): Type of the background fill. Can be one of: + :attr:`~telegram.BackgroundFill.SOLID`, :attr:`~telegram.BackgroundFill.GRADIENT` + or :attr:`~telegram.BackgroundFill.FREEFORM_GRADIENT`. + """ + + __slots__ = ("type",) + + SOLID: Final[constants.BackgroundFillType] = constants.BackgroundFillType.SOLID + """:const:`telegram.constants.BackgroundFillType.SOLID`""" + GRADIENT: Final[constants.BackgroundFillType] = constants.BackgroundFillType.GRADIENT + """:const:`telegram.constants.BackgroundFillType.GRADIENT`""" + FREEFORM_GRADIENT: Final[constants.BackgroundFillType] = ( + constants.BackgroundFillType.FREEFORM_GRADIENT + ) + """:const:`telegram.constants.BackgroundFillType.FREEFORM_GRADIENT`""" + + def __init__( + self, + type: str, # pylint: disable=redefined-builtin + *, + api_kwargs: Optional[JSONDict] = None, + ): + super().__init__(api_kwargs=api_kwargs) + # Required by all subclasses + self.type: str = enum.get_member(constants.BackgroundFillType, type, type) + + self._id_attrs = (self.type,) + self._freeze() + + @classmethod + def de_json( + cls, data: Optional[JSONDict], bot: Optional["Bot"] = None + ) -> Optional["BackgroundFill"]: + """See :meth:`telegram.TelegramObject.de_json`.""" + data = cls._parse_data(data) + + if not data: + return None + + _class_mapping: Dict[str, Type[BackgroundFill]] = { + cls.SOLID: BackgroundFillSolid, + cls.GRADIENT: BackgroundFillGradient, + cls.FREEFORM_GRADIENT: BackgroundFillFreeformGradient, + } + + if cls is BackgroundFill and data.get("type") in _class_mapping: + return _class_mapping[data.pop("type")].de_json(data=data, bot=bot) + + return super().de_json(data=data, bot=bot) + + +class BackgroundFillSolid(BackgroundFill): + """ + The background is filled using the selected color. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`color` is equal. + + .. versionadded:: 21.2 + + Args: + color (:obj:`int`): The color of the background fill in the `RGB24` format. + + Attributes: + type (:obj:`str`): Type of the background fill. Always + :attr:`~telegram.BackgroundFill.SOLID`. + color (:obj:`int`): The color of the background fill in the `RGB24` format. + """ + + __slots__ = ("color",) + + def __init__( + self, + color: int, + *, + api_kwargs: Optional[JSONDict] = None, + ): + super().__init__(type=self.SOLID, api_kwargs=api_kwargs) + + with self._unfrozen(): + self.color: int = color + + self._id_attrs = (self.color,) + + +class BackgroundFillGradient(BackgroundFill): + """ + The background is a gradient fill. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`top_color`, :attr:`bottom_color` + and :attr:`rotation_angle` are equal. + + .. versionadded:: 21.2 + + Args: + top_color (:obj:`int`): Top color of the gradient in the `RGB24` format. + bottom_color (:obj:`int`): Bottom color of the gradient in the `RGB24` format. + rotation_angle (:obj:`int`): Clockwise rotation angle of the background + fill in degrees; + 0-:tg-const:`telegram.constants.BackgroundFillLimit.MAX_ROTATION_ANGLE`. + + + Attributes: + type (:obj:`str`): Type of the background fill. Always + :attr:`~telegram.BackgroundFill.GRADIENT`. + top_color (:obj:`int`): Top color of the gradient in the `RGB24` format. + bottom_color (:obj:`int`): Bottom color of the gradient in the `RGB24` format. + rotation_angle (:obj:`int`): Clockwise rotation angle of the background + fill in degrees; + 0-:tg-const:`telegram.constants.BackgroundFillLimit.MAX_ROTATION_ANGLE`. + """ + + __slots__ = ("bottom_color", "rotation_angle", "top_color") + + def __init__( + self, + top_color: int, + bottom_color: int, + rotation_angle: int, + *, + api_kwargs: Optional[JSONDict] = None, + ): + super().__init__(type=self.GRADIENT, api_kwargs=api_kwargs) + + with self._unfrozen(): + self.top_color: int = top_color + self.bottom_color: int = bottom_color + self.rotation_angle: int = rotation_angle + + self._id_attrs = (self.top_color, self.bottom_color, self.rotation_angle) + + +class BackgroundFillFreeformGradient(BackgroundFill): + """ + The background is a freeform gradient that rotates after every message in the chat. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`colors` is equal. + + .. versionadded:: 21.2 + + Args: + colors (Sequence[:obj:`int`]): A list of the 3 or 4 base colors that are used to + generate the freeform gradient in the `RGB24` format + + Attributes: + type (:obj:`str`): Type of the background fill. Always + :attr:`~telegram.BackgroundFill.FREEFORM_GRADIENT`. + colors (Sequence[:obj:`int`]): A list of the 3 or 4 base colors that are used to + generate the freeform gradient in the `RGB24` format + """ + + __slots__ = ("colors",) + + def __init__( + self, + colors: Sequence[int], + *, + api_kwargs: Optional[JSONDict] = None, + ): + super().__init__(type=self.FREEFORM_GRADIENT, api_kwargs=api_kwargs) + + with self._unfrozen(): + self.colors: Tuple[int, ...] = parse_sequence_arg(colors) + + self._id_attrs = (self.colors,) + + +class BackgroundType(TelegramObject): + """Base class for Telegram BackgroundType Objects. It can be one of: + + * :class:`telegram.BackgroundTypeFill` + * :class:`telegram.BackgroundTypeWallpaper` + * :class:`telegram.BackgroundTypePattern` + * :class:`telegram.BackgroundTypeChatTheme`. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`type` is equal. + + .. versionadded:: 21.2 + + Args: + type (:obj:`str`): Type of the background. Can be one of: + :attr:`~telegram.BackgroundType.FILL`, :attr:`~telegram.BackgroundType.WALLPAPER` + :attr:`~telegram.BackgroundType.PATTERN` or + :attr:`~telegram.BackgroundType.CHAT_THEME`. + + Attributes: + type (:obj:`str`): Type of the background. Can be one of: + :attr:`~telegram.BackgroundType.FILL`, :attr:`~telegram.BackgroundType.WALLPAPER` + :attr:`~telegram.BackgroundType.PATTERN` or + :attr:`~telegram.BackgroundType.CHAT_THEME`. + + """ + + __slots__ = ("type",) + + FILL: Final[constants.BackgroundTypeType] = constants.BackgroundTypeType.FILL + """:const:`telegram.constants.BackgroundTypeType.FILL`""" + WALLPAPER: Final[constants.BackgroundTypeType] = constants.BackgroundTypeType.WALLPAPER + """:const:`telegram.constants.BackgroundTypeType.WALLPAPER`""" + PATTERN: Final[constants.BackgroundTypeType] = constants.BackgroundTypeType.PATTERN + """:const:`telegram.constants.BackgroundTypeType.PATTERN`""" + CHAT_THEME: Final[constants.BackgroundTypeType] = constants.BackgroundTypeType.CHAT_THEME + """:const:`telegram.constants.BackgroundTypeType.CHAT_THEME`""" + + def __init__( + self, + type: str, # pylint: disable=redefined-builtin + *, + api_kwargs: Optional[JSONDict] = None, + ): + super().__init__(api_kwargs=api_kwargs) + # Required by all subclasses + self.type: str = enum.get_member(constants.BackgroundTypeType, type, type) + + self._id_attrs = (self.type,) + self._freeze() + + @classmethod + def de_json( + cls, data: Optional[JSONDict], bot: Optional["Bot"] = None + ) -> Optional["BackgroundType"]: + """See :meth:`telegram.TelegramObject.de_json`.""" + data = cls._parse_data(data) + + if not data: + return None + + _class_mapping: Dict[str, Type[BackgroundType]] = { + cls.FILL: BackgroundTypeFill, + cls.WALLPAPER: BackgroundTypeWallpaper, + cls.PATTERN: BackgroundTypePattern, + cls.CHAT_THEME: BackgroundTypeChatTheme, + } + + if cls is BackgroundType and data.get("type") in _class_mapping: + 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) + + if "document" in data: + data["document"] = Document.de_json(data.get("document"), bot) + + return super().de_json(data=data, bot=bot) + + +class BackgroundTypeFill(BackgroundType): + """ + The background is automatically filled based on the selected colors. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`fill` and :attr:`dark_theme_dimming` are equal. + + .. versionadded:: 21.2 + + Args: + 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`. + + Attributes: + type (:obj:`str`): Type of the background. Always + :attr:`~telegram.BackgroundType.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`. + """ + + __slots__ = ("dark_theme_dimming", "fill") + + def __init__( + self, + fill: BackgroundFill, + dark_theme_dimming: int, + *, + api_kwargs: Optional[JSONDict] = None, + ): + super().__init__(type=self.FILL, api_kwargs=api_kwargs) + + with self._unfrozen(): + self.fill: BackgroundFill = fill + self.dark_theme_dimming: int = dark_theme_dimming + + self._id_attrs = (self.fill, self.dark_theme_dimming) + + +class BackgroundTypeWallpaper(BackgroundType): + """ + The background is a wallpaper in the `JPEG` format. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`document` and :attr:`dark_theme_dimming` are equal. + + .. versionadded:: 21.2 + + Args: + 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`. + is_blurred (:obj:`bool`, optional): :obj:`True`, if the wallpaper is downscaled to fit + in a 450x450 square and then box-blurred with radius 12 + is_moving (:obj:`bool`, optional): :obj:`True`, if the background moves slightly + when the device is tilted + + Attributes: + type (:obj:`str`): Type of the background. Always + :attr:`~telegram.BackgroundType.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`. + is_blurred (:obj:`bool`): Optional. :obj:`True`, if the wallpaper is downscaled to fit + in a 450x450 square and then box-blurred with radius 12 + is_moving (:obj:`bool`): Optional. :obj:`True`, if the background moves slightly + when the device is tilted + """ + + __slots__ = ("dark_theme_dimming", "document", "is_blurred", "is_moving") + + def __init__( + self, + document: Document, + dark_theme_dimming: int, + is_blurred: Optional[bool] = None, + is_moving: Optional[bool] = None, + *, + api_kwargs: Optional[JSONDict] = None, + ): + super().__init__(type=self.WALLPAPER, api_kwargs=api_kwargs) + + with self._unfrozen(): + # Required + self.document: Document = document + self.dark_theme_dimming: int = dark_theme_dimming + # Optionals + self.is_blurred: Optional[bool] = is_blurred + self.is_moving: Optional[bool] = is_moving + + self._id_attrs = (self.document, self.dark_theme_dimming) + + +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 + chosen by the user. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`document` and :attr:`fill` and :attr:`intensity` are equal. + + .. versionadded:: 21.2 + + Args: + 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; + 0-:tg-const:`telegram.constants.BackgroundTypeLimit.MAX_INTENSITY`. + is_inverted (:obj:`int`, optional): :obj:`True`, if the background fill must be applied + only to the pattern itself. All other pixels are black in this case. For dark + themes only. + is_moving (:obj:`bool`, optional): :obj:`True`, if the background moves slightly + when the device is tilted. + + Attributes: + type (:obj:`str`): Type of the background. Always + :attr:`~telegram.BackgroundType.PATTERN`. + 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; + 0-:tg-const:`telegram.constants.BackgroundTypeLimit.MAX_INTENSITY`. + is_inverted (:obj:`int`): Optional. :obj:`True`, if the background fill must be applied + only to the pattern itself. All other pixels are black in this case. For dark + themes only. + is_moving (:obj:`bool`): Optional. :obj:`True`, if the background moves slightly + when the device is tilted. + """ + + __slots__ = ( + "document", + "fill", + "intensity", + "is_inverted", + "is_moving", + ) + + def __init__( + self, + document: Document, + fill: BackgroundFill, + intensity: int, + is_inverted: Optional[bool] = None, + is_moving: Optional[bool] = None, + *, + api_kwargs: Optional[JSONDict] = None, + ): + super().__init__(type=self.PATTERN, api_kwargs=api_kwargs) + + with self._unfrozen(): + # Required + self.document: Document = document + self.fill: BackgroundFill = fill + self.intensity: int = intensity + # Optionals + self.is_inverted: Optional[bool] = is_inverted + self.is_moving: Optional[bool] = is_moving + + self._id_attrs = (self.document, self.fill, self.intensity) + + +class BackgroundTypeChatTheme(BackgroundType): + """ + The background is taken directly from a built-in chat theme. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`theme_name` is equal. + + .. versionadded:: 21.2 + + Args: + theme_name (:obj:`str`): Name of the chat theme, which is usually an emoji. + + Attributes: + type (:obj:`str`): Type of the background. Always + :attr:`~telegram.BackgroundType.CHAT_THEME`. + theme_name (:obj:`str`): Name of the chat theme, which is usually an emoji. + """ + + __slots__ = ("theme_name",) + + def __init__( + self, + theme_name: str, + *, + api_kwargs: Optional[JSONDict] = None, + ): + super().__init__(type=self.CHAT_THEME, api_kwargs=api_kwargs) + + with self._unfrozen(): + self.theme_name: str = theme_name + + self._id_attrs = (self.theme_name,) + + +class ChatBackground(TelegramObject): + """ + This object represents a chat background. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`type` is equal. + + .. versionadded:: 21.2 + + Args: + type (:class:`telegram.BackgroundType`): Type of the background. + + Attributes: + type (:class:`telegram.BackgroundType`): Type of the background. + """ + + __slots__ = ("type",) + + def __init__( + self, + type: BackgroundType, # pylint: disable=redefined-builtin + *, + api_kwargs: Optional[JSONDict] = None, + ): + super().__init__(api_kwargs=api_kwargs) + self.type: BackgroundType = type + + self._id_attrs = (self.type,) + self._freeze() + + @classmethod + def de_json( + cls, data: Optional[JSONDict], bot: Optional["Bot"] = None + ) -> Optional["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) + + return super().de_json(data=data, bot=bot) diff --git a/_chatboost.py b/_chatboost.py new file mode 100644 index 0000000000000000000000000000000000000000..7b972eec6d8461d57ea2c797e0e50228990a5cc6 --- /dev/null +++ b/_chatboost.py @@ -0,0 +1,451 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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 the classes that represent Telegram ChatBoosts.""" + +from datetime import datetime +from typing import TYPE_CHECKING, Dict, Final, Optional, Sequence, Tuple, Type + +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.datetime import extract_tzinfo_from_defaults, from_timestamp +from telegram._utils.types import JSONDict + +if TYPE_CHECKING: + from telegram import Bot + + +class ChatBoostAdded(TelegramObject): + """ + This object represents a service message about a user boosting a chat. + + Objects of this class are comparable in terms of equality. + Two objects of this class are considered equal, if their + :attr:`boost_count` are equal. + + .. versionadded:: 21.0 + + Args: + boost_count (:obj:`int`): Number of boosts added by the user. + + Attributes: + boost_count (:obj:`int`): Number of boosts added by the user. + + """ + + __slots__ = ("boost_count",) + + def __init__( + self, + boost_count: int, + *, + api_kwargs: Optional[JSONDict] = None, + ) -> None: + super().__init__(api_kwargs=api_kwargs) + self.boost_count: int = boost_count + self._id_attrs = (self.boost_count,) + + self._freeze() + + +class ChatBoostSource(TelegramObject): + """ + Base class for Telegram ChatBoostSource objects. It can be one of: + + * :class:`telegram.ChatBoostSourcePremium` + * :class:`telegram.ChatBoostSourceGiftCode` + * :class:`telegram.ChatBoostSourceGiveaway` + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`source` is equal. + + .. versionadded:: 20.8 + + Args: + source (:obj:`str`): The source of the chat boost. Can be one of: + :attr:`~telegram.ChatBoostSource.PREMIUM`, :attr:`~telegram.ChatBoostSource.GIFT_CODE`, + or :attr:`~telegram.ChatBoostSource.GIVEAWAY`. + + Attributes: + source (:obj:`str`): The source of the chat boost. Can be one of: + :attr:`~telegram.ChatBoostSource.PREMIUM`, :attr:`~telegram.ChatBoostSource.GIFT_CODE`, + or :attr:`~telegram.ChatBoostSource.GIVEAWAY`. + """ + + __slots__ = ("source",) + + PREMIUM: Final[str] = constants.ChatBoostSources.PREMIUM + """:const:`telegram.constants.ChatBoostSources.PREMIUM`""" + GIFT_CODE: Final[str] = constants.ChatBoostSources.GIFT_CODE + """:const:`telegram.constants.ChatBoostSources.GIFT_CODE`""" + GIVEAWAY: Final[str] = constants.ChatBoostSources.GIVEAWAY + """:const:`telegram.constants.ChatBoostSources.GIVEAWAY`""" + + def __init__(self, source: str, *, api_kwargs: Optional[JSONDict] = None): + super().__init__(api_kwargs=api_kwargs) + + # Required by all subclasses: + self.source: str = enum.get_member(constants.ChatBoostSources, source, source) + + self._id_attrs = (self.source,) + self._freeze() + + @classmethod + def de_json( + cls, data: Optional[JSONDict], bot: Optional["Bot"] = None + ) -> Optional["ChatBoostSource"]: + """See :meth:`telegram.TelegramObject.de_json`.""" + data = cls._parse_data(data) + + if not data: + return None + + _class_mapping: Dict[str, Type[ChatBoostSource]] = { + cls.PREMIUM: ChatBoostSourcePremium, + cls.GIFT_CODE: ChatBoostSourceGiftCode, + cls.GIVEAWAY: ChatBoostSourceGiveaway, + } + + if cls is ChatBoostSource and data.get("source") in _class_mapping: + 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) + + return super().de_json(data=data, bot=bot) + + +class ChatBoostSourcePremium(ChatBoostSource): + """ + The boost was obtained by subscribing to Telegram Premium or by gifting a Telegram Premium + subscription to another user. + + .. versionadded:: 20.8 + + Args: + user (:class:`telegram.User`): User that boosted the chat. + + Attributes: + source (:obj:`str`): The source of the chat boost. Always + :attr:`~telegram.ChatBoostSource.PREMIUM`. + user (:class:`telegram.User`): User that boosted the chat. + """ + + __slots__ = ("user",) + + def __init__(self, user: User, *, api_kwargs: Optional[JSONDict] = None): + super().__init__(source=self.PREMIUM, api_kwargs=api_kwargs) + + with self._unfrozen(): + self.user: User = user + + +class ChatBoostSourceGiftCode(ChatBoostSource): + """ + The boost was obtained by the creation of Telegram Premium gift codes to boost a chat. Each + such code boosts the chat 4 times for the duration of the corresponding Telegram Premium + subscription. + + .. versionadded:: 20.8 + + Args: + user (:class:`telegram.User`): User for which the gift code was created. + + Attributes: + source (:obj:`str`): The source of the chat boost. Always + :attr:`~telegram.ChatBoostSource.GIFT_CODE`. + user (:class:`telegram.User`): User for which the gift code was created. + """ + + __slots__ = ("user",) + + def __init__(self, user: User, *, api_kwargs: Optional[JSONDict] = None): + super().__init__(source=self.GIFT_CODE, api_kwargs=api_kwargs) + + with self._unfrozen(): + self.user: User = user + + +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. + + .. 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. + is_unclaimed (:obj:`bool`, optional): :obj:`True`, if the giveaway was completed, but + there was no user to win the prize. + + Attributes: + source (:obj:`str`): Source of the boost. Always + :attr:`~telegram.ChatBoostSource.GIVEAWAY`. + 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. + 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") + + def __init__( + self, + giveaway_message_id: int, + user: Optional[User] = None, + is_unclaimed: Optional[bool] = None, + *, + api_kwargs: Optional[JSONDict] = None, + ): + super().__init__(source=self.GIVEAWAY, api_kwargs=api_kwargs) + + with self._unfrozen(): + self.giveaway_message_id: int = giveaway_message_id + self.user: Optional[User] = user + self.is_unclaimed: Optional[bool] = is_unclaimed + + +class ChatBoost(TelegramObject): + """ + This object contains information about a chat boost. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`boost_id`, :attr:`add_date`, :attr:`expiration_date`, + and :attr:`source` are equal. + + .. versionadded:: 20.8 + + Args: + boost_id (:obj:`str`): Unique identifier of the boost. + add_date (:obj:`datetime.datetime`): Point in time when the chat was boosted. + expiration_date (:obj:`datetime.datetime`): Point in time when the boost + will automatically expire, unless the booster's Telegram Premium subscription is + prolonged. + source (:class:`telegram.ChatBoostSource`): Source of the added boost. + + Attributes: + boost_id (:obj:`str`): Unique identifier of the boost. + add_date (:obj:`datetime.datetime`): Point in time when the chat was boosted. + |datetime_localization| + expiration_date (:obj:`datetime.datetime`): Point in time when the boost + will automatically expire, unless the booster's Telegram Premium subscription is + prolonged. |datetime_localization| + source (:class:`telegram.ChatBoostSource`): Source of the added boost. + """ + + __slots__ = ("add_date", "boost_id", "expiration_date", "source") + + def __init__( + self, + boost_id: str, + add_date: datetime, + expiration_date: datetime, + source: ChatBoostSource, + *, + api_kwargs: Optional[JSONDict] = None, + ): + 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.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"]: + """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) + 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) + + return super().de_json(data=data, bot=bot) + + +class ChatBoostUpdated(TelegramObject): + """This object represents a boost added to a chat or changed. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`chat`, and :attr:`boost` are equal. + + .. versionadded:: 20.8 + + Args: + chat (:class:`telegram.Chat`): Chat which was boosted. + boost (:class:`telegram.ChatBoost`): Information about the chat boost. + + Attributes: + chat (:class:`telegram.Chat`): Chat which was boosted. + boost (:class:`telegram.ChatBoost`): Information about the chat boost. + """ + + __slots__ = ("boost", "chat") + + def __init__( + self, + chat: Chat, + boost: ChatBoost, + *, + api_kwargs: Optional[JSONDict] = None, + ): + super().__init__(api_kwargs=api_kwargs) + + self.chat: Chat = chat + self.boost: ChatBoost = boost + + self._id_attrs = (self.chat.id, self.boost) + self._freeze() + + @classmethod + def de_json( + cls, data: Optional[JSONDict], bot: Optional["Bot"] = None + ) -> Optional["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) + + return super().de_json(data=data, bot=bot) + + +class ChatBoostRemoved(TelegramObject): + """ + This object represents a boost removed from a chat. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`chat`, :attr:`boost_id`, :attr:`remove_date`, and + :attr:`source` are equal. + + Args: + chat (:class:`telegram.Chat`): Chat which was boosted. + boost_id (:obj:`str`): Unique identifier of the boost. + remove_date (:obj:`datetime.datetime`): Point in time when the boost was removed. + source (:class:`telegram.ChatBoostSource`): Source of the removed boost. + + Attributes: + chat (:class:`telegram.Chat`): Chat which was boosted. + boost_id (:obj:`str`): Unique identifier of the boost. + remove_date (:obj:`datetime.datetime`): Point in time when the boost was removed. + |datetime_localization| + source (:class:`telegram.ChatBoostSource`): Source of the removed boost. + """ + + __slots__ = ("boost_id", "chat", "remove_date", "source") + + def __init__( + self, + chat: Chat, + boost_id: str, + remove_date: datetime, + source: ChatBoostSource, + *, + api_kwargs: Optional[JSONDict] = None, + ): + super().__init__(api_kwargs=api_kwargs) + + self.chat: Chat = chat + self.boost_id: str = boost_id + self.remove_date: 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"]: + """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) + loc_tzinfo = extract_tzinfo_from_defaults(bot) + data["remove_date"] = from_timestamp(data["remove_date"], tzinfo=loc_tzinfo) + + return super().de_json(data=data, bot=bot) + + +class UserChatBoosts(TelegramObject): + """This object represents a list of boosts added to a chat by a user. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`boosts` are equal. + + .. versionadded:: 20.8 + + Args: + boosts (Sequence[:class:`telegram.ChatBoost`]): List of boosts added to the chat by the + user. + + Attributes: + boosts (Tuple[:class:`telegram.ChatBoost`]): List of boosts added to the chat by the user. + """ + + __slots__ = ("boosts",) + + def __init__( + self, + boosts: Sequence[ChatBoost], + *, + api_kwargs: Optional[JSONDict] = None, + ): + super().__init__(api_kwargs=api_kwargs) + + 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"]: + """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) + + return super().de_json(data=data, bot=bot) diff --git a/_chatfullinfo.py b/_chatfullinfo.py new file mode 100644 index 0000000000000000000000000000000000000000..de26101f33c4f8d4e46c78c4acdad1d6114e581c --- /dev/null +++ b/_chatfullinfo.py @@ -0,0 +1,549 @@ +#!/usr/bin/env python +# pylint: disable=redefined-builtin +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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 ChatFullInfo.""" +from datetime import datetime +from typing import TYPE_CHECKING, Optional, Sequence, Tuple + +from telegram._birthdate import Birthdate +from telegram._chat import Chat, _ChatBase +from telegram._chatlocation import ChatLocation +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.datetime import extract_tzinfo_from_defaults, from_timestamp +from telegram._utils.types import JSONDict + +if TYPE_CHECKING: + from telegram import Bot, BusinessIntro, BusinessLocation, BusinessOpeningHours, Message + + +class ChatFullInfo(_ChatBase): + """ + This object contains full information about a chat. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`~telegram.Chat.id` is equal. + + .. versionadded:: 21.2 + + .. versionchanged:: 21.3 + Explicit support for all shortcut methods known from :class:`telegram.Chat` on this + object. Previously those were only available because this class inherited from + :class:`telegram.Chat`. + + Args: + id (:obj:`int`): Unique identifier for this chat. + type (:obj:`str`): Type of chat, can be either :attr:`PRIVATE`, :attr:`GROUP`, + :attr:`SUPERGROUP` or :attr:`CHANNEL`. + accent_color_id (:obj:`int`, optional): Identifier of the + :class:`accent color ` for the chat name and + backgrounds of the chat photo, reply header, and link preview. See `accent colors`_ + for more details. + + .. versionadded:: 20.8 + max_reaction_count (:obj:`int`): The maximum number of reactions that can be set on a + message in the chat. + + .. versionadded:: 21.2 + title (:obj:`str`, optional): Title, for supergroups, channels and group chats. + username (:obj:`str`, optional): Username, for private chats, supergroups and channels if + available. + first_name (:obj:`str`, optional): First name of the other party in a private chat. + last_name (:obj:`str`, optional): Last name of the other party in a private chat. + is_forum (:obj:`bool`, optional): :obj:`True`, if the supergroup chat is a forum + (has topics_ enabled). + + .. versionadded:: 20.0 + photo (:class:`telegram.ChatPhoto`, optional): Chat photo. + active_usernames (Sequence[:obj:`str`], optional): If set, the list of all `active chat + usernames `_; for private chats, supergroups and channels. + + .. versionadded:: 20.0 + birthdate (:class:`telegram.Birthdate`, optional): For private chats, + the date of birth of the user. + + .. versionadded:: 21.1 + business_intro (:class:`telegram.BusinessIntro`, optional): For private chats with + business accounts, the intro of the business. + + .. versionadded:: 21.1 + business_location (:class:`telegram.BusinessLocation`, optional): For private chats with + business accounts, the location of the business. + + .. versionadded:: 21.1 + business_opening_hours (:class:`telegram.BusinessOpeningHours`, optional): For private + chats with business accounts, the opening hours of the business. + + .. versionadded:: 21.1 + 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 + reactions allowed in the chat. If omitted, then all of + :const:`telegram.constants.ReactionEmoji` are allowed. + + .. versionadded:: 20.8 + background_custom_emoji_id (:obj:`str`, optional): Custom emoji identifier of emoji chosen + by the chat for the reply header and link preview background. + + .. versionadded:: 20.8 + profile_accent_color_id (:obj:`int`, optional): Identifier of the + :class:`accent color ` for the chat's profile + background. See profile `accent colors`_ for more details. + + .. versionadded:: 20.8 + profile_background_custom_emoji_id (:obj:`str`, optional): Custom emoji identifier of + the emoji chosen by the chat for its profile background. + + .. versionadded:: 20.8 + emoji_status_custom_emoji_id (:obj:`str`, optional): Custom emoji identifier of emoji + status of the chat or the other party in a private chat. + + .. versionadded:: 20.0 + emoji_status_expiration_date (:class:`datetime.datetime`, optional): Expiration date of + emoji status of the chat or the other party in a private chat, as a datetime object, + if any. + + |datetime_localization| + + .. versionadded:: 20.5 + bio (:obj:`str`, optional): Bio of the other party in a private chat. + has_private_forwards (:obj:`bool`, optional): :obj:`True`, if privacy settings of the other + party in the private chat allows to use ``tg://user?id=`` links only in chats + with the user. + + .. versionadded:: 13.9 + has_restricted_voice_and_video_messages (:obj:`bool`, optional): :obj:`True`, if the + privacy settings of the other party restrict sending voice and video note messages + in the private chat. + + .. versionadded:: 20.0 + join_to_send_messages (:obj:`bool`, optional): :obj:`True`, if users need to join the + supergroup before they can send messages. + + .. versionadded:: 20.0 + join_by_request (:obj:`bool`, optional): :obj:`True`, if all users directly joining the + supergroup without using an invite link need to be approved by supergroup + administrators. + + .. versionadded:: 20.0 + description (:obj:`str`, optional): Description, for groups, supergroups and channel chats. + invite_link (:obj:`str`, optional): Primary invite link, for groups, supergroups and + channel. + pinned_message (:class:`telegram.Message`, optional): The most recent pinned message + (by sending date). + permissions (:class:`telegram.ChatPermissions`): Optional. Default chat member permissions, + for groups and supergroups. + slow_mode_delay (:obj:`int`, optional): For supergroups, the minimum allowed delay between + consecutive messages sent by each unprivileged user. + unrestrict_boost_count (:obj:`int`, optional): For supergroups, the minimum number of + boosts that a non-administrator user needs to add in order to ignore slow mode and chat + permissions. + + .. versionadded:: 21.0 + message_auto_delete_time (:obj:`int`, optional): The time after which all messages sent to + the chat will be automatically deleted; in seconds. + + .. versionadded:: 13.4 + has_aggressive_anti_spam_enabled (:obj:`bool`, optional): :obj:`True`, if aggressive + anti-spam checks are enabled in the supergroup. The field is only available to chat + administrators. + + .. versionadded:: 20.0 + has_hidden_members (:obj:`bool`, optional): :obj:`True`, if non-administrators can only + get the list of bots and administrators in the chat. + + .. versionadded:: 20.0 + has_protected_content (:obj:`bool`, optional): :obj:`True`, if messages from the chat can't + be forwarded to other chats. + + .. versionadded:: 13.9 + has_visible_history (:obj:`bool`, optional): :obj:`True`, if new chat members will have + access to old messages; available only to chat administrators. + + .. versionadded:: 20.8 + sticker_set_name (:obj:`str`, optional): For supergroups, name of group sticker set. + can_set_sticker_set (:obj:`bool`, optional): :obj:`True`, if the bot can change group the + sticker set. + custom_emoji_sticker_set_name (:obj:`str`, optional): For supergroups, the name of the + group's custom emoji sticker set. Custom emoji from this set can be used by all users + and bots in the group. + + .. versionadded:: 21.0 + linked_chat_id (:obj:`int`, optional): Unique identifier for the linked chat, i.e. the + discussion group identifier for a channel and vice versa; for supergroups and channel + chats. + location (:class:`telegram.ChatLocation`, optional): For supergroups, the location to which + the supergroup is connected. + can_send_paid_media (:obj:`bool`, optional): :obj:`True`, if paid media messages can be + sent or forwarded to the channel chat. The field is available only for channel chats. + + .. versionadded:: 21.4 + + Attributes: + id (:obj:`int`): Unique identifier for this chat. + type (:obj:`str`): Type of chat, can be either :attr:`PRIVATE`, :attr:`GROUP`, + :attr:`SUPERGROUP` or :attr:`CHANNEL`. + accent_color_id (:obj:`int`): Optional. Identifier of the + :class:`accent color ` for the chat name and + backgrounds of the chat photo, reply header, and link preview. See `accent colors`_ + for more details. + + .. versionadded:: 20.8 + max_reaction_count (:obj:`int`): The maximum number of reactions that can be set on a + message in the chat. + + .. versionadded:: 21.2 + title (:obj:`str`, optional): Title, for supergroups, channels and group chats. + username (:obj:`str`, optional): Username, for private chats, supergroups and channels if + available. + first_name (:obj:`str`, optional): First name of the other party in a private chat. + last_name (:obj:`str`, optional): Last name of the other party in a private chat. + is_forum (:obj:`bool`, optional): :obj:`True`, if the supergroup chat is a forum + (has topics_ enabled). + + .. versionadded:: 20.0 + photo (:class:`telegram.ChatPhoto`): Optional. Chat photo. + active_usernames (Tuple[:obj:`str`]): Optional. If set, the list of all `active chat + usernames `_; for private chats, supergroups and channels. + + This list is empty if the chat has no active usernames or this chat instance was not + obtained via :meth:`~telegram.Bot.get_chat`. + + .. versionadded:: 20.0 + birthdate (:class:`telegram.Birthdate`): Optional. For private chats, + the date of birth of the user. + + .. versionadded:: 21.1 + business_intro (:class:`telegram.BusinessIntro`): Optional. For private chats with + business accounts, the intro of the business. + + .. versionadded:: 21.1 + business_location (:class:`telegram.BusinessLocation`): Optional. For private chats with + business accounts, the location of the business. + + .. versionadded:: 21.1 + business_opening_hours (:class:`telegram.BusinessOpeningHours`): Optional. For private + chats with business accounts, the opening hours of the business. + + .. versionadded:: 21.1 + 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 + reactions allowed in the chat. If omitted, then all of + :const:`telegram.constants.ReactionEmoji` are allowed. + + .. versionadded:: 20.8 + background_custom_emoji_id (:obj:`str`): Optional. Custom emoji identifier of emoji chosen + by the chat for the reply header and link preview background. + + .. versionadded:: 20.8 + profile_accent_color_id (:obj:`int`): Optional. Identifier of the + :class:`accent color ` for the chat's profile + background. See profile `accent colors`_ for more details. + + .. versionadded:: 20.8 + profile_background_custom_emoji_id (:obj:`str`): Optional. Custom emoji identifier of + the emoji chosen by the chat for its profile background. + + .. versionadded:: 20.8 + emoji_status_custom_emoji_id (:obj:`str`): Optional. Custom emoji identifier of emoji + status of the chat or the other party in a private chat. + + .. versionadded:: 20.0 + emoji_status_expiration_date (:class:`datetime.datetime`): Optional. Expiration date of + emoji status of the chat or the other party in a private chat, as a datetime object, + if any. + + |datetime_localization| + + .. versionadded:: 20.5 + bio (:obj:`str`): Optional. Bio of the other party in a private chat. + has_private_forwards (:obj:`bool`): Optional. :obj:`True`, if privacy settings of the other + party in the private chat allows to use ``tg://user?id=`` links only in chats + with the user. + + .. versionadded:: 13.9 + has_restricted_voice_and_video_messages (:obj:`bool`): Optional. :obj:`True`, if the + privacy settings of the other party restrict sending voice and video note messages + in the private chat. + + .. versionadded:: 20.0 + join_to_send_messages (:obj:`bool`): Optional. :obj:`True`, if users need to join + the supergroup before they can send messages. + + .. versionadded:: 20.0 + join_by_request (:obj:`bool`): Optional. :obj:`True`, if all users directly joining the + supergroup without using an invite link need to be approved by supergroup + administrators. + + .. versionadded:: 20.0 + description (:obj:`str`): Optional. Description, for groups, supergroups and channel chats. + invite_link (:obj:`str`): Optional. Primary invite link, for groups, supergroups and + channel. + pinned_message (:class:`telegram.Message`): Optional. The most recent pinned message + (by sending date). + permissions (:class:`telegram.ChatPermissions`): Optional. Default chat member permissions, + for groups and supergroups. + slow_mode_delay (:obj:`int`): Optional. For supergroups, the minimum allowed delay between + consecutive messages sent by each unprivileged user. + unrestrict_boost_count (:obj:`int`): Optional. For supergroups, the minimum number of + boosts that a non-administrator user needs to add in order to ignore slow mode and chat + permissions. + + .. versionadded:: 21.0 + message_auto_delete_time (:obj:`int`): Optional. The time after which all messages sent to + the chat will be automatically deleted; in seconds. + + .. versionadded:: 13.4 + has_aggressive_anti_spam_enabled (:obj:`bool`): Optional. :obj:`True`, if aggressive + anti-spam checks are enabled in the supergroup. The field is only available to chat + administrators. + + .. versionadded:: 20.0 + has_hidden_members (:obj:`bool`): Optional. :obj:`True`, if non-administrators can only + get the list of bots and administrators in the chat. + + .. versionadded:: 20.0 + has_protected_content (:obj:`bool`): Optional. :obj:`True`, if messages from the chat can't + be forwarded to other chats. + + .. versionadded:: 13.9 + has_visible_history (:obj:`bool`): Optional. :obj:`True`, if new chat members will have + access to old messages; available only to chat administrators. + + .. versionadded:: 20.8 + sticker_set_name (:obj:`str`): Optional. For supergroups, name of Group sticker set. + can_set_sticker_set (:obj:`bool`): Optional. :obj:`True`, if the bot can change group the + sticker set. + custom_emoji_sticker_set_name (:obj:`str`): Optional. For supergroups, the name of the + group's custom emoji sticker set. Custom emoji from this set can be used by all users + and bots in the group. + + .. versionadded:: 21.0 + linked_chat_id (:obj:`int`): Optional. Unique identifier for the linked chat, i.e. the + discussion group identifier for a channel and vice versa; for supergroups and channel + chats. + location (:class:`telegram.ChatLocation`): Optional. For supergroups, the location to which + the supergroup is connected. + can_send_paid_media (:obj:`bool`): Optional. :obj:`True`, if paid media messages can be + sent or forwarded to the channel chat. The field is available only for channel chats. + + .. versionadded:: 21.4 + + .. _accent colors: https://core.telegram.org/bots/api#accent-colors + .. _topics: https://telegram.org/blog/topics-in-groups-collectible-usernames#topics-in-groups + """ + + __slots__ = ( + "accent_color_id", + "active_usernames", + "available_reactions", + "background_custom_emoji_id", + "bio", + "birthdate", + "business_intro", + "business_location", + "business_opening_hours", + "can_send_paid_media", + "can_set_sticker_set", + "custom_emoji_sticker_set_name", + "description", + "emoji_status_custom_emoji_id", + "emoji_status_expiration_date", + "has_aggressive_anti_spam_enabled", + "has_hidden_members", + "has_private_forwards", + "has_protected_content", + "has_restricted_voice_and_video_messages", + "has_visible_history", + "invite_link", + "join_by_request", + "join_to_send_messages", + "linked_chat_id", + "location", + "max_reaction_count", + "message_auto_delete_time", + "permissions", + "personal_chat", + "photo", + "pinned_message", + "profile_accent_color_id", + "profile_background_custom_emoji_id", + "slow_mode_delay", + "sticker_set_name", + "unrestrict_boost_count", + ) + + def __init__( + self, + id: int, + type: str, + accent_color_id: int, + max_reaction_count: int, + title: Optional[str] = None, + username: Optional[str] = None, + first_name: Optional[str] = None, + last_name: Optional[str] = None, + is_forum: Optional[bool] = None, + photo: Optional[ChatPhoto] = None, + active_usernames: Optional[Sequence[str]] = None, + birthdate: Optional[Birthdate] = None, + business_intro: Optional["BusinessIntro"] = None, + business_location: Optional["BusinessLocation"] = None, + business_opening_hours: Optional["BusinessOpeningHours"] = None, + personal_chat: Optional["Chat"] = None, + available_reactions: Optional[Sequence[ReactionType]] = None, + background_custom_emoji_id: Optional[str] = None, + 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, + bio: Optional[str] = None, + has_private_forwards: Optional[bool] = None, + has_restricted_voice_and_video_messages: Optional[bool] = None, + join_to_send_messages: Optional[bool] = None, + join_by_request: Optional[bool] = None, + description: Optional[str] = None, + invite_link: Optional[str] = None, + pinned_message: Optional["Message"] = None, + permissions: Optional[ChatPermissions] = None, + slow_mode_delay: Optional[int] = None, + unrestrict_boost_count: Optional[int] = None, + message_auto_delete_time: Optional[int] = None, + has_aggressive_anti_spam_enabled: Optional[bool] = None, + has_hidden_members: Optional[bool] = None, + has_protected_content: Optional[bool] = None, + has_visible_history: Optional[bool] = None, + sticker_set_name: Optional[str] = None, + can_set_sticker_set: Optional[bool] = None, + custom_emoji_sticker_set_name: Optional[str] = None, + linked_chat_id: Optional[int] = None, + location: Optional[ChatLocation] = None, + can_send_paid_media: Optional[bool] = None, + *, + api_kwargs: Optional[JSONDict] = None, + ): + super().__init__( + id=id, + type=type, + title=title, + username=username, + first_name=first_name, + last_name=last_name, + is_forum=is_forum, + api_kwargs=api_kwargs, + ) + + # Required and unique to this class- + with self._unfrozen(): + self.max_reaction_count: int = max_reaction_count + self.photo: Optional[ChatPhoto] = photo + self.bio: Optional[str] = bio + self.has_private_forwards: Optional[bool] = has_private_forwards + self.description: Optional[str] = description + self.invite_link: Optional[str] = invite_link + self.pinned_message: Optional[Message] = pinned_message + self.permissions: Optional[ChatPermissions] = permissions + self.slow_mode_delay: Optional[int] = slow_mode_delay + self.message_auto_delete_time: Optional[int] = ( + int(message_auto_delete_time) if message_auto_delete_time is not None else None + ) + self.has_protected_content: Optional[bool] = has_protected_content + self.has_visible_history: Optional[bool] = has_visible_history + self.sticker_set_name: Optional[str] = sticker_set_name + self.can_set_sticker_set: Optional[bool] = can_set_sticker_set + self.linked_chat_id: Optional[int] = linked_chat_id + self.location: Optional[ChatLocation] = location + self.join_to_send_messages: Optional[bool] = join_to_send_messages + self.join_by_request: Optional[bool] = join_by_request + 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.emoji_status_custom_emoji_id: Optional[str] = emoji_status_custom_emoji_id + self.emoji_status_expiration_date: Optional[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( + available_reactions + ) + self.accent_color_id: Optional[int] = accent_color_id + self.background_custom_emoji_id: Optional[str] = background_custom_emoji_id + self.profile_accent_color_id: Optional[int] = profile_accent_color_id + self.profile_background_custom_emoji_id: Optional[str] = ( + profile_background_custom_emoji_id + ) + self.unrestrict_boost_count: Optional[int] = unrestrict_boost_count + self.custom_emoji_sticker_set_name: Optional[str] = custom_emoji_sticker_set_name + self.birthdate: Optional[Birthdate] = birthdate + self.personal_chat: Optional[Chat] = personal_chat + self.business_intro: Optional[BusinessIntro] = business_intro + 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 + + @classmethod + def de_json( + cls, data: Optional[JSONDict], bot: Optional["Bot"] = None + ) -> Optional["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) + + data["emoji_status_expiration_date"] = from_timestamp( + data.get("emoji_status_expiration_date"), tzinfo=loc_tzinfo + ) + + data["photo"] = ChatPhoto.de_json(data.get("photo"), bot) + + from telegram import ( # pylint: disable=import-outside-toplevel + BusinessIntro, + BusinessLocation, + BusinessOpeningHours, + 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 + ) + + return super().de_json(data=data, bot=bot) diff --git a/_chatinvitelink.py b/_chatinvitelink.py new file mode 100644 index 0000000000000000000000000000000000000000..b26de4e332b25fa40943dcbf411bccd3b66e919c --- /dev/null +++ b/_chatinvitelink.py @@ -0,0 +1,195 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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 an invite link for a chat.""" +import datetime +from typing import TYPE_CHECKING, Optional + +from telegram._telegramobject import TelegramObject +from telegram._user import User +from telegram._utils.datetime import extract_tzinfo_from_defaults, from_timestamp +from telegram._utils.types import JSONDict + +if TYPE_CHECKING: + from telegram import Bot + + +class ChatInviteLink(TelegramObject): + """This object represents an invite link for a chat. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`invite_link`, :attr:`creator`, :attr:`creates_join_request`, + :attr:`is_primary` and :attr:`is_revoked` are equal. + + .. versionadded:: 13.4 + .. versionchanged:: 20.0 + + * The argument & attribute :attr:`creates_join_request` is now required to comply with the + Bot API. + * Comparing objects of this class now also takes :attr:`creates_join_request` into account. + + Args: + invite_link (:obj:`str`): The invite link. + creator (:class:`telegram.User`): Creator of the link. + creates_join_request (:obj:`bool`): :obj:`True`, if users joining the chat via + the link need to be approved by chat administrators. + + .. versionadded:: 13.8 + is_primary (:obj:`bool`): :obj:`True`, if the link is primary. + is_revoked (:obj:`bool`): :obj:`True`, if the link is revoked. + expire_date (:class:`datetime.datetime`, optional): Date when the link will expire or + has been expired. + + .. versionchanged:: 20.3 + |datetime_localization| + 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`- + :tg-const:`telegram.constants.ChatInviteLinkLimit.MAX_MEMBER_LIMIT`. + name (:obj:`str`, optional): Invite link name. + 0-:tg-const:`telegram.constants.ChatInviteLinkLimit.NAME_LENGTH` characters. + + .. versionadded:: 13.8 + pending_join_request_count (:obj:`int`, optional): Number of pending join requests + 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 ``'…'``. + creator (:class:`telegram.User`): Creator of the link. + creates_join_request (:obj:`bool`): :obj:`True`, if users joining the chat via + the link need to be approved by chat administrators. + + .. versionadded:: 13.8 + is_primary (:obj:`bool`): :obj:`True`, if the link is primary. + is_revoked (:obj:`bool`): :obj:`True`, if the link is revoked. + expire_date (:class:`datetime.datetime`): Optional. Date when the link will expire or + has been expired. + + .. versionchanged:: 20.3 + |datetime_localization| + 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`- + :tg-const:`telegram.constants.ChatInviteLinkLimit.MAX_MEMBER_LIMIT`. + name (:obj:`str`): Optional. Invite link name. + 0-:tg-const:`telegram.constants.ChatInviteLinkLimit.NAME_LENGTH` characters. + + .. versionadded:: 13.8 + pending_join_request_count (:obj:`int`): Optional. Number of pending join requests + 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 + + """ + + __slots__ = ( + "creates_join_request", + "creator", + "expire_date", + "invite_link", + "is_primary", + "is_revoked", + "member_limit", + "name", + "pending_join_request_count", + "subscription_period", + "subscription_price", + ) + + def __init__( + self, + invite_link: str, + creator: User, + creates_join_request: bool, + is_primary: bool, + is_revoked: bool, + expire_date: Optional[datetime.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, + ): + super().__init__(api_kwargs=api_kwargs) + # Required + self.invite_link: str = invite_link + self.creator: User = creator + self.creates_join_request: bool = creates_join_request + self.is_primary: bool = is_primary + self.is_revoked: bool = is_revoked + + # Optionals + self.expire_date: Optional[datetime.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, + self.creator, + self.is_primary, + self.is_revoked, + ) + + self._freeze() + + @classmethod + def de_json( + cls, data: Optional[JSONDict], bot: Optional["Bot"] = None + ) -> Optional["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["expire_date"] = from_timestamp(data.get("expire_date", None), tzinfo=loc_tzinfo) + + return super().de_json(data=data, bot=bot) diff --git a/_chatjoinrequest.py b/_chatjoinrequest.py new file mode 100644 index 0000000000000000000000000000000000000000..9c444d97b4d7b843cb328cf3159acab81c5732ba --- /dev/null +++ b/_chatjoinrequest.py @@ -0,0 +1,213 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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 ChatJoinRequest.""" +import datetime +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.datetime import extract_tzinfo_from_defaults, from_timestamp +from telegram._utils.defaultvalue import DEFAULT_NONE +from telegram._utils.types import JSONDict, ODVInput + +if TYPE_CHECKING: + from telegram import Bot + + +class ChatJoinRequest(TelegramObject): + """This object represents a join request sent to a chat. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`chat`, :attr:`from_user` and :attr:`date` are equal. + + Note: + * Since Bot API 5.5, bots are allowed to contact users who sent a join request to a chat + where the bot is an administrator with the + :attr:`~telegram.ChatMemberAdministrator.can_invite_users` administrator right - even + if the user never interacted with the bot before. + * Telegram does not guarantee that :attr:`from_user.id ` coincides with the + ``chat_id`` of the user. Please use :attr:`user_chat_id` to contact the user in + response to their join request. + + .. versionadded:: 13.8 + .. versionchanged:: 20.1 + In Bot API 6.5 the argument :paramref:`user_chat_id` was added, which changes the position + of the optional arguments :paramref:`bio` and :paramref:`invite_link`. + + Args: + chat (:class:`telegram.Chat`): Chat to which the request was sent. + from_user (:class:`telegram.User`): User that sent the join request. + date (:class:`datetime.datetime`): Date the request was sent. + + .. versionchanged:: 20.3 + |datetime_localization| + user_chat_id (:obj:`int`): Identifier of a private chat with the user who sent the join + request. This number may have more than 32 significant bits and some programming + languages may have difficulty/silent defects in interpreting it. But it has at most 52 + significant bits, so a 64-bit integer or double-precision float type are safe for + storing this identifier. The bot can use this identifier for 5 minutes to send messages + until the join request is processed, assuming no other administrator contacted the + user. + + .. versionadded:: 20.1 + bio (:obj:`str`, optional): Bio of the user. + invite_link (:class:`telegram.ChatInviteLink`, optional): Chat invite link that was used + by the user to send the join request. + + Attributes: + chat (:class:`telegram.Chat`): Chat to which the request was sent. + from_user (:class:`telegram.User`): User that sent the join request. + date (:class:`datetime.datetime`): Date the request was sent. + + .. versionchanged:: 20.3 + |datetime_localization| + user_chat_id (:obj:`int`): Identifier of a private chat with the user who sent the join + request. This number may have more than 32 significant bits and some programming + languages may have difficulty/silent defects in interpreting it. But it has at most 52 + significant bits, so a 64-bit integer or double-precision float type are safe for + storing this identifier. The bot can use this identifier for 24 hours to send messages + until the join request is processed, assuming no other administrator contacted the + user. + + .. versionadded:: 20.1 + bio (:obj:`str`): Optional. Bio of the user. + invite_link (:class:`telegram.ChatInviteLink`): Optional. Chat invite link that was used + by the user to send the join request. + + Note: + When a user joins a *public* group via an invite link, this attribute may not + be present. However, this behavior is undocument and may be subject to change. + See `this GitHub thread `_ + for some discussion. + + """ + + __slots__ = ("bio", "chat", "date", "from_user", "invite_link", "user_chat_id") + + def __init__( + self, + chat: Chat, + from_user: User, + date: datetime.datetime, + user_chat_id: int, + bio: Optional[str] = None, + invite_link: Optional[ChatInviteLink] = None, + *, + api_kwargs: Optional[JSONDict] = None, + ): + super().__init__(api_kwargs=api_kwargs) + # Required + self.chat: Chat = chat + self.from_user: User = from_user + self.date: datetime.datetime = date + self.user_chat_id: int = user_chat_id + + # Optionals + self.bio: Optional[str] = bio + self.invite_link: Optional[ChatInviteLink] = invite_link + + self._id_attrs = (self.chat, self.from_user, self.date) + + self._freeze() + + @classmethod + def de_json( + cls, data: Optional[JSONDict], bot: Optional["Bot"] = None + ) -> Optional["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["date"] = from_timestamp(data.get("date", None), tzinfo=loc_tzinfo) + data["invite_link"] = ChatInviteLink.de_json(data.get("invite_link"), bot) + + return super().de_json(data=data, bot=bot) + + async def approve( + 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.approve_chat_join_request( + chat_id=update.effective_chat.id, user_id=update.effective_user.id, *args, **kwargs + ) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.approve_chat_join_request`. + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + """ + return await self.get_bot().approve_chat_join_request( + chat_id=self.chat.id, + user_id=self.from_user.id, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def decline( + 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.decline_chat_join_request( + chat_id=update.effective_chat.id, user_id=update.effective_user.id, *args, **kwargs + ) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.decline_chat_join_request`. + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + """ + return await self.get_bot().decline_chat_join_request( + chat_id=self.chat.id, + user_id=self.from_user.id, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) diff --git a/_chatlocation.py b/_chatlocation.py new file mode 100644 index 0000000000000000000000000000000000000000..04f9854a23aee0269bd482d028ab8825a359e5e6 --- /dev/null +++ b/_chatlocation.py @@ -0,0 +1,93 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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 location to which a chat is connected.""" + +from typing import TYPE_CHECKING, Final, Optional + +from telegram import constants +from telegram._files.location import Location +from telegram._telegramobject import TelegramObject +from telegram._utils.types import JSONDict + +if TYPE_CHECKING: + from telegram import Bot + + +class ChatLocation(TelegramObject): + """This object represents a location to which a chat is connected. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`location` is equal. + + Args: + location (:class:`telegram.Location`): The location to which the supergroup is connected. + Can't be a live location. + address (:obj:`str`): Location address; + :tg-const:`telegram.ChatLocation.MIN_ADDRESS`- + :tg-const:`telegram.ChatLocation.MAX_ADDRESS` characters, as defined by the chat owner. + Attributes: + location (:class:`telegram.Location`): The location to which the supergroup is connected. + Can't be a live location. + address (:obj:`str`): Location address; + :tg-const:`telegram.ChatLocation.MIN_ADDRESS`- + :tg-const:`telegram.ChatLocation.MAX_ADDRESS` characters, as defined by the chat owner. + + """ + + __slots__ = ("address", "location") + + def __init__( + self, + location: Location, + address: str, + *, + api_kwargs: Optional[JSONDict] = None, + ): + super().__init__(api_kwargs=api_kwargs) + self.location: Location = location + self.address: str = address + + self._id_attrs = (self.location,) + + self._freeze() + + @classmethod + def de_json( + cls, data: Optional[JSONDict], bot: Optional["Bot"] = None + ) -> Optional["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) + + return super().de_json(data=data, bot=bot) + + MIN_ADDRESS: Final[int] = constants.LocationLimit.MIN_CHAT_LOCATION_ADDRESS + """:const:`telegram.constants.LocationLimit.MIN_CHAT_LOCATION_ADDRESS` + + .. versionadded:: 20.0 + """ + MAX_ADDRESS: Final[int] = constants.LocationLimit.MAX_CHAT_LOCATION_ADDRESS + """:const:`telegram.constants.LocationLimit.MAX_CHAT_LOCATION_ADDRESS` + + .. versionadded:: 20.0 + """ diff --git a/_chatmember.py b/_chatmember.py new file mode 100644 index 0000000000000000000000000000000000000000..da84516b16563960b7b8ba995eca3737fd38cc4f --- /dev/null +++ b/_chatmember.py @@ -0,0 +1,665 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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 ChatMember.""" + +import datetime +from typing import TYPE_CHECKING, Dict, Final, Optional, Type + +from telegram import constants +from telegram._telegramobject import TelegramObject +from telegram._user import User +from telegram._utils.datetime import extract_tzinfo_from_defaults, from_timestamp +from telegram._utils.types import JSONDict + +if TYPE_CHECKING: + from telegram import Bot + + +class ChatMember(TelegramObject): + """Base class for Telegram ChatMember Objects. + Currently, the following 6 types of chat members are supported: + + * :class:`telegram.ChatMemberOwner` + * :class:`telegram.ChatMemberAdministrator` + * :class:`telegram.ChatMemberMember` + * :class:`telegram.ChatMemberRestricted` + * :class:`telegram.ChatMemberLeft` + * :class:`telegram.ChatMemberBanned` + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`user` and :attr:`status` are equal. + + Examples: + :any:`Chat Member Bot ` + + .. versionchanged:: 20.0 + + * As of Bot API 5.3, :class:`ChatMember` is nothing but the base class for the subclasses + listed above and is no longer returned directly by :meth:`~telegram.Bot.get_chat`. + Therefore, most of the arguments and attributes were removed and you should no longer + use :class:`ChatMember` directly. + * The constant ``ChatMember.CREATOR`` was replaced by :attr:`~telegram.ChatMember.OWNER` + * The constant ``ChatMember.KICKED`` was replaced by :attr:`~telegram.ChatMember.BANNED` + + Args: + user (:class:`telegram.User`): Information about the user. + status (:obj:`str`): The member's status in the chat. Can be + :attr:`~telegram.ChatMember.ADMINISTRATOR`, :attr:`~telegram.ChatMember.OWNER`, + :attr:`~telegram.ChatMember.BANNED`, :attr:`~telegram.ChatMember.LEFT`, + :attr:`~telegram.ChatMember.MEMBER` or :attr:`~telegram.ChatMember.RESTRICTED`. + + Attributes: + user (:class:`telegram.User`): Information about the user. + status (:obj:`str`): The member's status in the chat. Can be + :attr:`~telegram.ChatMember.ADMINISTRATOR`, :attr:`~telegram.ChatMember.OWNER`, + :attr:`~telegram.ChatMember.BANNED`, :attr:`~telegram.ChatMember.LEFT`, + :attr:`~telegram.ChatMember.MEMBER` or :attr:`~telegram.ChatMember.RESTRICTED`. + + """ + + __slots__ = ("status", "user") + + ADMINISTRATOR: Final[str] = constants.ChatMemberStatus.ADMINISTRATOR + """:const:`telegram.constants.ChatMemberStatus.ADMINISTRATOR`""" + OWNER: Final[str] = constants.ChatMemberStatus.OWNER + """:const:`telegram.constants.ChatMemberStatus.OWNER`""" + BANNED: Final[str] = constants.ChatMemberStatus.BANNED + """:const:`telegram.constants.ChatMemberStatus.BANNED`""" + LEFT: Final[str] = constants.ChatMemberStatus.LEFT + """:const:`telegram.constants.ChatMemberStatus.LEFT`""" + MEMBER: Final[str] = constants.ChatMemberStatus.MEMBER + """:const:`telegram.constants.ChatMemberStatus.MEMBER`""" + RESTRICTED: Final[str] = constants.ChatMemberStatus.RESTRICTED + """:const:`telegram.constants.ChatMemberStatus.RESTRICTED`""" + + def __init__( + self, + user: User, + status: str, + *, + api_kwargs: Optional[JSONDict] = None, + ): + super().__init__(api_kwargs=api_kwargs) + # Required by all subclasses + self.user: User = user + self.status: str = status + + self._id_attrs = (self.user, self.status) + + self._freeze() + + @classmethod + def de_json( + cls, data: Optional[JSONDict], bot: Optional["Bot"] = None + ) -> Optional["ChatMember"]: + """See :meth:`telegram.TelegramObject.de_json`.""" + data = cls._parse_data(data) + + if not data: + return None + + _class_mapping: Dict[str, Type[ChatMember]] = { + cls.OWNER: ChatMemberOwner, + cls.ADMINISTRATOR: ChatMemberAdministrator, + cls.MEMBER: ChatMemberMember, + cls.RESTRICTED: ChatMemberRestricted, + cls.LEFT: ChatMemberLeft, + cls.BANNED: ChatMemberBanned, + } + + 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) + 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) + + # This is a deprecated field that TG still returns for backwards compatibility + # Let's filter it out to speed up the de-json process + if cls is ChatMemberRestricted and data.get("can_send_media_messages") is not None: + api_kwargs = {"can_send_media_messages": data.pop("can_send_media_messages")} + return super()._de_json(data=data, bot=bot, api_kwargs=api_kwargs) + + return super().de_json(data=data, bot=bot) + + +class ChatMemberOwner(ChatMember): + """ + Represents a chat member that owns the chat + and has all administrator privileges. + + .. versionadded:: 13.7 + + Args: + user (:class:`telegram.User`): Information about the user. + is_anonymous (:obj:`bool`): :obj:`True`, if the + user's presence in the chat is hidden. + custom_title (:obj:`str`, optional): Custom title for this user. + + Attributes: + status (:obj:`str`): The member's status in the chat, + always :tg-const:`telegram.ChatMember.OWNER`. + user (:class:`telegram.User`): Information about the user. + is_anonymous (:obj:`bool`): :obj:`True`, if the user's + presence in the chat is hidden. + custom_title (:obj:`str`): Optional. Custom title for + this user. + """ + + __slots__ = ("custom_title", "is_anonymous") + + def __init__( + self, + user: User, + is_anonymous: bool, + custom_title: Optional[str] = None, + *, + api_kwargs: Optional[JSONDict] = None, + ): + super().__init__(status=ChatMember.OWNER, user=user, api_kwargs=api_kwargs) + with self._unfrozen(): + self.is_anonymous: bool = is_anonymous + self.custom_title: Optional[str] = custom_title + + +class ChatMemberAdministrator(ChatMember): + """ + Represents a chat member that has some additional privileges. + + .. versionadded:: 13.7 + .. versionchanged:: 20.0 + + * Argument and attribute ``can_manage_voice_chats`` were renamed to + :paramref:`can_manage_video_chats` and :attr:`can_manage_video_chats` in accordance to + Bot API 6.0. + * The argument :paramref:`can_manage_topics` was added, which changes the position of the + optional argument :paramref:`custom_title`. + + .. versionchanged:: 21.1 + As of this version, :attr:`can_post_stories`, :attr:`can_edit_stories`, + and :attr:`can_delete_stories` is now required. Thus, the order of arguments had to be + changed. + + Args: + user (:class:`telegram.User`): Information about the user. + can_be_edited (:obj:`bool`): :obj:`True`, if the bot + is allowed to edit administrator privileges of that user. + is_anonymous (:obj:`bool`): :obj:`True`, if the user's + presence in the chat is hidden. + can_manage_chat (:obj:`bool`): :obj:`True`, if the administrator can access the chat event + log, get boost list, see hidden supergroup and channel members, report spam messages + and ignore slow mode. Implied by any other administrator privilege. + can_delete_messages (:obj:`bool`): :obj:`True`, if the + administrator can delete messages of other users. + can_manage_video_chats (:obj:`bool`): :obj:`True`, if the + administrator can manage video chats. + + .. versionadded:: 20.0 + can_restrict_members (:obj:`bool`): :obj:`True`, if the + administrator can restrict, ban or unban chat members. + can_promote_members (:obj:`bool`): :obj:`True`, if the administrator + can add new administrators with a subset of his own privileges or demote + administrators that he has promoted, directly or indirectly (promoted by + administrators that were appointed by the user). + can_change_info (:obj:`bool`): :obj:`True`, if the user can change + the chat title, photo and other settings. + can_invite_users (:obj:`bool`): :obj:`True`, if the user can invite + new users to the chat. + can_post_messages (:obj:`bool`, optional): :obj:`True`, if the + administrator can post messages in the channel, or access channel statistics; + for channels only. + can_edit_messages (:obj:`bool`, optional): :obj:`True`, if the + administrator can edit messages of other users and can pin + messages; for channels only. + can_pin_messages (:obj:`bool`, optional): :obj:`True`, if the user is allowed + to pin messages; for groups and supergroups only. + can_post_stories (:obj:`bool`): :obj:`True`, if the administrator can post + stories to the chat. + + .. versionadded:: 20.6 + .. versionchanged:: 21.0 + |non_optional_story_argument| + can_edit_stories (:obj:`bool`): :obj:`True`, if the administrator can edit stories posted + by other users, post stories to the chat page, pin chat stories, and access the chat's + story archive + + .. versionadded:: 20.6 + .. versionchanged:: 21.0 + |non_optional_story_argument| + can_delete_stories (:obj:`bool`): :obj:`True`, if the administrator can delete + stories posted by other users. + + .. versionadded:: 20.6 + .. versionchanged:: 21.0 + |non_optional_story_argument| + can_manage_topics (:obj:`bool`, optional): :obj:`True`, if the user is allowed + to create, rename, close, and reopen forum topics; for supergroups only. + + .. versionadded:: 20.0 + custom_title (:obj:`str`, optional): Custom title for this user. + + Attributes: + status (:obj:`str`): The member's status in the chat, + always :tg-const:`telegram.ChatMember.ADMINISTRATOR`. + user (:class:`telegram.User`): Information about the user. + can_be_edited (:obj:`bool`): :obj:`True`, if the bot + is allowed to edit administrator privileges of that user. + is_anonymous (:obj:`bool`): :obj:`True`, if the user's + presence in the chat is hidden. + can_manage_chat (:obj:`bool`): :obj:`True`, if the administrator can access the chat event + log, get boost list, see hidden supergroup and channel members, report spam messages + and ignore slow mode. Implied by any other administrator privilege. + can_delete_messages (:obj:`bool`): :obj:`True`, if the + administrator can delete messages of other users. + can_manage_video_chats (:obj:`bool`): :obj:`True`, if the + administrator can manage video chats. + + .. versionadded:: 20.0 + can_restrict_members (:obj:`bool`): :obj:`True`, if the + administrator can restrict, ban or unban chat members, or access supergroup statistics. + can_promote_members (:obj:`bool`): :obj:`True`, if the administrator can add new + administrators with a subset of their own privileges or demote administrators + that they have promoted, directly or indirectly (promoted by administrators that + were appointed by the user). + can_change_info (:obj:`bool`): :obj:`True`, if the user can change + the chat title, photo and other settings. + can_invite_users (:obj:`bool`): :obj:`True`, if the user can invite + new users to the chat. + can_post_messages (:obj:`bool`): Optional. :obj:`True`, if the + administrator can post messages in the channel or access channel statistics; + for channels only. + can_edit_messages (:obj:`bool`): Optional. :obj:`True`, if the + administrator can edit messages of other users and can pin + messages; for channels only. + can_pin_messages (:obj:`bool`): Optional. :obj:`True`, if the user is allowed + to pin messages; for groups and supergroups only. + can_post_stories (:obj:`bool`): :obj:`True`, if the administrator can post + stories to the chat. + + .. versionadded:: 20.6 + .. versionchanged:: 21.0 + |non_optional_story_argument| + can_edit_stories (:obj:`bool`): :obj:`True`, if the administrator can edit stories posted + by other users, post stories to the chat page, pin chat stories, and access the chat's + story archive + + .. versionadded:: 20.6 + .. versionchanged:: 21.0 + |non_optional_story_argument| + can_delete_stories (:obj:`bool`): :obj:`True`, if the administrator can delete + stories posted by other users. + + .. versionadded:: 20.6 + .. versionchanged:: 21.0 + |non_optional_story_argument| + can_manage_topics (:obj:`bool`): Optional. :obj:`True`, if the user is allowed + to create, rename, close, and reopen forum topics; for supergroups only + + .. versionadded:: 20.0 + custom_title (:obj:`str`): Optional. Custom title for this user. + """ + + __slots__ = ( + "can_be_edited", + "can_change_info", + "can_delete_messages", + "can_delete_stories", + "can_edit_messages", + "can_edit_stories", + "can_invite_users", + "can_manage_chat", + "can_manage_topics", + "can_manage_video_chats", + "can_pin_messages", + "can_post_messages", + "can_post_stories", + "can_promote_members", + "can_restrict_members", + "custom_title", + "is_anonymous", + ) + + def __init__( + self, + user: User, + can_be_edited: bool, + is_anonymous: bool, + can_manage_chat: bool, + can_delete_messages: bool, + can_manage_video_chats: bool, + can_restrict_members: bool, + can_promote_members: bool, + can_change_info: bool, + can_invite_users: bool, + can_post_stories: bool, + can_edit_stories: bool, + can_delete_stories: bool, + can_post_messages: Optional[bool] = None, + can_edit_messages: Optional[bool] = None, + can_pin_messages: Optional[bool] = None, + can_manage_topics: Optional[bool] = None, + custom_title: Optional[str] = None, + *, + api_kwargs: Optional[JSONDict] = None, + ): + super().__init__(status=ChatMember.ADMINISTRATOR, user=user, api_kwargs=api_kwargs) + with self._unfrozen(): + self.can_be_edited: bool = can_be_edited + self.is_anonymous: bool = is_anonymous + self.can_manage_chat: bool = can_manage_chat + self.can_delete_messages: bool = can_delete_messages + self.can_manage_video_chats: bool = can_manage_video_chats + self.can_restrict_members: bool = can_restrict_members + self.can_promote_members: bool = can_promote_members + self.can_change_info: bool = can_change_info + self.can_invite_users: bool = can_invite_users + self.can_post_stories: bool = can_post_stories + self.can_edit_stories: bool = can_edit_stories + self.can_delete_stories: bool = can_delete_stories + # Optionals + self.can_post_messages: Optional[bool] = can_post_messages + self.can_edit_messages: Optional[bool] = can_edit_messages + self.can_pin_messages: Optional[bool] = can_pin_messages + self.can_manage_topics: Optional[bool] = can_manage_topics + self.custom_title: Optional[str] = custom_title + + +class ChatMemberMember(ChatMember): + """ + Represents a chat member that has no additional + privileges or restrictions. + + .. versionadded:: 13.7 + + Args: + user (:class:`telegram.User`): Information about the user. + 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__ = ("until_date",) + + def __init__( + self, + user: User, + until_date: Optional[datetime.datetime] = None, + *, + api_kwargs: Optional[JSONDict] = None, + ): + super().__init__(status=ChatMember.MEMBER, user=user, api_kwargs=api_kwargs) + with self._unfrozen(): + self.until_date: Optional[datetime.datetime] = until_date + + +class ChatMemberRestricted(ChatMember): + """ + Represents a chat member that is under certain restrictions + in the chat. Supergroups only. + + .. versionadded:: 13.7 + .. versionchanged:: 20.0 + All arguments were made positional and their order was changed. + The argument can_manage_topics was added. + + .. versionchanged:: 20.5 + Removed deprecated argument and attribute ``can_send_media_messages``. + + Args: + user (:class:`telegram.User`): Information about the user. + is_member (:obj:`bool`): :obj:`True`, if the user is a + member of the chat at the moment of the request. + can_change_info (:obj:`bool`): :obj:`True`, if the user can change + the chat title, photo and other settings. + can_invite_users (:obj:`bool`): :obj:`True`, if the user can invite + new users to the chat. + can_pin_messages (:obj:`bool`): :obj:`True`, if the user is allowed + to pin messages; groups and supergroups only. + can_send_messages (:obj:`bool`): :obj:`True`, if the user is allowed + to send text messages, contacts, invoices, locations and venues. + can_send_polls (:obj:`bool`): :obj:`True`, if the user is allowed + to send polls. + can_send_other_messages (:obj:`bool`): :obj:`True`, if the user is allowed + to send animations, games, stickers and use inline bots. + can_add_web_page_previews (:obj:`bool`): :obj:`True`, if the user is + allowed to add web page previews to their messages. + can_manage_topics (:obj:`bool`): :obj:`True`, if the user is allowed to create + forum topics. + + .. versionadded:: 20.0 + until_date (:class:`datetime.datetime`): Date when restrictions + will be lifted for this user. + + .. versionchanged:: 20.3 + |datetime_localization| + can_send_audios (:obj:`bool`): :obj:`True`, if the user is allowed to send audios. + + .. versionadded:: 20.1 + can_send_documents (:obj:`bool`): :obj:`True`, if the user is allowed to send documents. + + .. versionadded:: 20.1 + can_send_photos (:obj:`bool`): :obj:`True`, if the user is allowed to send photos. + + .. versionadded:: 20.1 + can_send_videos (:obj:`bool`): :obj:`True`, if the user is allowed to send videos. + + .. versionadded:: 20.1 + can_send_video_notes (:obj:`bool`): :obj:`True`, if the user is allowed to send video + notes. + + .. versionadded:: 20.1 + can_send_voice_notes (:obj:`bool`): :obj:`True`, if the user is allowed to send voice + notes. + + .. versionadded:: 20.1 + + Attributes: + status (:obj:`str`): The member's status in the chat, + always :tg-const:`telegram.ChatMember.RESTRICTED`. + user (:class:`telegram.User`): Information about the user. + is_member (:obj:`bool`): :obj:`True`, if the user is a + member of the chat at the moment of the request. + can_change_info (:obj:`bool`): :obj:`True`, if the user can change + the chat title, photo and other settings. + can_invite_users (:obj:`bool`): :obj:`True`, if the user can invite + new users to the chat. + can_pin_messages (:obj:`bool`): :obj:`True`, if the user is allowed + to pin messages; groups and supergroups only. + can_send_messages (:obj:`bool`): :obj:`True`, if the user is allowed + to send text messages, contacts, locations and venues. + can_send_polls (:obj:`bool`): :obj:`True`, if the user is allowed + to send polls. + can_send_other_messages (:obj:`bool`): :obj:`True`, if the user is allowed + to send animations, games, stickers and use inline bots. + can_add_web_page_previews (:obj:`bool`): :obj:`True`, if the user is + allowed to add web page previews to their messages. + can_manage_topics (:obj:`bool`): :obj:`True`, if the user is allowed to create + forum topics. + + .. versionadded:: 20.0 + until_date (:class:`datetime.datetime`): Date when restrictions + will be lifted for this user. + + .. versionchanged:: 20.3 + |datetime_localization| + can_send_audios (:obj:`bool`): :obj:`True`, if the user is allowed to send audios. + + .. versionadded:: 20.1 + can_send_documents (:obj:`bool`): :obj:`True`, if the user is allowed to send documents. + + .. versionadded:: 20.1 + can_send_photos (:obj:`bool`): :obj:`True`, if the user is allowed to send photos. + + .. versionadded:: 20.1 + can_send_videos (:obj:`bool`): :obj:`True`, if the user is allowed to send videos. + + .. versionadded:: 20.1 + can_send_video_notes (:obj:`bool`): :obj:`True`, if the user is allowed to send video + notes. + + .. versionadded:: 20.1 + can_send_voice_notes (:obj:`bool`): :obj:`True`, if the user is allowed to send voice + notes. + + .. versionadded:: 20.1 + + """ + + __slots__ = ( + "can_add_web_page_previews", + "can_change_info", + "can_invite_users", + "can_manage_topics", + "can_pin_messages", + "can_send_audios", + "can_send_documents", + "can_send_messages", + "can_send_other_messages", + "can_send_photos", + "can_send_polls", + "can_send_video_notes", + "can_send_videos", + "can_send_voice_notes", + "is_member", + "until_date", + ) + + def __init__( + self, + user: User, + is_member: bool, + can_change_info: bool, + can_invite_users: bool, + can_pin_messages: bool, + can_send_messages: bool, + can_send_polls: bool, + can_send_other_messages: bool, + can_add_web_page_previews: bool, + can_manage_topics: bool, + until_date: datetime.datetime, + can_send_audios: bool, + can_send_documents: bool, + can_send_photos: bool, + can_send_videos: bool, + can_send_video_notes: bool, + can_send_voice_notes: bool, + *, + api_kwargs: Optional[JSONDict] = None, + ): + super().__init__(status=ChatMember.RESTRICTED, user=user, api_kwargs=api_kwargs) + with self._unfrozen(): + self.is_member: bool = is_member + self.can_change_info: bool = can_change_info + self.can_invite_users: bool = can_invite_users + self.can_pin_messages: bool = can_pin_messages + self.can_send_messages: bool = can_send_messages + self.can_send_polls: bool = can_send_polls + 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.can_send_audios: bool = can_send_audios + self.can_send_documents: bool = can_send_documents + self.can_send_photos: bool = can_send_photos + self.can_send_videos: bool = can_send_videos + self.can_send_video_notes: bool = can_send_video_notes + self.can_send_voice_notes: bool = can_send_voice_notes + + +class ChatMemberLeft(ChatMember): + """ + Represents a chat member that isn't currently a member of the chat, + but may join it themselves. + + .. versionadded:: 13.7 + + Args: + user (:class:`telegram.User`): Information about the user. + + Attributes: + status (:obj:`str`): The member's status in the chat, + always :tg-const:`telegram.ChatMember.LEFT`. + user (:class:`telegram.User`): Information about the user. + """ + + __slots__ = () + + def __init__( + self, + user: User, + *, + api_kwargs: Optional[JSONDict] = None, + ): + super().__init__(status=ChatMember.LEFT, user=user, api_kwargs=api_kwargs) + self._freeze() + + +class ChatMemberBanned(ChatMember): + """ + Represents a chat member that was banned in the chat and + can't return to the chat or view chat messages. + + .. versionadded:: 13.7 + + Args: + user (:class:`telegram.User`): Information about the user. + until_date (:class:`datetime.datetime`): Date when restrictions + will be lifted for this user. + + .. versionchanged:: 20.3 + |datetime_localization| + + Attributes: + status (:obj:`str`): The member's status in the chat, + always :tg-const:`telegram.ChatMember.BANNED`. + user (:class:`telegram.User`): Information about the user. + until_date (:class:`datetime.datetime`): Date when restrictions + will be lifted for this user. + + .. versionchanged:: 20.3 + |datetime_localization| + + """ + + __slots__ = ("until_date",) + + def __init__( + self, + user: User, + until_date: datetime.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 diff --git a/_chatmemberupdated.py b/_chatmemberupdated.py new file mode 100644 index 0000000000000000000000000000000000000000..1aacb2185338abba37c497c7de4ecd3d350fdf3b --- /dev/null +++ b/_chatmemberupdated.py @@ -0,0 +1,219 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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 ChatMemberUpdated.""" +import datetime +from typing import TYPE_CHECKING, Dict, Optional, Tuple, 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.datetime import extract_tzinfo_from_defaults, from_timestamp +from telegram._utils.types import JSONDict + +if TYPE_CHECKING: + from telegram import Bot + + +class ChatMemberUpdated(TelegramObject): + """This object represents changes in the status of a chat member. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`chat`, :attr:`from_user`, :attr:`date`, + :attr:`old_chat_member` and :attr:`new_chat_member` are equal. + + .. versionadded:: 13.4 + + Note: + In Python :keyword:`from` is a reserved word. Use :paramref:`from_user` instead. + + Examples: + :any:`Chat Member Bot ` + + Args: + chat (:class:`telegram.Chat`): Chat the user belongs to. + from_user (:class:`telegram.User`): Performer of the action, which resulted in the change. + date (:class:`datetime.datetime`): Date the change was done in Unix time. Converted to + :class:`datetime.datetime`. + + .. versionchanged:: 20.3 + |datetime_localization| + old_chat_member (:class:`telegram.ChatMember`): Previous information about the chat member. + new_chat_member (:class:`telegram.ChatMember`): New information about the chat member. + invite_link (:class:`telegram.ChatInviteLink`, optional): Chat invite link, which was used + by the user to join the chat. For joining by invite link events only. + via_chat_folder_invite_link (:obj:`bool`, optional): :obj:`True`, if the user joined the + chat via a chat folder invite link + + .. versionadded:: 20.3 + via_join_request (:obj:`bool`, optional): :obj:`True`, if the user joined the chat after + sending a direct join request without using an invite link and being approved by + an administrator + + .. versionadded:: 21.2 + + Attributes: + chat (:class:`telegram.Chat`): Chat the user belongs to. + from_user (:class:`telegram.User`): Performer of the action, which resulted in the change. + date (:class:`datetime.datetime`): Date the change was done in Unix time. Converted to + :class:`datetime.datetime`. + + .. versionchanged:: 20.3 + |datetime_localization| + old_chat_member (:class:`telegram.ChatMember`): Previous information about the chat member. + new_chat_member (:class:`telegram.ChatMember`): New information about the chat member. + invite_link (:class:`telegram.ChatInviteLink`): Optional. Chat invite link, which was used + by the user to join the chat. For joining by invite link events only. + via_chat_folder_invite_link (:obj:`bool`): Optional. :obj:`True`, if the user joined the + chat via a chat folder invite link + + .. versionadded:: 20.3 + via_join_request (:obj:`bool`): Optional. :obj:`True`, if the user joined the chat after + sending a direct join request without using an invite link and being approved + by an administrator + + .. versionadded:: 21.2 + + """ + + __slots__ = ( + "chat", + "date", + "from_user", + "invite_link", + "new_chat_member", + "old_chat_member", + "via_chat_folder_invite_link", + "via_join_request", + ) + + def __init__( + self, + chat: Chat, + from_user: User, + date: datetime.datetime, + old_chat_member: ChatMember, + new_chat_member: ChatMember, + invite_link: Optional[ChatInviteLink] = None, + via_chat_folder_invite_link: Optional[bool] = None, + via_join_request: Optional[bool] = None, + *, + api_kwargs: Optional[JSONDict] = None, + ): + super().__init__(api_kwargs=api_kwargs) + # Required + self.chat: Chat = chat + self.from_user: User = from_user + self.date: datetime.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 + + # Optionals + self.invite_link: Optional[ChatInviteLink] = invite_link + self.via_join_request: Optional[bool] = via_join_request + + self._id_attrs = ( + self.chat, + self.from_user, + self.date, + self.old_chat_member, + self.new_chat_member, + ) + + self._freeze() + + @classmethod + def de_json( + cls, data: Optional[JSONDict], bot: Optional["Bot"] = None + ) -> Optional["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["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) + + return super().de_json(data=data, bot=bot) + + def _get_attribute_difference(self, attribute: str) -> Tuple[object, object]: + try: + old = self.old_chat_member[attribute] + except KeyError: + old = None + + try: + new = self.new_chat_member[attribute] + except KeyError: + new = None + + return old, new + + def difference( + self, + ) -> Dict[ + str, + Tuple[ + Union[str, bool, datetime.datetime, User], Union[str, bool, datetime.datetime, User] + ], + ]: + """Computes the difference between :attr:`old_chat_member` and :attr:`new_chat_member`. + + Example: + .. code:: pycon + + >>> chat_member_updated.difference() + {'custom_title': ('old title', 'new title')} + + Note: + To determine, if the :attr:`telegram.ChatMember.user` attribute has changed, *every* + attribute of the user will be checked. + + .. versionadded:: 13.5 + + Returns: + 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 + # user.to_dict() is unhashable, so that needs some special casing further down + old_dict = self.old_chat_member.to_dict() + old_user_dict = old_dict.pop("user") + new_dict = self.new_chat_member.to_dict() + new_user_dict = new_dict.pop("user") + + # Generator for speed: we only need to iterate over it once + # we can't directly use the values from old_dict ^ new_dict b/c that set is unordered + attributes = (entry[0] for entry in set(old_dict.items()) ^ set(new_dict.items())) + + result = {attribute: self._get_attribute_difference(attribute) for attribute in attributes} + if old_user_dict != new_user_dict: + result["user"] = (self.old_chat_member.user, self.new_chat_member.user) + + return result # type: ignore[return-value] diff --git a/_chatpermissions.py b/_chatpermissions.py new file mode 100644 index 0000000000000000000000000000000000000000..c4e9e94b7a937d7468ca059dc3ef42f633aee8b0 --- /dev/null +++ b/_chatpermissions.py @@ -0,0 +1,249 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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 ChatPermission.""" +from typing import TYPE_CHECKING, Optional + +from telegram._telegramobject import TelegramObject +from telegram._utils.types import JSONDict + +if TYPE_CHECKING: + from telegram import Bot + + +class ChatPermissions(TelegramObject): + """Describes actions that a non-administrator user is allowed to take in a chat. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`can_send_messages`, + :attr:`can_send_polls`, :attr:`can_send_other_messages`, :attr:`can_add_web_page_previews`, + :attr:`can_change_info`, :attr:`can_invite_users`, :attr:`can_pin_messages`, + :attr:`can_send_audios`, :attr:`can_send_documents`, :attr:`can_send_photos`, + :attr:`can_send_videos`, :attr:`can_send_video_notes`, :attr:`can_send_voice_notes`, and + :attr:`can_manage_topics` are equal. + + .. versionchanged:: 20.0 + :attr:`can_manage_topics` is considered as well when comparing objects of + this type in terms of equality. + .. versionchanged:: 20.5 + + * :attr:`can_send_audios`, :attr:`can_send_documents`, :attr:`can_send_photos`, + :attr:`can_send_videos`, :attr:`can_send_video_notes` and :attr:`can_send_voice_notes` + are considered as well when comparing objects of this type in terms of equality. + * Removed deprecated argument and attribute ``can_send_media_messages``. + + + Note: + Though not stated explicitly in the official docs, Telegram changes not only the + permissions that are set, but also sets all the others to :obj:`False`. However, since not + documented, this behavior may change unbeknown to PTB. + + Args: + can_send_messages (:obj:`bool`, optional): :obj:`True`, if the user is allowed to send text + messages, contacts, locations and venues. + can_send_polls (:obj:`bool`, optional): :obj:`True`, if the user is allowed to send polls. + can_send_other_messages (:obj:`bool`, optional): :obj:`True`, if the user is allowed to + send animations, games, stickers and use inline bots. + can_add_web_page_previews (:obj:`bool`, optional): :obj:`True`, if the user is allowed to + add web page previews to their messages. + can_change_info (:obj:`bool`, optional): :obj:`True`, if the user is allowed to change the + chat title, photo and other settings. Ignored in public supergroups. + can_invite_users (:obj:`bool`, optional): :obj:`True`, if the user is allowed to invite new + users to the chat. + can_pin_messages (:obj:`bool`, optional): :obj:`True`, if the user is allowed to pin + messages. Ignored in public supergroups. + can_manage_topics (:obj:`bool`, optional): :obj:`True`, if the user is allowed + to create forum topics. If omitted defaults to the value of + :attr:`can_pin_messages`. + + .. versionadded:: 20.0 + can_send_audios (:obj:`bool`): :obj:`True`, if the user is allowed to send audios. + + .. versionadded:: 20.1 + can_send_documents (:obj:`bool`): :obj:`True`, if the user is allowed to send documents. + + .. versionadded:: 20.1 + can_send_photos (:obj:`bool`): :obj:`True`, if the user is allowed to send photos. + + .. versionadded:: 20.1 + can_send_videos (:obj:`bool`): :obj:`True`, if the user is allowed to send videos. + + .. versionadded:: 20.1 + can_send_video_notes (:obj:`bool`): :obj:`True`, if the user is allowed to send video + notes. + + .. versionadded:: 20.1 + can_send_voice_notes (:obj:`bool`): :obj:`True`, if the user is allowed to send voice + notes. + + .. versionadded:: 20.1 + + Attributes: + can_send_messages (:obj:`bool`): Optional. :obj:`True`, if the user is allowed to send text + messages, contacts, locations and venues. + can_send_polls (:obj:`bool`): Optional. :obj:`True`, if the user is allowed to send polls, + implies :attr:`can_send_messages`. + can_send_other_messages (:obj:`bool`): Optional. :obj:`True`, if the user is allowed to + send animations, games, stickers and use inline bots. + can_add_web_page_previews (:obj:`bool`): Optional. :obj:`True`, if the user is allowed to + add web page previews to their messages. + can_change_info (:obj:`bool`): Optional. :obj:`True`, if the user is allowed to change the + chat title, photo and other settings. Ignored in public supergroups. + can_invite_users (:obj:`bool`): Optional. :obj:`True`, if the user is allowed to invite + new users to the chat. + can_pin_messages (:obj:`bool`): Optional. :obj:`True`, if the user is allowed to pin + messages. Ignored in public supergroups. + can_manage_topics (:obj:`bool`): Optional. :obj:`True`, if the user is allowed + to create forum topics. If omitted defaults to the value of + :attr:`can_pin_messages`. + + .. versionadded:: 20.0 + can_send_audios (:obj:`bool`): :obj:`True`, if the user is allowed to send audios. + + .. versionadded:: 20.1 + can_send_documents (:obj:`bool`): :obj:`True`, if the user is allowed to send documents. + + .. versionadded:: 20.1 + can_send_photos (:obj:`bool`): :obj:`True`, if the user is allowed to send photos. + + .. versionadded:: 20.1 + can_send_videos (:obj:`bool`): :obj:`True`, if the user is allowed to send videos. + + .. versionadded:: 20.1 + can_send_video_notes (:obj:`bool`): :obj:`True`, if the user is allowed to send video + notes. + + .. versionadded:: 20.1 + can_send_voice_notes (:obj:`bool`): :obj:`True`, if the user is allowed to send voice + notes. + + .. versionadded:: 20.1 + + """ + + __slots__ = ( + "can_add_web_page_previews", + "can_change_info", + "can_invite_users", + "can_manage_topics", + "can_pin_messages", + "can_send_audios", + "can_send_documents", + "can_send_messages", + "can_send_other_messages", + "can_send_photos", + "can_send_polls", + "can_send_video_notes", + "can_send_videos", + "can_send_voice_notes", + ) + + def __init__( + self, + can_send_messages: Optional[bool] = None, + can_send_polls: Optional[bool] = None, + can_send_other_messages: Optional[bool] = None, + can_add_web_page_previews: Optional[bool] = None, + can_change_info: Optional[bool] = None, + can_invite_users: Optional[bool] = None, + can_pin_messages: Optional[bool] = None, + can_manage_topics: Optional[bool] = None, + can_send_audios: Optional[bool] = None, + can_send_documents: Optional[bool] = None, + can_send_photos: Optional[bool] = None, + can_send_videos: Optional[bool] = None, + can_send_video_notes: Optional[bool] = None, + can_send_voice_notes: Optional[bool] = None, + *, + api_kwargs: Optional[JSONDict] = None, + ): + super().__init__(api_kwargs=api_kwargs) + # Required + self.can_send_messages: Optional[bool] = can_send_messages + self.can_send_polls: Optional[bool] = can_send_polls + self.can_send_other_messages: Optional[bool] = can_send_other_messages + self.can_add_web_page_previews: Optional[bool] = can_add_web_page_previews + self.can_change_info: Optional[bool] = can_change_info + self.can_invite_users: Optional[bool] = can_invite_users + self.can_pin_messages: Optional[bool] = can_pin_messages + self.can_manage_topics: Optional[bool] = can_manage_topics + self.can_send_audios: Optional[bool] = can_send_audios + self.can_send_documents: Optional[bool] = can_send_documents + self.can_send_photos: Optional[bool] = can_send_photos + self.can_send_videos: Optional[bool] = can_send_videos + self.can_send_video_notes: Optional[bool] = can_send_video_notes + self.can_send_voice_notes: Optional[bool] = can_send_voice_notes + + self._id_attrs = ( + self.can_send_messages, + self.can_send_polls, + self.can_send_other_messages, + self.can_add_web_page_previews, + self.can_change_info, + self.can_invite_users, + self.can_pin_messages, + self.can_manage_topics, + self.can_send_audios, + self.can_send_documents, + self.can_send_photos, + self.can_send_videos, + self.can_send_video_notes, + self.can_send_voice_notes, + ) + + self._freeze() + + @classmethod + def all_permissions(cls) -> "ChatPermissions": + """ + This method returns an :class:`ChatPermissions` instance with all attributes + set to :obj:`True`. This is e.g. useful when unrestricting a chat member with + :meth:`telegram.Bot.restrict_chat_member`. + + .. versionadded:: 20.0 + + """ + return cls(*(14 * (True,))) + + @classmethod + def no_permissions(cls) -> "ChatPermissions": + """ + This method returns an :class:`ChatPermissions` instance + with all attributes set to :obj:`False`. + + .. versionadded:: 20.0 + """ + return cls(*(14 * (False,))) + + @classmethod + def de_json( + cls, data: Optional[JSONDict], bot: Optional["Bot"] = None + ) -> Optional["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 + if data.get("can_send_media_messages") is not None: + api_kwargs["can_send_media_messages"] = data.pop("can_send_media_messages") + + return super()._de_json(data=data, bot=bot, api_kwargs=api_kwargs) diff --git a/_choseninlineresult.py b/_choseninlineresult.py new file mode 100644 index 0000000000000000000000000000000000000000..76380e958395a047fc61f2ba9bb5af987b392da3 --- /dev/null +++ b/_choseninlineresult.py @@ -0,0 +1,109 @@ +#!/usr/bin/env python +# pylint: disable=too-many-arguments +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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 ChosenInlineResult.""" + +from typing import TYPE_CHECKING, Optional + +from telegram._files.location import Location +from telegram._telegramobject import TelegramObject +from telegram._user import User +from telegram._utils.types import JSONDict + +if TYPE_CHECKING: + from telegram import Bot + + +class ChosenInlineResult(TelegramObject): + """ + Represents a result of an inline query that was chosen by the user and sent to their chat + partner. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`result_id` is equal. + + Note: + * In Python :keyword:`from` is a reserved word. Use :paramref:`from_user` instead. + * It is necessary to enable inline feedback via `@Botfather `_ in + order to receive these objects in updates. + + Args: + result_id (:obj:`str`): The unique identifier for the result that was chosen. + from_user (:class:`telegram.User`): The user that chose the result. + location (:class:`telegram.Location`, optional): Sender location, only for bots that + require user location. + inline_message_id (:obj:`str`, optional): Identifier of the sent inline message. Available + only if there is an inline keyboard attached to the message. Will be also received in + callback queries and can be used to edit the message. + query (:obj:`str`): The query that was used to obtain the result. + + Attributes: + result_id (:obj:`str`): The unique identifier for the result that was chosen. + from_user (:class:`telegram.User`): The user that chose the result. + location (:class:`telegram.Location`): Optional. Sender location, only for bots that + require user location. + inline_message_id (:obj:`str`): Optional. Identifier of the sent inline message. Available + only if there is an inline keyboard attached to the message. Will be also received in + callback queries and can be used to edit the message. + query (:obj:`str`): The query that was used to obtain the result. + + """ + + __slots__ = ("from_user", "inline_message_id", "location", "query", "result_id") + + def __init__( + self, + result_id: str, + from_user: User, + query: str, + location: Optional[Location] = None, + inline_message_id: Optional[str] = None, + *, + api_kwargs: Optional[JSONDict] = None, + ): + super().__init__(api_kwargs=api_kwargs) + + # Required + self.result_id: str = result_id + self.from_user: User = from_user + self.query: str = query + # Optionals + self.location: Optional[Location] = location + self.inline_message_id: Optional[str] = inline_message_id + + self._id_attrs = (self.result_id,) + + self._freeze() + + @classmethod + def de_json( + cls, data: Optional[JSONDict], bot: Optional["Bot"] = None + ) -> Optional["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) + # Optionals + data["location"] = Location.de_json(data.get("location"), bot) + + return super().de_json(data=data, bot=bot) diff --git a/_dice.py b/_dice.py new file mode 100644 index 0000000000000000000000000000000000000000..621e4b13f98b79b0a797bd7b7233c4a68e811773 --- /dev/null +++ b/_dice.py @@ -0,0 +1,160 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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 Dice.""" +from typing import Final, List, Optional + +from telegram import constants +from telegram._telegramobject import TelegramObject +from telegram._utils.types import JSONDict + + +class Dice(TelegramObject): + """ + This object represents an animated emoji with a random value for currently supported base + emoji. (The singular form of "dice" is "die". However, PTB mimics the Telegram API, which uses + the term "dice".) + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`value` and :attr:`emoji` are equal. + + Note: + If :attr:`emoji` is :tg-const:`telegram.Dice.DARTS`, a value of 6 currently + represents a bullseye, while a value of 1 indicates that the dartboard was missed. + However, this behaviour is undocumented and might be changed by Telegram. + + If :attr:`emoji` is :tg-const:`telegram.Dice.BASKETBALL`, a value of 4 or 5 + currently score a basket, while a value of 1 to 3 indicates that the basket was missed. + However, this behaviour is undocumented and might be changed by Telegram. + + If :attr:`emoji` is :tg-const:`telegram.Dice.FOOTBALL`, a value of 4 to 5 + currently scores a goal, while a value of 1 to 3 indicates that the goal was missed. + However, this behaviour is undocumented and might be changed by Telegram. + + If :attr:`emoji` is :tg-const:`telegram.Dice.BOWLING`, a value of 6 knocks + all the pins, while a value of 1 means all the pins were missed. + However, this behaviour is undocumented and might be changed by Telegram. + + If :attr:`emoji` is :tg-const:`telegram.Dice.SLOT_MACHINE`, each value + corresponds to a unique combination of symbols, which + can be found in our + :wiki:`wiki `. + However, this behaviour is undocumented and might be changed by Telegram. + + .. + In args, some links for limits of `value` intentionally point to constants for only + one emoji of a group to avoid duplication. For example, maximum value for Dice, Darts and + Bowling is linked to a constant for Bowling. + + Args: + value (:obj:`int`): Value of the dice. + :tg-const:`telegram.Dice.MIN_VALUE`-:tg-const:`telegram.Dice.MAX_VALUE_BOWLING` + for :tg-const:`telegram.Dice.DICE`, :tg-const:`telegram.Dice.DARTS` and + :tg-const:`telegram.Dice.BOWLING` base emoji, + :tg-const:`telegram.Dice.MIN_VALUE`-:tg-const:`telegram.Dice.MAX_VALUE_BASKETBALL` + for :tg-const:`telegram.Dice.BASKETBALL` and :tg-const:`telegram.Dice.FOOTBALL` + base emoji, + :tg-const:`telegram.Dice.MIN_VALUE`-:tg-const:`telegram.Dice.MAX_VALUE_SLOT_MACHINE` + for :tg-const:`telegram.Dice.SLOT_MACHINE` base emoji. + emoji (:obj:`str`): Emoji on which the dice throw animation is based. + + Attributes: + value (:obj:`int`): Value of the dice. + :tg-const:`telegram.Dice.MIN_VALUE`-:tg-const:`telegram.Dice.MAX_VALUE_BOWLING` + for :tg-const:`telegram.Dice.DICE`, :tg-const:`telegram.Dice.DARTS` and + :tg-const:`telegram.Dice.BOWLING` base emoji, + :tg-const:`telegram.Dice.MIN_VALUE`-:tg-const:`telegram.Dice.MAX_VALUE_BASKETBALL` + for :tg-const:`telegram.Dice.BASKETBALL` and :tg-const:`telegram.Dice.FOOTBALL` + base emoji, + :tg-const:`telegram.Dice.MIN_VALUE`-:tg-const:`telegram.Dice.MAX_VALUE_SLOT_MACHINE` + for :tg-const:`telegram.Dice.SLOT_MACHINE` base emoji. + emoji (:obj:`str`): Emoji on which the dice throw animation is based. + + """ + + __slots__ = ("emoji", "value") + + def __init__(self, value: int, emoji: str, *, api_kwargs: Optional[JSONDict] = None): + super().__init__(api_kwargs=api_kwargs) + self.value: int = value + self.emoji: str = emoji + + self._id_attrs = (self.value, self.emoji) + + self._freeze() + + DICE: Final[str] = constants.DiceEmoji.DICE + """:const:`telegram.constants.DiceEmoji.DICE`""" + DARTS: Final[str] = constants.DiceEmoji.DARTS + """:const:`telegram.constants.DiceEmoji.DARTS`""" + BASKETBALL: Final[str] = constants.DiceEmoji.BASKETBALL + """:const:`telegram.constants.DiceEmoji.BASKETBALL`""" + FOOTBALL: Final[str] = constants.DiceEmoji.FOOTBALL + """:const:`telegram.constants.DiceEmoji.FOOTBALL`""" + SLOT_MACHINE: Final[str] = constants.DiceEmoji.SLOT_MACHINE + """:const:`telegram.constants.DiceEmoji.SLOT_MACHINE`""" + BOWLING: Final[str] = constants.DiceEmoji.BOWLING + """ + :const:`telegram.constants.DiceEmoji.BOWLING` + + .. versionadded:: 13.4 + """ + 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` + + .. versionadded:: 20.0 + """ + + MAX_VALUE_BOWLING: Final[int] = constants.DiceLimit.MAX_VALUE_BOWLING + """:const:`telegram.constants.DiceLimit.MAX_VALUE_BOWLING` + + .. versionadded:: 20.0 + """ + + MAX_VALUE_DARTS: Final[int] = constants.DiceLimit.MAX_VALUE_DARTS + """:const:`telegram.constants.DiceLimit.MAX_VALUE_DARTS` + + .. versionadded:: 20.0 + """ + + MAX_VALUE_DICE: Final[int] = constants.DiceLimit.MAX_VALUE_DICE + """:const:`telegram.constants.DiceLimit.MAX_VALUE_DICE` + + .. versionadded:: 20.0 + """ + + MAX_VALUE_BASKETBALL: Final[int] = constants.DiceLimit.MAX_VALUE_BASKETBALL + """:const:`telegram.constants.DiceLimit.MAX_VALUE_BASKETBALL` + + .. versionadded:: 20.0 + """ + + MAX_VALUE_FOOTBALL: Final[int] = constants.DiceLimit.MAX_VALUE_FOOTBALL + """:const:`telegram.constants.DiceLimit.MAX_VALUE_FOOTBALL` + + .. versionadded:: 20.0 + """ + + MAX_VALUE_SLOT_MACHINE: Final[int] = constants.DiceLimit.MAX_VALUE_SLOT_MACHINE + """:const:`telegram.constants.DiceLimit.MAX_VALUE_SLOT_MACHINE` + + .. versionadded:: 20.0 + """ diff --git a/_forcereply.py b/_forcereply.py new file mode 100644 index 0000000000000000000000000000000000000000..cce00996bbda415a5788f434a34af06f729b96be --- /dev/null +++ b/_forcereply.py @@ -0,0 +1,106 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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 ForceReply.""" + +from typing import Final, Optional + +from telegram import constants +from telegram._telegramobject import TelegramObject +from telegram._utils.types import JSONDict + + +class ForceReply(TelegramObject): + """ + Upon receiving a message with this object, Telegram clients will display a reply interface to + the user (act as if the user has selected the bot's message and tapped 'Reply'). This can be + extremely useful if you want to create user-friendly step-by-step interfaces without having + to sacrifice `privacy mode `_. Not + supported in channels and for messages sent on behalf of a Telegram Business account. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`selective` is equal. + + .. versionchanged:: 20.0 + The (undocumented) argument ``force_reply`` was removed and instead :attr:`force_reply` + is now always set to :obj:`True` as expected by the Bot API. + + Args: + selective (:obj:`bool`, optional): Use this parameter if you want to force reply from + specific users only. Targets: + + 1) Users that are @mentioned in the :attr:`~telegram.Message.text` of the + :class:`telegram.Message` object. + 2) If the bot's message is a reply to a message in the same chat and forum topic, + sender of the original message. + + input_field_placeholder (:obj:`str`, optional): The placeholder to be shown in the input + field when the reply is active; + :tg-const:`telegram.ForceReply.MIN_INPUT_FIELD_PLACEHOLDER`- + :tg-const:`telegram.ForceReply.MAX_INPUT_FIELD_PLACEHOLDER` + characters. + + .. versionadded:: 13.7 + + Attributes: + force_reply (:obj:`True`): Shows reply interface to the user, as if they manually selected + the bots message and tapped 'Reply'. + selective (:obj:`bool`): Optional. Force reply from specific users only. Targets: + + 1) Users that are @mentioned in the :attr:`~telegram.Message.text` of the + :class:`telegram.Message` object. + 2) If the bot's message is a reply to a message in the same chat and forum topic, + sender of the original message. + input_field_placeholder (:obj:`str`): Optional. The placeholder to be shown in the input + field when the reply is active; + :tg-const:`telegram.ForceReply.MIN_INPUT_FIELD_PLACEHOLDER`- + :tg-const:`telegram.ForceReply.MAX_INPUT_FIELD_PLACEHOLDER` + characters. + + .. versionadded:: 13.7 + + """ + + __slots__ = ("force_reply", "input_field_placeholder", "selective") + + def __init__( + self, + selective: Optional[bool] = None, + input_field_placeholder: Optional[str] = None, + *, + api_kwargs: Optional[JSONDict] = None, + ): + super().__init__(api_kwargs=api_kwargs) + self.force_reply: bool = True + self.selective: Optional[bool] = selective + self.input_field_placeholder: Optional[str] = input_field_placeholder + + self._id_attrs = (self.selective,) + + self._freeze() + + MIN_INPUT_FIELD_PLACEHOLDER: Final[int] = constants.ReplyLimit.MIN_INPUT_FIELD_PLACEHOLDER + """:const:`telegram.constants.ReplyLimit.MIN_INPUT_FIELD_PLACEHOLDER` + + .. versionadded:: 20.0 + """ + MAX_INPUT_FIELD_PLACEHOLDER: Final[int] = constants.ReplyLimit.MAX_INPUT_FIELD_PLACEHOLDER + """:const:`telegram.constants.ReplyLimit.MAX_INPUT_FIELD_PLACEHOLDER` + + .. versionadded:: 20.0 + """ diff --git a/_forumtopic.py b/_forumtopic.py new file mode 100644 index 0000000000000000000000000000000000000000..bd66e40d053554a963d7b2c17db8d51c5ab0b07b --- /dev/null +++ b/_forumtopic.py @@ -0,0 +1,215 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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 objects related to Telegram forum topics.""" +from typing import Optional + +from telegram._telegramobject import TelegramObject +from telegram._utils.types import JSONDict + + +class ForumTopic(TelegramObject): + """ + This object represents a forum topic. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`message_thread_id`, :attr:`name` and :attr:`icon_color` + are equal. + + .. versionadded:: 20.0 + + Args: + message_thread_id (:obj:`int`): Unique identifier of the forum topic + name (:obj:`str`): Name of the topic + icon_color (:obj:`int`): Color of the topic icon in RGB format + icon_custom_emoji_id (:obj:`str`, optional): Unique identifier of the custom emoji shown + as the topic icon. + + Attributes: + message_thread_id (:obj:`int`): Unique identifier of the forum topic + name (:obj:`str`): Name of the topic + icon_color (:obj:`int`): Color of the topic icon in RGB format + icon_custom_emoji_id (:obj:`str`): Optional. Unique identifier of the custom emoji shown + as the topic icon. + """ + + __slots__ = ("icon_color", "icon_custom_emoji_id", "message_thread_id", "name") + + def __init__( + self, + message_thread_id: int, + name: str, + icon_color: int, + icon_custom_emoji_id: Optional[str] = None, + *, + api_kwargs: Optional[JSONDict] = None, + ): + super().__init__(api_kwargs=api_kwargs) + self.message_thread_id: int = message_thread_id + self.name: str = name + self.icon_color: int = icon_color + self.icon_custom_emoji_id: Optional[str] = icon_custom_emoji_id + + self._id_attrs = (self.message_thread_id, self.name, self.icon_color) + + self._freeze() + + +class ForumTopicCreated(TelegramObject): + """ + This object represents the content of a service message about a new forum topic created in + the chat. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`name` and :attr:`icon_color` are equal. + + .. versionadded:: 20.0 + + Args: + name (:obj:`str`): Name of the topic + icon_color (:obj:`int`): Color of the topic icon in RGB format + icon_custom_emoji_id (:obj:`str`, optional): Unique identifier of the custom emoji shown + as the topic icon. + + Attributes: + name (:obj:`str`): Name of the topic + icon_color (:obj:`int`): Color of the topic icon in RGB format + icon_custom_emoji_id (:obj:`str`): Optional. Unique identifier of the custom emoji shown + as the topic icon. + """ + + __slots__ = ("icon_color", "icon_custom_emoji_id", "name") + + def __init__( + self, + name: str, + icon_color: int, + icon_custom_emoji_id: Optional[str] = None, + *, + api_kwargs: Optional[JSONDict] = None, + ): + super().__init__(api_kwargs=api_kwargs) + self.name: str = name + self.icon_color: int = icon_color + self.icon_custom_emoji_id: Optional[str] = icon_custom_emoji_id + + self._id_attrs = (self.name, self.icon_color) + + self._freeze() + + +class ForumTopicClosed(TelegramObject): + """ + This object represents a service message about a forum topic closed in the chat. + Currently holds no information. + + .. versionadded:: 20.0 + """ + + __slots__ = () + + def __init__(self, *, api_kwargs: Optional[JSONDict] = None) -> None: + super().__init__(api_kwargs=api_kwargs) + + self._freeze() + + +class ForumTopicReopened(TelegramObject): + """ + This object represents a service message about a forum topic reopened in the chat. + Currently holds no information. + + .. versionadded:: 20.0 + """ + + __slots__ = () + + def __init__(self, *, api_kwargs: Optional[JSONDict] = None) -> None: + super().__init__(api_kwargs=api_kwargs) + + self._freeze() + + +class ForumTopicEdited(TelegramObject): + """ + This object represents a service message about an edited forum topic. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`name` and :attr:`icon_custom_emoji_id` are equal. + + .. versionadded:: 20.0 + + Args: + name (:obj:`str`, optional): New name of the topic, if it was edited. + icon_custom_emoji_id (:obj:`str`, optional): New identifier of the custom emoji shown as + the topic icon, if it was edited; an empty string if the icon was removed. + + Attributes: + name (:obj:`str`): Optional. New name of the topic, if it was edited. + icon_custom_emoji_id (:obj:`str`): Optional. New identifier of the custom emoji shown as + the topic icon, if it was edited; an empty string if the icon was removed. + """ + + __slots__ = ("icon_custom_emoji_id", "name") + + def __init__( + self, + name: Optional[str] = None, + icon_custom_emoji_id: Optional[str] = None, + *, + api_kwargs: Optional[JSONDict] = None, + ): + super().__init__(api_kwargs=api_kwargs) + self.name: Optional[str] = name + self.icon_custom_emoji_id: Optional[str] = icon_custom_emoji_id + + self._id_attrs = (self.name, self.icon_custom_emoji_id) + + self._freeze() + + +class GeneralForumTopicHidden(TelegramObject): + """ + This object represents a service message about General forum topic hidden in the chat. + Currently holds no information. + + .. versionadded:: 20.0 + """ + + __slots__ = () + + def __init__(self, *, api_kwargs: Optional[JSONDict] = None): + super().__init__(api_kwargs=api_kwargs) + + self._freeze() + + +class GeneralForumTopicUnhidden(TelegramObject): + """ + This object represents a service message about General forum topic unhidden in the chat. + Currently holds no information. + + .. versionadded:: 20.0 + """ + + __slots__ = () + + def __init__(self, *, api_kwargs: Optional[JSONDict] = None): + super().__init__(api_kwargs=api_kwargs) + + self._freeze() diff --git a/_giveaway.py b/_giveaway.py new file mode 100644 index 0000000000000000000000000000000000000000..b287433fe0b830d584ad2501fd234fa8a61d337b --- /dev/null +++ b/_giveaway.py @@ -0,0 +1,344 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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 objects that are related to Telegram giveaways.""" +import datetime +from typing import TYPE_CHECKING, Optional, Sequence, Tuple + +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.datetime import extract_tzinfo_from_defaults, from_timestamp +from telegram._utils.types import JSONDict + +if TYPE_CHECKING: + from telegram import Bot, Message + + +class Giveaway(TelegramObject): + """This object represents a message about a scheduled giveaway. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`chats`, :attr:`winners_selection_date` and + :attr:`winner_count` are equal. + + .. versionadded:: 20.8 + + Args: + 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| + winner_count (:obj:`int`): The number of users which are supposed to be selected as winners + of the giveaway. + only_new_members (:obj:`True`, optional): If :obj:`True`, only users who join the chats + after the giveaway started should be eligible to win. + 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 (Sequence[:obj:`str`]): A list 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. + premium_subscription_month_count (:obj:`int`, optional): The number of months the Telegram + Premium subscription won from the giveaway will be active for. + + Attributes: + chats (Sequence[: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| + winner_count (:obj:`int`): The number of users which are supposed to be selected as winners + of the giveaway. + only_new_members (:obj:`True`): Optional. If :obj:`True`, only users who join the chats + after the giveaway started should be eligible to win. + 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 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. + premium_subscription_month_count (:obj:`int`): Optional. The number of months the Telegram + Premium subscription won from the giveaway will be active for. + """ + + __slots__ = ( + "chats", + "country_codes", + "has_public_winners", + "only_new_members", + "premium_subscription_month_count", + "prize_description", + "winner_count", + "winners_selection_date", + ) + + def __init__( + self, + chats: Sequence[Chat], + winners_selection_date: datetime.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, + *, + 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.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.premium_subscription_month_count: Optional[int] = premium_subscription_month_count + + self._id_attrs = ( + self.chats, + self.winners_selection_date, + self.winner_count, + ) + + self._freeze() + + @classmethod + def de_json( + cls, data: Optional[JSONDict], bot: Optional["Bot"] = None + ) -> Optional["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["winners_selection_date"] = from_timestamp( + data.get("winners_selection_date"), tzinfo=loc_tzinfo + ) + + return super().de_json(data=data, bot=bot) + + +class GiveawayCreated(TelegramObject): + """This object represents a service message about the creation of a scheduled giveaway. + Currently holds no information. + """ + + __slots__ = () + + def __init__(self, *, api_kwargs: Optional[JSONDict] = None): + super().__init__(api_kwargs=api_kwargs) + + self._freeze() + + +class GiveawayWinners(TelegramObject): + """This object represents a message about the completion of a giveaway with public winners. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`chat`, :attr:`giveaway_message_id`, + :attr:`winners_selection_date`, :attr:`winner_count` and :attr:`winners` are equal. + + .. versionadded:: 20.8 + + Args: + chat (:class:`telegram.Chat`): The chat that created the giveaway + giveaway_message_id (:obj:`int`): Identifier of the message with the giveaway in the chat + 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 (Sequence[:class:`telegram.User`]): List 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 + 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 + only_new_members (:obj:`True`, optional): :obj:`True`, if only users who had joined the + chats after the giveaway started were eligible to win + was_refunded (:obj:`True`, optional): :obj:`True`, if the giveaway was canceled because the + payment for it was refunded + prize_description (:obj:`str`, optional): Description of additional giveaway prize + + Attributes: + chat (:class:`telegram.Chat`): The chat that created the giveaway + giveaway_message_id (:obj:`int`): Identifier of the message with the giveaway in the chat + 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 + :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 + 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 + only_new_members (:obj:`True`): Optional. :obj:`True`, if only users who had joined the + chats after the giveaway started were eligible to win + was_refunded (:obj:`True`): Optional. :obj:`True`, if the giveaway was canceled because the + payment for it was refunded + prize_description (:obj:`str`): Optional. Description of additional giveaway prize + """ + + __slots__ = ( + "additional_chat_count", + "chat", + "giveaway_message_id", + "only_new_members", + "premium_subscription_month_count", + "prize_description", + "unclaimed_prize_count", + "was_refunded", + "winner_count", + "winners", + "winners_selection_date", + ) + + def __init__( + self, + chat: Chat, + giveaway_message_id: int, + winners_selection_date: datetime.datetime, + winner_count: int, + winners: Sequence[User], + additional_chat_count: Optional[int] = None, + premium_subscription_month_count: Optional[int] = None, + unclaimed_prize_count: Optional[int] = None, + only_new_members: Optional[bool] = None, + was_refunded: Optional[bool] = None, + prize_description: Optional[str] = None, + *, + api_kwargs: Optional[JSONDict] = None, + ): + super().__init__(api_kwargs=api_kwargs) + + self.chat: Chat = chat + self.giveaway_message_id: int = giveaway_message_id + self.winners_selection_date: datetime.datetime = winners_selection_date + self.winner_count: int = winner_count + 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._id_attrs = ( + self.chat, + self.giveaway_message_id, + self.winners_selection_date, + self.winner_count, + self.winners, + ) + + self._freeze() + + @classmethod + def de_json( + cls, data: Optional[JSONDict], bot: Optional["Bot"] = None + ) -> Optional["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["winners_selection_date"] = from_timestamp( + data.get("winners_selection_date"), tzinfo=loc_tzinfo + ) + + return super().de_json(data=data, bot=bot) + + +class GiveawayCompleted(TelegramObject): + """This object represents a service message about the completion of a giveaway without public + winners. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`winner_count` and :attr:`unclaimed_prize_count` are equal. + + .. versionadded:: 20.8 + + + Args: + 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 + + 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 + """ + + __slots__ = ("giveaway_message", "unclaimed_prize_count", "winner_count") + + def __init__( + self, + winner_count: int, + unclaimed_prize_count: Optional[int] = None, + giveaway_message: Optional["Message"] = None, + *, + api_kwargs: Optional[JSONDict] = None, + ): + super().__init__(api_kwargs=api_kwargs) + + self.winner_count: int = winner_count + self.unclaimed_prize_count: Optional[int] = unclaimed_prize_count + self.giveaway_message: Optional[Message] = giveaway_message + + self._id_attrs = ( + self.winner_count, + self.unclaimed_prize_count, + ) + + self._freeze() + + @classmethod + def de_json( + cls, data: Optional[JSONDict], bot: Optional["Bot"] = None + ) -> Optional["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) + + return super().de_json(data=data, bot=bot) diff --git a/_keyboardbutton.py b/_keyboardbutton.py new file mode 100644 index 0000000000000000000000000000000000000000..ad08f2f98ad9d15148efe650a17c6b7c2a7ed1ac --- /dev/null +++ b/_keyboardbutton.py @@ -0,0 +1,191 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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 KeyboardButton.""" + +from typing import TYPE_CHECKING, Optional + +from telegram._keyboardbuttonpolltype import KeyboardButtonPollType +from telegram._keyboardbuttonrequest import KeyboardButtonRequestChat, KeyboardButtonRequestUsers +from telegram._telegramobject import TelegramObject +from telegram._utils.types import JSONDict +from telegram._webappinfo import WebAppInfo + +if TYPE_CHECKING: + from telegram import Bot + + +class KeyboardButton(TelegramObject): + """ + This object represents one button of the reply keyboard. At most one of the optional fields + must be used to specify type of the button. For simple text buttons, :obj:`str` + can be used instead of this object to specify text of the button. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`text`, :attr:`request_contact`, :attr:`request_location`, + :attr:`request_poll`, :attr:`web_app`, :attr:`request_users` and :attr:`request_chat` are + equal. + + Note: + * Optional fields are mutually exclusive. + * :attr:`request_contact` and :attr:`request_location` options will only work in Telegram + versions released after 9 April, 2016. Older clients will display unsupported message. + * :attr:`request_poll` option will only work in Telegram versions released after 23 + January, 2020. Older clients will display unsupported message. + * :attr:`web_app` option will only work in Telegram versions released after 16 April, 2022. + Older clients will display unsupported message. + * :attr:`request_users` and :attr:`request_chat` options will only work in Telegram + versions released after 3 February, 2023. Older clients will display unsupported + message. + + .. versionchanged:: 21.0 + Removed deprecated argument and attribute ``request_user``. + .. versionchanged:: 20.0 + :attr:`web_app` is considered as well when comparing objects of this type in terms of + equality. + .. versionchanged:: 20.5 + :attr:`request_users` and :attr:`request_chat` are considered as well when + comparing objects of this type in terms of equality. + + Args: + text (:obj:`str`): Text of the button. If none of the optional fields are used, it will be + sent to the bot as a message when the button is pressed. + request_contact (:obj:`bool`, optional): If :obj:`True`, the user's phone number will be + sent as a contact when the button is pressed. Available in private chats only. + request_location (:obj:`bool`, optional): If :obj:`True`, the user's current location will + be sent when the button is pressed. Available in private chats only. + request_poll (:class:`~telegram.KeyboardButtonPollType`, optional): If specified, the user + will be asked to create a poll and send it to the bot when the button is pressed. + Available in private chats only. + web_app (:class:`~telegram.WebAppInfo`, optional): If specified, the described `Web App + `_ will be launched when the button is pressed. + The Web App will be able to send a :attr:`Message.web_app_data` service message. + Available in private chats only. + + .. versionadded:: 20.0 + + request_users (:class:`KeyboardButtonRequestUsers`, optional): If specified, pressing the + button will open a list of suitable users. Tapping on any user will send its + identifier to the bot in a :attr:`telegram.Message.users_shared` service message. + Available in private chats only. + + .. versionadded:: 20.8 + request_chat (:class:`KeyboardButtonRequestChat`, optional): If specified, pressing the + button will open a list of suitable chats. Tapping on a chat will send its + identifier to the bot in a :attr:`telegram.Message.chat_shared` service message. + Available in private chats only. + + .. versionadded:: 20.1 + Attributes: + text (:obj:`str`): Text of the button. If none of the optional fields are used, it will be + sent to the bot as a message when the button is pressed. + request_contact (:obj:`bool`): Optional. If :obj:`True`, the user's phone number will be + sent as a contact when the button is pressed. Available in private chats only. + request_location (:obj:`bool`): Optional. If :obj:`True`, the user's current location will + be sent when the button is pressed. Available in private chats only. + request_poll (:class:`~telegram.KeyboardButtonPollType`): Optional. If specified, + the user will be asked to create a poll and send it to the bot when the button is + pressed. Available in private chats only. + web_app (:class:`~telegram.WebAppInfo`): Optional. If specified, the described `Web App + `_ will be launched when the button is pressed. + The Web App will be able to send a :attr:`Message.web_app_data` service message. + Available in private chats only. + + .. versionadded:: 20.0 + request_users (:class:`KeyboardButtonRequestUsers`): Optional. If specified, pressing the + button will open a list of suitable users. Tapping on any user will send its + identifier to the bot in a :attr:`telegram.Message.users_shared` service message. + Available in private chats only. + + .. versionadded:: 20.8 + request_chat (:class:`KeyboardButtonRequestChat`): Optional. If specified, pressing the + button will open a list of suitable chats. Tapping on a chat will send its + identifier to the bot in a :attr:`telegram.Message.chat_shared` service message. + Available in private chats only. + + .. versionadded:: 20.1 + """ + + __slots__ = ( + "request_chat", + "request_contact", + "request_location", + "request_poll", + "request_users", + "text", + "web_app", + ) + + def __init__( + self, + text: str, + request_contact: Optional[bool] = None, + request_location: Optional[bool] = None, + request_poll: Optional[KeyboardButtonPollType] = None, + web_app: Optional[WebAppInfo] = None, + request_chat: Optional[KeyboardButtonRequestChat] = None, + request_users: Optional[KeyboardButtonRequestUsers] = None, + *, + api_kwargs: Optional[JSONDict] = None, + ): + super().__init__(api_kwargs=api_kwargs) + + # Required + self.text: str = text + # Optionals + self.request_contact: Optional[bool] = request_contact + self.request_location: Optional[bool] = request_location + self.request_poll: Optional[KeyboardButtonPollType] = request_poll + self.web_app: Optional[WebAppInfo] = web_app + self.request_users: Optional[KeyboardButtonRequestUsers] = request_users + self.request_chat: Optional[KeyboardButtonRequestChat] = request_chat + + self._id_attrs = ( + self.text, + self.request_contact, + self.request_location, + self.request_poll, + self.web_app, + self.request_users, + self.request_chat, + ) + + self._freeze() + + @classmethod + def de_json( + cls, data: Optional[JSONDict], bot: Optional["Bot"] = None + ) -> Optional["KeyboardButton"]: + """See :meth:`telegram.TelegramObject.de_json`.""" + data = cls._parse_data(data) + + if not data: + return None + + data["request_poll"] = KeyboardButtonPollType.de_json(data.get("request_poll"), bot) + data["request_users"] = KeyboardButtonRequestUsers.de_json(data.get("request_users"), bot) + data["request_chat"] = KeyboardButtonRequestChat.de_json(data.get("request_chat"), bot) + data["web_app"] = WebAppInfo.de_json(data.get("web_app"), bot) + + 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 + if request_user := data.get("request_user"): + api_kwargs = {"request_user": request_user} + + return super()._de_json(data=data, bot=bot, api_kwargs=api_kwargs) diff --git a/_keyboardbuttonpolltype.py b/_keyboardbuttonpolltype.py new file mode 100644 index 0000000000000000000000000000000000000000..f3b987a7fc03b68c0fd2a139905e03af53ac6d6d --- /dev/null +++ b/_keyboardbuttonpolltype.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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 type of a Telegram Poll.""" +from typing import Optional + +from telegram._telegramobject import TelegramObject +from telegram._utils import enum +from telegram._utils.types import JSONDict +from telegram.constants import PollType + + +class KeyboardButtonPollType(TelegramObject): + """This object represents type of a poll, which is allowed to be created + and sent when the corresponding button is pressed. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`type` is equal. + + Examples: + :any:`Poll Bot ` + + Args: + type (:obj:`str`, optional): If :tg-const:`telegram.Poll.QUIZ` is passed, the user will be + allowed to create only polls in the quiz mode. If :tg-const:`telegram.Poll.REGULAR` is + passed, only regular polls will be allowed. Otherwise, the user will be allowed to + create a poll of any type. + Attributes: + type (:obj:`str`): Optional. If equals :tg-const:`telegram.Poll.QUIZ`, the user will + be allowed to create only polls in the quiz mode. If equals + :tg-const:`telegram.Poll.REGULAR`, only regular polls will be allowed. + Otherwise, the user will be allowed to create a poll of any type. + """ + + __slots__ = ("type",) + + def __init__( + self, + type: Optional[str] = None, # pylint: disable=redefined-builtin + *, + api_kwargs: Optional[JSONDict] = None, + ): + super().__init__(api_kwargs=api_kwargs) + self.type: Optional[str] = enum.get_member(PollType, type, type) + + self._id_attrs = (self.type,) + + self._freeze() diff --git a/_keyboardbuttonrequest.py b/_keyboardbuttonrequest.py new file mode 100644 index 0000000000000000000000000000000000000000..4416952112efce4176454fd7eb54f92053541844 --- /dev/null +++ b/_keyboardbuttonrequest.py @@ -0,0 +1,276 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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 two objects to request chats/users.""" + +from typing import TYPE_CHECKING, Optional + +from telegram._chatadministratorrights import ChatAdministratorRights +from telegram._telegramobject import TelegramObject +from telegram._utils.types import JSONDict + +if TYPE_CHECKING: + from telegram import Bot + + +class KeyboardButtonRequestUsers(TelegramObject): + """This object defines the criteria used to request a suitable user. The identifier of the + selected user will be shared with the bot when the corresponding button is pressed. `More + about requesting users » `_. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`request_id` is equal. + + .. versionadded:: 20.8 + This class was previously named ``KeyboardButtonRequestUser``. + + Args: + request_id (:obj:`int`): Signed 32-bit identifier of the request, which will be received + back in the :class:`telegram.UsersShared` object. Must be unique within the message. + user_is_bot (:obj:`bool`, optional): Pass :obj:`True` to request a bot, pass :obj:`False` + to request a regular user. If not specified, no additional restrictions are applied. + user_is_premium (:obj:`bool`, optional): Pass :obj:`True` to request a premium user, pass + :obj:`False` to request a non-premium user. If not specified, no additional + restrictions are applied. + max_quantity (:obj:`int`, optional): The maximum number of users to be selected; + :tg-const:`telegram.constants.KeyboardButtonRequestUsersLimit.MIN_QUANTITY` - + :tg-const:`telegram.constants.KeyboardButtonRequestUsersLimit.MAX_QUANTITY`. + Defaults to :tg-const:`telegram.constants.KeyboardButtonRequestUsersLimit.MIN_QUANTITY` + . + + .. versionadded:: 20.8 + request_name (:obj:`bool`, optional): Pass :obj:`True` to request the users' first and last + name. + + .. versionadded:: 21.1 + request_username (:obj:`bool`, optional): Pass :obj:`True` to request the users' username. + + .. versionadded:: 21.1 + request_photo (:obj:`bool`, optional): Pass :obj:`True` to request the users' photo. + + .. versionadded:: 21.1 + + Attributes: + request_id (:obj:`int`): Identifier of the request. + user_is_bot (:obj:`bool`): Optional. Pass :obj:`True` to request a bot, pass :obj:`False` + to request a regular user. If not specified, no additional restrictions are applied. + user_is_premium (:obj:`bool`): Optional. Pass :obj:`True` to request a premium user, pass + :obj:`False` to request a non-premium user. If not specified, no additional + restrictions are applied. + max_quantity (:obj:`int`): Optional. The maximum number of users to be selected; + :tg-const:`telegram.constants.KeyboardButtonRequestUsersLimit.MIN_QUANTITY` - + :tg-const:`telegram.constants.KeyboardButtonRequestUsersLimit.MAX_QUANTITY`. + Defaults to :tg-const:`telegram.constants.KeyboardButtonRequestUsersLimit.MIN_QUANTITY` + . + + .. versionadded:: 20.8 + request_name (:obj:`bool`): Optional. Pass :obj:`True` to request the users' first and last + name. + + .. versionadded:: 21.1 + request_username (:obj:`bool`): Optional. Pass :obj:`True` to request the users' username. + + .. versionadded:: 21.1 + request_photo (:obj:`bool`): Optional. Pass :obj:`True` to request the users' photo. + + .. versionadded:: 21.1 + + """ + + __slots__ = ( + "max_quantity", + "request_id", + "request_name", + "request_photo", + "request_username", + "user_is_bot", + "user_is_premium", + ) + + def __init__( + self, + request_id: int, + user_is_bot: Optional[bool] = None, + user_is_premium: Optional[bool] = None, + max_quantity: Optional[int] = None, + request_name: Optional[bool] = None, + request_username: Optional[bool] = None, + request_photo: Optional[bool] = None, + *, + api_kwargs: Optional[JSONDict] = None, + ): + super().__init__(api_kwargs=api_kwargs) + # Required + self.request_id: int = request_id + + # Optionals + self.user_is_bot: Optional[bool] = user_is_bot + self.user_is_premium: Optional[bool] = user_is_premium + self.max_quantity: Optional[int] = max_quantity + self.request_name: Optional[bool] = request_name + self.request_username: Optional[bool] = request_username + self.request_photo: Optional[bool] = request_photo + + self._id_attrs = (self.request_id,) + + self._freeze() + + +class KeyboardButtonRequestChat(TelegramObject): + """This object defines the criteria used to request a suitable chat. The identifier of the + selected user will be shared with the bot when the corresponding button is pressed. `More + about requesting users » `_. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`request_id` is equal. + + .. versionadded:: 20.1 + + Args: + request_id (:obj:`int`): Signed 32-bit identifier of the request, which will be received + back in the :class:`telegram.ChatShared` object. Must be unique within the message. + chat_is_channel (:obj:`bool`): Pass :obj:`True` to request a channel chat, pass + :obj:`False` to request a group or a supergroup chat. + chat_is_forum (:obj:`bool`, optional): Pass :obj:`True` to request a forum supergroup, pass + :obj:`False` to request a non-forum chat. If not specified, no additional + restrictions are applied. + chat_has_username (:obj:`bool`, optional): Pass :obj:`True` to request a supergroup or a + channel with a username, pass :obj:`False` to request a chat without a username. If + not specified, no additional restrictions are applied. + chat_is_created (:obj:`bool`, optional): Pass :obj:`True` to request a chat owned by the + user. Otherwise, no additional restrictions are applied. + user_administrator_rights (:class:`ChatAdministratorRights`, optional): Specifies the + required administrator rights of the user in the chat. If not specified, no additional + restrictions are applied. + bot_administrator_rights (:class:`ChatAdministratorRights`, optional): Specifies the + required administrator rights of the bot in the chat. The rights must be a subset of + :paramref:`user_administrator_rights`. If not specified, no additional restrictions are + applied. + bot_is_member (:obj:`bool`, optional): Pass :obj:`True` to request a chat with the bot + as a member. Otherwise, no additional restrictions are applied. + request_title (:obj:`bool`, optional): Pass :obj:`True` to request the chat's title. + + .. versionadded:: 21.1 + request_username (:obj:`bool`, optional): Pass :obj:`True` to request the chat's username. + + .. versionadded:: 21.1 + request_photo (:obj:`bool`, optional): Pass :obj:`True` to request the chat's photo. + + .. versionadded:: 21.1 + Attributes: + request_id (:obj:`int`): Identifier of the request. + chat_is_channel (:obj:`bool`): Pass :obj:`True` to request a channel chat, pass + :obj:`False` to request a group or a supergroup chat. + chat_is_forum (:obj:`bool`): Optional. Pass :obj:`True` to request a forum supergroup, pass + :obj:`False` to request a non-forum chat. If not specified, no additional + restrictions are applied. + chat_has_username (:obj:`bool`): Optional. Pass :obj:`True` to request a supergroup or a + channel with a username, pass :obj:`False` to request a chat without a username. If + not specified, no additional restrictions are applied. + chat_is_created (:obj:`bool`) Optional. Pass :obj:`True` to request a chat owned by the + user. Otherwise, no additional restrictions are applied. + user_administrator_rights (:class:`ChatAdministratorRights`) Optional. Specifies the + required administrator rights of the user in the chat. If not specified, no additional + restrictions are applied. + bot_administrator_rights (:class:`ChatAdministratorRights`) Optional. Specifies the + required administrator rights of the bot in the chat. The rights must be a subset of + :attr:`user_administrator_rights`. If not specified, no additional restrictions are + applied. + bot_is_member (:obj:`bool`) Optional. Pass :obj:`True` to request a chat with the bot + as a member. Otherwise, no additional restrictions are applied. + request_title (:obj:`bool`): Optional. Pass :obj:`True` to request the chat's title. + + .. versionadded:: 21.1 + request_username (:obj:`bool`): Optional. Pass :obj:`True` to request the chat's username. + + .. versionadded:: 21.1 + request_photo (:obj:`bool`): Optional. Pass :obj:`True` to request the chat's photo. + + .. versionadded:: 21.1 + """ + + __slots__ = ( + "bot_administrator_rights", + "bot_is_member", + "chat_has_username", + "chat_is_channel", + "chat_is_created", + "chat_is_forum", + "request_id", + "request_photo", + "request_title", + "request_username", + "user_administrator_rights", + ) + + def __init__( + self, + request_id: int, + chat_is_channel: bool, + chat_is_forum: Optional[bool] = None, + chat_has_username: Optional[bool] = None, + chat_is_created: Optional[bool] = None, + user_administrator_rights: Optional[ChatAdministratorRights] = None, + bot_administrator_rights: Optional[ChatAdministratorRights] = None, + bot_is_member: Optional[bool] = None, + request_title: Optional[bool] = None, + request_username: Optional[bool] = None, + request_photo: Optional[bool] = None, + *, + api_kwargs: Optional[JSONDict] = None, + ): + super().__init__(api_kwargs=api_kwargs) + # required + self.request_id: int = request_id + self.chat_is_channel: bool = chat_is_channel + + # optional + self.chat_is_forum: Optional[bool] = chat_is_forum + self.chat_has_username: Optional[bool] = chat_has_username + self.chat_is_created: Optional[bool] = chat_is_created + self.user_administrator_rights: Optional[ChatAdministratorRights] = ( + user_administrator_rights + ) + self.bot_administrator_rights: Optional[ChatAdministratorRights] = bot_administrator_rights + self.bot_is_member: Optional[bool] = bot_is_member + self.request_title: Optional[bool] = request_title + self.request_username: Optional[bool] = request_username + self.request_photo: Optional[bool] = request_photo + + self._id_attrs = (self.request_id,) + + self._freeze() + + @classmethod + def de_json( + cls, data: Optional[JSONDict], bot: Optional["Bot"] = None + ) -> Optional["KeyboardButtonRequestChat"]: + """See :meth:`telegram.TelegramObject.de_json`.""" + data = cls._parse_data(data) + + if not data: + return None + + data["user_administrator_rights"] = ChatAdministratorRights.de_json( + data.get("user_administrator_rights"), bot + ) + data["bot_administrator_rights"] = ChatAdministratorRights.de_json( + data.get("bot_administrator_rights"), bot + ) + + return super().de_json(data=data, bot=bot) diff --git a/_linkpreviewoptions.py b/_linkpreviewoptions.py new file mode 100644 index 0000000000000000000000000000000000000000..b88fbc55877fd48f28619c9136404c4d949eccec --- /dev/null +++ b/_linkpreviewoptions.py @@ -0,0 +1,103 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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 the LinkPreviewOptions class.""" + + +from typing import Optional + +from telegram._telegramobject import TelegramObject +from telegram._utils.defaultvalue import DEFAULT_NONE +from telegram._utils.types import JSONDict, ODVInput + + +class LinkPreviewOptions(TelegramObject): + """ + Describes the options used for link preview generation. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`is_disabled`, :attr:`url`, :attr:`prefer_small_media`, + :attr:`prefer_large_media`, and :attr:`show_above_text` are equal. + + .. versionadded:: 20.8 + + Args: + is_disabled (:obj:`bool`, optional): :obj:`True`, if the link preview is disabled. + url (:obj:`str`, optional): The URL to use for the link preview. If empty, then the first + URL found in the message text will be used. + prefer_small_media (:obj:`bool`, optional): :obj:`True`, if the media in the link preview + is supposed to be shrunk; ignored if the URL isn't explicitly specified or media size + change isn't supported for the preview. + prefer_large_media (:obj:`bool`, optional): :obj:`True`, if the media in the link preview + is supposed to be enlarged; ignored if the URL isn't explicitly specified or media + size change isn't supported for the preview. + show_above_text (:obj:`bool`, optional): :obj:`True`, if the link preview must be shown + above the message text; otherwise, the link preview will be shown below the message + text. + + Attributes: + is_disabled (:obj:`bool`): Optional. :obj:`True`, if the link preview is disabled. + url (:obj:`str`): Optional. The URL to use for the link preview. If empty, then the first + URL found in the message text will be used. + prefer_small_media (:obj:`bool`): Optional. :obj:`True`, if the media in the link preview + is supposed to be shrunk; ignored if the URL isn't explicitly specified or media size + change isn't supported for the preview. + prefer_large_media (:obj:`bool`): Optional. :obj:`True`, if the media in the link preview + is supposed to be enlarged; ignored if the URL isn't explicitly specified or media size + change isn't supported for the preview. + show_above_text (:obj:`bool`): Optional. :obj:`True`, if the link preview must be shown + above the message text; otherwise, the link preview will be shown below the message + text. + """ + + __slots__ = ( + "is_disabled", + "prefer_large_media", + "prefer_small_media", + "show_above_text", + "url", + ) + + def __init__( + self, + is_disabled: ODVInput[bool] = DEFAULT_NONE, + url: ODVInput[str] = DEFAULT_NONE, + prefer_small_media: ODVInput[bool] = DEFAULT_NONE, + prefer_large_media: ODVInput[bool] = DEFAULT_NONE, + show_above_text: ODVInput[bool] = DEFAULT_NONE, + *, + api_kwargs: Optional[JSONDict] = None, + ): + super().__init__(api_kwargs=api_kwargs) + + # Optionals + + self.is_disabled: ODVInput[bool] = is_disabled + self.url: ODVInput[str] = url + self.prefer_small_media: ODVInput[bool] = prefer_small_media + self.prefer_large_media: ODVInput[bool] = prefer_large_media + self.show_above_text: ODVInput[bool] = show_above_text + + self._id_attrs = ( + self.is_disabled, + self.url, + self.prefer_small_media, + self.prefer_large_media, + self.show_above_text, + ) + self._freeze() diff --git a/_loginurl.py b/_loginurl.py new file mode 100644 index 0000000000000000000000000000000000000000..4201b7ab50faa3b75e62c1a0473ce9be6f6b76f2 --- /dev/null +++ b/_loginurl.py @@ -0,0 +1,105 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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 LoginUrl.""" +from typing import Optional + +from telegram._telegramobject import TelegramObject +from telegram._utils.types import JSONDict + + +class LoginUrl(TelegramObject): + """This object represents a parameter of the inline keyboard button used to automatically + authorize a user. Serves as a great replacement for the Telegram Login Widget when the user is + coming from Telegram. All the user needs to do is tap/click a button and confirm that they want + to log in. Telegram apps support these buttons as of version 5.7. + + Sample bot: `@discussbot `_ + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`url` is equal. + + Note: + You must always check the hash of the received data to verify the authentication + and the integrity of the data as described in + `Checking authorization `_ + + Args: + url (:obj:`str`): An HTTPS URL to be opened with user authorization data added to the query + string when the button is pressed. If the user refuses to provide authorization data, + the original URL without information about the user will be opened. The data added is + the same as described in + `Receiving authorization data + `_. + forward_text (:obj:`str`, optional): New text of the button in forwarded messages. + bot_username (:obj:`str`, optional): Username of a bot, which will be used for user + authorization. See + `Setting up a bot `_ + for more details. If not specified, the current + bot's username will be assumed. The url's domain must be the same as the domain linked + with the bot. See + `Linking your domain to the bot + `_ + for more details. + request_write_access (:obj:`bool`, optional): Pass :obj:`True` to request the permission + for your bot to send messages to the user. + + Attributes: + url (:obj:`str`): An HTTPS URL to be opened with user authorization data added to the query + string when the button is pressed. If the user refuses to provide authorization data, + the original URL without information about the user will be opened. The data added is + the same as described in + `Receiving authorization data + `_. + forward_text (:obj:`str`): Optional. New text of the button in forwarded messages. + bot_username (:obj:`str`): Optional. Username of a bot, which will be used for user + authorization. See + `Setting up a bot `_ + for more details. If not specified, the current + bot's username will be assumed. The url's domain must be the same as the domain linked + with the bot. See + `Linking your domain to the bot + `_ + for more details. + request_write_access (:obj:`bool`): Optional. Pass :obj:`True` to request the permission + for your bot to send messages to the user. + + """ + + __slots__ = ("bot_username", "forward_text", "request_write_access", "url") + + def __init__( + self, + url: str, + forward_text: Optional[str] = None, + bot_username: Optional[str] = None, + request_write_access: Optional[bool] = None, + *, + api_kwargs: Optional[JSONDict] = None, + ): + super().__init__(api_kwargs=api_kwargs) + # Required + self.url: str = url + # Optional + self.forward_text: Optional[str] = forward_text + self.bot_username: Optional[str] = bot_username + self.request_write_access: Optional[bool] = request_write_access + + self._id_attrs = (self.url,) + + self._freeze() diff --git a/_menubutton.py b/_menubutton.py new file mode 100644 index 0000000000000000000000000000000000000000..50b6511b08df7db86c40d6f56db2568345328fe2 --- /dev/null +++ b/_menubutton.py @@ -0,0 +1,201 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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 objects related to Telegram menu buttons.""" +from typing import TYPE_CHECKING, Dict, Final, Optional, Type + +from telegram import constants +from telegram._telegramobject import TelegramObject +from telegram._utils import enum +from telegram._utils.types import JSONDict +from telegram._webappinfo import WebAppInfo + +if TYPE_CHECKING: + from telegram import Bot + + +class MenuButton(TelegramObject): + """This object describes the bot's menu button in a private chat. It should be one of + + * :class:`telegram.MenuButtonCommands` + * :class:`telegram.MenuButtonWebApp` + * :class:`telegram.MenuButtonDefault` + + If a menu button other than :class:`telegram.MenuButtonDefault` is set for a private chat, + then it is applied in the chat. Otherwise the default menu button is applied. By default, the + menu button opens the list of bot commands. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`type` is equal. For subclasses with additional attributes, + the notion of equality is overridden. + + .. versionadded:: 20.0 + + Args: + type (:obj:`str`): Type of menu button that the instance represents. + + Attributes: + type (:obj:`str`): Type of menu button that the instance represents. + """ + + __slots__ = ("type",) + + def __init__( + self, + type: str, + *, + api_kwargs: Optional[JSONDict] = None, + ): # pylint: disable=redefined-builtin + super().__init__(api_kwargs=api_kwargs) + self.type: str = enum.get_member(constants.MenuButtonType, type, type) + + self._id_attrs = (self.type,) + + self._freeze() + + @classmethod + def de_json( + cls, data: Optional[JSONDict], bot: Optional["Bot"] = None + ) -> Optional["MenuButton"]: + """Converts JSON data to the appropriate :class:`MenuButton` object, i.e. takes + care of selecting the correct subclass. + + Args: + 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. + + .. versionchanged:: 21.4 + :paramref:`bot` is now optional and defaults to :obj:`None` + + Returns: + The Telegram object. + + """ + data = cls._parse_data(data) + + if data is None: + return None + + if not data and cls is MenuButton: + return None + + _class_mapping: Dict[str, Type[MenuButton]] = { + cls.COMMANDS: MenuButtonCommands, + cls.WEB_APP: MenuButtonWebApp, + cls.DEFAULT: MenuButtonDefault, + } + + if cls is MenuButton and data.get("type") in _class_mapping: + return _class_mapping[data.pop("type")].de_json(data, bot=bot) + return super().de_json(data=data, bot=bot) + + COMMANDS: Final[str] = constants.MenuButtonType.COMMANDS + """:const:`telegram.constants.MenuButtonType.COMMANDS`""" + WEB_APP: Final[str] = constants.MenuButtonType.WEB_APP + """:const:`telegram.constants.MenuButtonType.WEB_APP`""" + DEFAULT: Final[str] = constants.MenuButtonType.DEFAULT + """:const:`telegram.constants.MenuButtonType.DEFAULT`""" + + +class MenuButtonCommands(MenuButton): + """Represents a menu button, which opens the bot's list of commands. + + .. include:: inclusions/menu_button_command_video.rst + + .. versionadded:: 20.0 + Attributes: + type (:obj:`str`): :tg-const:`telegram.constants.MenuButtonType.COMMANDS`. + """ + + __slots__ = () + + def __init__(self, *, api_kwargs: Optional[JSONDict] = None): + super().__init__(type=constants.MenuButtonType.COMMANDS, api_kwargs=api_kwargs) + self._freeze() + + +class MenuButtonWebApp(MenuButton): + """Represents a menu button, which launches a + `Web App `_. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`type`, :attr:`text` and :attr:`web_app` + are equal. + + .. versionadded:: 20.0 + + Args: + text (:obj:`str`): Text of the button. + web_app (:class:`telegram.WebAppInfo`): 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.answerWebAppQuery` + of :class:`~telegram.Bot`. Alternatively, a ``t.me`` link to a Web App of the bot can + be specified in the object instead of the Web App's URL, in which case the Web App + will be opened as if the user pressed the link. + + + Attributes: + type (:obj:`str`): :tg-const:`telegram.constants.MenuButtonType.WEB_APP`. + text (:obj:`str`): Text of the button. + web_app (:class:`telegram.WebAppInfo`): 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.answerWebAppQuery` + of :class:`~telegram.Bot`. Alternatively, a ``t.me`` link to a Web App of the bot can + be specified in the object instead of the Web App's URL, in which case the Web App + will be opened as if the user pressed the link. + """ + + __slots__ = ("text", "web_app") + + def __init__(self, text: str, web_app: WebAppInfo, *, api_kwargs: Optional[JSONDict] = None): + super().__init__(type=constants.MenuButtonType.WEB_APP, api_kwargs=api_kwargs) + with self._unfrozen(): + self.text: str = text + self.web_app: WebAppInfo = web_app + + self._id_attrs = (self.type, self.text, self.web_app) + + @classmethod + def de_json( + cls, data: Optional[JSONDict], bot: Optional["Bot"] = None + ) -> Optional["MenuButtonWebApp"]: + """See :meth:`telegram.TelegramObject.de_json`.""" + data = cls._parse_data(data) + + if not data: + return None + + data["web_app"] = WebAppInfo.de_json(data.get("web_app"), bot) + + return super().de_json(data=data, bot=bot) # type: ignore[return-value] + + +class MenuButtonDefault(MenuButton): + """Describes that no specific value for the menu button was set. + + .. versionadded:: 20.0 + Attributes: + type (:obj:`str`): :tg-const:`telegram.constants.MenuButtonType.DEFAULT`. + """ + + __slots__ = () + + def __init__(self, *, api_kwargs: Optional[JSONDict] = None): + super().__init__(type=constants.MenuButtonType.DEFAULT, api_kwargs=api_kwargs) + self._freeze() diff --git a/_message.py b/_message.py new file mode 100644 index 0000000000000000000000000000000000000000..11bee5724939dff60d052afa11c20126e4759bfb --- /dev/null +++ b/_message.py @@ -0,0 +1,5037 @@ +#!/usr/bin/env python +# pylint: disable=too-many-instance-attributes, too-many-arguments +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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 Message.""" + +import datetime +import re +from html import escape +from typing import TYPE_CHECKING, Dict, List, Optional, Sequence, Tuple, TypedDict, Union + +from telegram._chat import Chat +from telegram._chatbackground import ChatBackground +from telegram._chatboost import ChatBoostAdded +from telegram._dice import Dice +from telegram._files.animation import Animation +from telegram._files.audio import Audio +from telegram._files.contact import Contact +from telegram._files.document import Document +from telegram._files.location import Location +from telegram._files.photosize import PhotoSize +from telegram._files.sticker import Sticker +from telegram._files.venue import Venue +from telegram._files.video import Video +from telegram._files.videonote import VideoNote +from telegram._files.voice import Voice +from telegram._forumtopic import ( + ForumTopicClosed, + ForumTopicCreated, + ForumTopicEdited, + ForumTopicReopened, + GeneralForumTopicHidden, + GeneralForumTopicUnhidden, +) +from telegram._games.game import Game +from telegram._inline.inlinekeyboardmarkup import InlineKeyboardMarkup +from telegram._linkpreviewoptions import LinkPreviewOptions +from telegram._messageautodeletetimerchanged import MessageAutoDeleteTimerChanged +from telegram._messageentity import MessageEntity +from telegram._paidmedia import PaidMediaInfo +from telegram._passport.passportdata import PassportData +from telegram._payment.invoice import Invoice +from telegram._payment.refundedpayment import RefundedPayment +from telegram._payment.successfulpayment import SuccessfulPayment +from telegram._poll import Poll +from telegram._proximityalerttriggered import ProximityAlertTriggered +from telegram._reply import ReplyParameters +from telegram._shared import ChatShared, UsersShared +from telegram._story import Story +from telegram._telegramobject import TelegramObject +from telegram._user import User +from telegram._utils.argumentparsing import parse_sequence_arg +from telegram._utils.datetime import extract_tzinfo_from_defaults, from_timestamp +from telegram._utils.defaultvalue import DEFAULT_NONE, DefaultValue +from telegram._utils.entities import parse_message_entities, parse_message_entity +from telegram._utils.strings import TextEncoding +from telegram._utils.types import ( + CorrectOptionID, + FileInput, + JSONDict, + MarkdownVersion, + ODVInput, + ReplyMarkup, +) +from telegram._utils.warnings import warn +from telegram._videochat import ( + VideoChatEnded, + VideoChatParticipantsInvited, + VideoChatScheduled, + VideoChatStarted, +) +from telegram._webappdata import WebAppData +from telegram._writeaccessallowed import WriteAccessAllowed +from telegram.constants import ZERO_DATE, MessageAttachmentType, ParseMode +from telegram.helpers import escape_markdown +from telegram.warnings import PTBDeprecationWarning + +if TYPE_CHECKING: + from telegram import ( + Bot, + ExternalReplyInfo, + GameHighScore, + Giveaway, + GiveawayCompleted, + GiveawayCreated, + GiveawayWinners, + InputMedia, + InputMediaAudio, + InputMediaDocument, + InputMediaPhoto, + InputMediaVideo, + InputPollOption, + LabeledPrice, + MessageId, + MessageOrigin, + ReactionType, + TextQuote, + ) + + +class _ReplyKwargs(TypedDict): + __slots__ = ("chat_id", "reply_parameters") # type: ignore[misc] + + chat_id: Union[str, int] + reply_parameters: ReplyParameters + + +class MaybeInaccessibleMessage(TelegramObject): + """Base class for Telegram Message Objects. + + Currently, that includes :class:`telegram.Message` and :class:`telegram.InaccessibleMessage`. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`message_id` and :attr:`chat` are equal + + .. versionchanged:: 21.0 + ``__bool__`` is no longer overriden and defaults to Pythons standard implementation. + + .. versionadded:: 20.8 + + Args: + message_id (:obj:`int`): Unique message identifier. + date (:class:`datetime.datetime`): Date the message was sent in Unix time or 0 in Unix + time. Converted to :class:`datetime.datetime` + + |datetime_localization| + chat (:class:`telegram.Chat`): Conversation the message belongs to. + + Attributes: + message_id (:obj:`int`): Unique message identifier. + date (:class:`datetime.datetime`): Date the message was sent in Unix time or 0 in Unix + time. Converted to :class:`datetime.datetime` + + |datetime_localization| + chat (:class:`telegram.Chat`): Conversation the message belongs to. + """ + + __slots__ = ("chat", "date", "message_id") + + def __init__( + self, + chat: Chat, + message_id: int, + date: datetime.datetime, + *, + api_kwargs: Optional[JSONDict] = None, + ): + super().__init__(api_kwargs=api_kwargs) + self.chat: Chat = chat + self.message_id: int = message_id + self.date: datetime.datetime = date + + self._id_attrs = (self.message_id, self.chat) + + self._freeze() + + @property + def is_accessible(self) -> bool: + """Convenience attribute. :obj:`True`, if the date is not 0 in Unix time. + + .. versionadded:: 20.8 + """ + # Once we drop support for python 3.9, this can be made a TypeGuard function: + # def is_accessible(self) -> TypeGuard[Message]: + return self.date != ZERO_DATE + + @classmethod + def _de_json( + cls, + data: Optional[JSONDict], + bot: Optional["Bot"] = None, + api_kwargs: Optional[JSONDict] = None, + ) -> Optional["MaybeInaccessibleMessage"]: + """See :meth:`telegram.TelegramObject.de_json`.""" + data = cls._parse_data(data) + + if not data: + return None + + if cls is MaybeInaccessibleMessage: + if data["date"] == 0: + return InaccessibleMessage.de_json(data=data, bot=bot) + return Message.de_json(data=data, bot=bot) + + # Get the local timezone from the bot if it has defaults + loc_tzinfo = extract_tzinfo_from_defaults(bot) + + # this is to include the Literal from InaccessibleMessage + if data["date"] == 0: + data["date"] = ZERO_DATE + else: + data["date"] = from_timestamp(data["date"], tzinfo=loc_tzinfo) + + data["chat"] = Chat.de_json(data.get("chat"), bot) + return super()._de_json(data=data, bot=bot, api_kwargs=api_kwargs) + + +class InaccessibleMessage(MaybeInaccessibleMessage): + """This object represents an inaccessible message. + + These are messages that are e.g. deleted. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`message_id` and :attr:`chat` are equal + + .. versionadded:: 20.8 + + Args: + message_id (:obj:`int`): Unique message identifier. + chat (:class:`telegram.Chat`): Chat the message belongs to. + + Attributes: + message_id (:obj:`int`): Unique message identifier. + date (:class:`constants.ZERO_DATE`): Always :tg-const:`telegram.constants.ZERO_DATE`. + The field can be used to differentiate regular and inaccessible messages. + chat (:class:`telegram.Chat`): Chat the message belongs to. + """ + + __slots__ = () + + def __init__( + self, + chat: Chat, + message_id: int, + *, + api_kwargs: Optional[JSONDict] = None, + ): + super().__init__(chat=chat, message_id=message_id, date=ZERO_DATE, api_kwargs=api_kwargs) + self._freeze() + + +class Message(MaybeInaccessibleMessage): + # fmt: off + """This object represents a message. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`message_id` and :attr:`chat` are equal. + + Note: + In Python :keyword:`from` is a reserved word. Use :paramref:`from_user` instead. + + .. versionchanged:: 21.0 + Removed deprecated arguments and attributes ``user_shared``, ``forward_from``, + ``forward_from_chat``, ``forward_from_message_id``, ``forward_signature``, + ``forward_sender_name`` and ``forward_date``. + + .. versionchanged:: 20.8 + * This class is now a subclass of :class:`telegram.MaybeInaccessibleMessage`. + * The :paramref:`pinned_message` now can be either :class:`telegram.Message` or + :class:`telegram.InaccessibleMessage`. + + .. versionchanged:: 20.0 + + * The arguments and attributes ``voice_chat_scheduled``, ``voice_chat_started`` and + ``voice_chat_ended``, ``voice_chat_participants_invited`` were renamed to + :paramref:`video_chat_scheduled`/:attr:`video_chat_scheduled`, + :paramref:`video_chat_started`/:attr:`video_chat_started`, + :paramref:`video_chat_ended`/:attr:`video_chat_ended` and + :paramref:`video_chat_participants_invited`/:attr:`video_chat_participants_invited`, + respectively, in accordance to Bot API 6.0. + * The following are now keyword-only arguments in Bot methods: + ``{read, write, connect, pool}_timeout``, ``api_kwargs``, ``contact``, ``quote``, + ``filename``, ``loaction``, ``venue``. Use a named argument for those, + and notice that some positional arguments changed position as a result. + + Args: + message_id (:obj:`int`): Unique message identifier inside this chat. + from_user (:class:`telegram.User`, optional): Sender of the message; may be empty for + messages sent to channels. For backward compatibility, if the message was sent on + behalf of a chat, the field contains a fake sender user in non-channel chats. + sender_chat (:class:`telegram.Chat`, optional): Sender of the message when sent on behalf + of a chat. For example, the supergroup itself for messages sent by its anonymous + administrators or a linked channel for messages automatically forwarded to the + channel's discussion group. For backward compatibility, if the message was sent on + behalf of a chat, the field from contains a fake sender user in non-channel chats. + date (:class:`datetime.datetime`): Date the message was sent in Unix time. Converted to + :class:`datetime.datetime`. + + .. versionchanged:: 20.3 + |datetime_localization| + chat (:class:`telegram.Chat`): Conversation the message belongs to. + is_automatic_forward (:obj:`bool`, optional): :obj:`True`, if the message is a channel + post that was automatically forwarded to the connected discussion group. + + .. versionadded:: 13.9 + reply_to_message (:class:`telegram.Message`, optional): For replies, the original message. + Note that the Message object in this field will not contain further + ``reply_to_message`` fields even if it itself is a reply. + edit_date (:class:`datetime.datetime`, optional): Date the message was last edited in Unix + time. Converted to :class:`datetime.datetime`. + + .. versionchanged:: 20.3 + |datetime_localization| + has_protected_content (:obj:`bool`, optional): :obj:`True`, if the message can't be + forwarded. + + .. versionadded:: 13.9 + is_from_offline (:obj:`bool`, optional): :obj:`True`, if the message was sent + by an implicit action, for example, as an away or a greeting business message, + or as a scheduled message. + + .. versionadded:: 21.1 + media_group_id (:obj:`str`, optional): The unique identifier of a media message group this + message belongs to. + text (:obj:`str`, optional): For text messages, the actual UTF-8 text of the message, + 0-:tg-const:`telegram.constants.MessageLimit.MAX_TEXT_LENGTH` characters. + entities (Sequence[:class:`telegram.MessageEntity`], optional): For text messages, special + entities like usernames, URLs, bot commands, etc. that appear in the text. See + :attr:`parse_entity` and :attr:`parse_entities` methods for how to use properly. + This list is empty if the message does not contain entities. + + .. versionchanged:: 20.0 + |sequenceclassargs| + + link_preview_options (:class:`telegram.LinkPreviewOptions`, optional): Options used for + link preview generation for the message, if it is a text message and link preview + options were changed. + + .. versionadded:: 20.8 + + effect_id (:obj:`str`, optional): Unique identifier of the message effect added to the + message. + + .. versionadded:: 21.3 + + caption_entities (Sequence[:class:`telegram.MessageEntity`], optional): For messages with a + Caption. Special entities like usernames, URLs, bot commands, etc. that appear in the + caption. See :attr:`Message.parse_caption_entity` and :attr:`parse_caption_entities` + methods for how to use properly. This list is empty if the message does not contain + caption entities. + + .. versionchanged:: 20.0 + |sequenceclassargs| + + show_caption_above_media (:obj:`bool`, optional): |show_cap_above_med| + + .. versionadded:: 21.3 + audio (:class:`telegram.Audio`, optional): Message is an audio file, information + about the file. + document (:class:`telegram.Document`, optional): Message is a general file, information + about the file. + animation (:class:`telegram.Animation`, optional): Message is an animation, information + about the animation. For backward compatibility, when this field is set, the document + field will also be set. + game (:class:`telegram.Game`, optional): Message is a game, information about the game. + :ref:`More about games >> `. + photo (Sequence[:class:`telegram.PhotoSize`], optional): Message is a photo, available + sizes of the photo. This list is empty if the message does not contain a photo. + + .. versionchanged:: 20.0 + |sequenceclassargs| + + sticker (:class:`telegram.Sticker`, optional): Message is a sticker, information + about the sticker. + story (:class:`telegram.Story`, optional): Message is a forwarded story. + + .. versionadded:: 20.5 + video (:class:`telegram.Video`, optional): Message is a video, information about the + video. + voice (:class:`telegram.Voice`, optional): Message is a voice message, information about + the file. + video_note (:class:`telegram.VideoNote`, optional): Message is a + `video note `_, information + about the video message. + new_chat_members (Sequence[:class:`telegram.User`], optional): New members that were added + to the group or supergroup and information about them (the bot itself may be one of + these members). This list is empty if the message does not contain new chat members. + + .. versionchanged:: 20.0 + |sequenceclassargs| + + caption (:obj:`str`, optional): Caption for the animation, audio, document, paid media, + photo, video + or voice, 0-:tg-const:`telegram.constants.MessageLimit.CAPTION_LENGTH` characters. + contact (:class:`telegram.Contact`, optional): Message is a shared contact, information + about the contact. + location (:class:`telegram.Location`, optional): Message is a shared location, information + about the location. + venue (:class:`telegram.Venue`, optional): Message is a venue, information about the + venue. For backward compatibility, when this field is set, the location field will + also be set. + left_chat_member (:class:`telegram.User`, optional): A member was removed from the group, + information about them (this member may be the bot itself). + new_chat_title (:obj:`str`, optional): A chat title was changed to this value. + new_chat_photo (Sequence[:class:`telegram.PhotoSize`], optional): A chat photo was changed + to this value. This list is empty if the message does not contain a new chat photo. + + .. versionchanged:: 20.0 + |sequenceclassargs| + + delete_chat_photo (:obj:`bool`, optional): Service message: The chat photo was deleted. + group_chat_created (:obj:`bool`, optional): Service message: The group has been created. + supergroup_chat_created (:obj:`bool`, optional): Service message: The supergroup has been + created. This field can't be received in a message coming through updates, because bot + can't be a member of a supergroup when it is created. It can only be found in + :attr:`reply_to_message` if someone replies to a very first message in a directly + created supergroup. + channel_chat_created (:obj:`bool`, optional): Service message: The channel has been + created. This field can't be received in a message coming through updates, because bot + can't be a member of a channel when it is created. It can only be found in + :attr:`reply_to_message` if someone replies to a very first message in a channel. + message_auto_delete_timer_changed (:class:`telegram.MessageAutoDeleteTimerChanged`, \ + optional): Service message: auto-delete timer settings changed in the chat. + + .. versionadded:: 13.4 + migrate_to_chat_id (:obj:`int`, optional): The group has been migrated to a supergroup + with the specified identifier. + migrate_from_chat_id (:obj:`int`, optional): The supergroup has been migrated from a group + with the specified identifier. + pinned_message (:class:`telegram.MaybeInaccessibleMessage`, optional): Specified message + was pinned. Note that the Message object in this field will not contain further + :attr:`reply_to_message` fields even if it is itself a reply. + + .. versionchanged:: 20.8 + This attribute now is either :class:`telegram.Message` or + :class:`telegram.InaccessibleMessage`. + invoice (:class:`telegram.Invoice`, optional): Message is an invoice for a payment, + information about the invoice. + :ref:`More about payments >> `. + successful_payment (:class:`telegram.SuccessfulPayment`, optional): Message is a service + message about a successful payment, information about the payment. + :ref:`More about payments >> `. + connected_website (:obj:`str`, optional): The domain name of the website on which the user + has logged in. + `More about Telegram Login >> `_. + author_signature (:obj:`str`, optional): Signature of the post author for messages in + channels, or the custom title of an anonymous group administrator. + passport_data (:class:`telegram.PassportData`, optional): Telegram Passport data. + poll (:class:`telegram.Poll`, optional): Message is a native poll, + information about the poll. + dice (:class:`telegram.Dice`, optional): Message is a dice with random value. + via_bot (:class:`telegram.User`, optional): Bot through which message was sent. + proximity_alert_triggered (:class:`telegram.ProximityAlertTriggered`, optional): Service + message. A user in the chat triggered another user's proximity alert while sharing + Live Location. + video_chat_scheduled (:class:`telegram.VideoChatScheduled`, optional): Service message: + video chat scheduled. + + .. versionadded:: 20.0 + video_chat_started (:class:`telegram.VideoChatStarted`, optional): Service message: video + chat started. + + .. versionadded:: 20.0 + video_chat_ended (:class:`telegram.VideoChatEnded`, optional): Service message: video chat + ended. + + .. versionadded:: 20.0 + video_chat_participants_invited (:class:`telegram.VideoChatParticipantsInvited` optional): + Service message: new participants invited to a video chat. + + .. versionadded:: 20.0 + web_app_data (:class:`telegram.WebAppData`, optional): Service message: data sent by a Web + App. + + .. versionadded:: 20.0 + reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): Inline keyboard attached + to the message. :paramref:`~telegram.InlineKeyboardButton.login_url` buttons are + represented as ordinary url buttons. + is_topic_message (:obj:`bool`, optional): :obj:`True`, if the message is sent to a forum + topic. + + .. versionadded:: 20.0 + message_thread_id (:obj:`int`, optional): Unique identifier of a message thread to which + the message belongs; for supergroups only. + + .. versionadded:: 20.0 + forum_topic_created (:class:`telegram.ForumTopicCreated`, optional): Service message: + forum topic created. + + .. versionadded:: 20.0 + forum_topic_closed (:class:`telegram.ForumTopicClosed`, optional): Service message: + forum topic closed. + + .. versionadded:: 20.0 + forum_topic_reopened (:class:`telegram.ForumTopicReopened`, optional): Service message: + forum topic reopened. + + .. versionadded:: 20.0 + forum_topic_edited (:class:`telegram.ForumTopicEdited`, optional): Service message: + forum topic edited. + + .. versionadded:: 20.0 + general_forum_topic_hidden (:class:`telegram.GeneralForumTopicHidden`, optional): + Service message: General forum topic hidden. + + .. versionadded:: 20.0 + general_forum_topic_unhidden (:class:`telegram.GeneralForumTopicUnhidden`, optional): + Service message: General forum topic unhidden. + + .. versionadded:: 20.0 + write_access_allowed (:class:`telegram.WriteAccessAllowed`, optional): Service message: + the user allowed the bot to write messages after adding it to the attachment or side + menu, launching a Web App from a link, or accepting an explicit request from a Web App + sent by the method + `requestWriteAccess `_. + + .. versionadded:: 20.0 + has_media_spoiler (:obj:`bool`, optional): :obj:`True`, if the message media is covered + by a spoiler animation. + + .. versionadded:: 20.0 + users_shared (:class:`telegram.UsersShared`, optional): Service message: users were shared + with the bot + + .. versionadded:: 20.8 + chat_shared (:class:`telegram.ChatShared`, optional):Service message: a chat was shared + with the bot. + + .. versionadded:: 20.1 + giveaway_created (:class:`telegram.GiveawayCreated`, optional): Service message: a + scheduled giveaway was created + + .. versionadded:: 20.8 + giveaway (:class:`telegram.Giveaway`, optional): The message is a scheduled giveaway + message + + .. versionadded:: 20.8 + giveaway_winners (:class:`telegram.GiveawayWinners`, optional): A giveaway with public + winners was completed + + .. versionadded:: 20.8 + giveaway_completed (:class:`telegram.GiveawayCompleted`, optional): Service message: a + giveaway without public winners was completed + + .. versionadded:: 20.8 + external_reply (:class:`telegram.ExternalReplyInfo`, optional): Information about the + message that is being replied to, which may come from another chat or forum topic. + + .. versionadded:: 20.8 + quote (:class:`telegram.TextQuote`, optional): For replies that quote part of the original + message, the quoted part of the message. + + .. versionadded:: 20.8 + forward_origin (:class:`telegram.MessageOrigin`, optional): Information about the original + message for forwarded messages + + .. versionadded:: 20.8 + reply_to_story (:class:`telegram.Story`, optional): For replies to a story, the original + story. + + .. versionadded:: 21.0 + boost_added (:class:`telegram.ChatBoostAdded`, optional): Service message: user boosted + the chat. + + .. versionadded:: 21.0 + sender_boost_count (:obj:`int`, optional): If the sender of the + message boosted the chat, the number of boosts added by the user. + + .. versionadded:: 21.0 + business_connection_id (:obj:`str`, optional): Unique identifier of the business connection + from which the message was received. If non-empty, the message belongs to a chat of the + corresponding business account that is independent from any potential bot chat which + might share the same identifier. + + .. versionadded:: 21.1 + + sender_business_bot (:class:`telegram.User`, optional): The bot that actually sent the + message on behalf of the business account. Available only for outgoing messages sent + on behalf of the connected business account. + + .. versionadded:: 21.1 + + chat_background_set (:class:`telegram.ChatBackground`, optional): Service message: chat + background set. + + .. versionadded:: 21.2 + paid_media (:class:`telegram.PaidMediaInfo`, optional): Message contains paid media; + information about the paid media. + + .. versionadded:: 21.4 + refunded_payment (:class:`telegram.RefundedPayment`, optional): Message is a service + message about a refunded payment, information about the payment. + + .. versionadded:: 21.4 + + Attributes: + message_id (:obj:`int`): Unique message identifier inside this chat. + from_user (:class:`telegram.User`): Optional. Sender of the message; may be empty for + messages sent to channels. For backward compatibility, if the message was sent on + behalf of a chat, the field contains a fake sender user in non-channel chats. + sender_chat (:class:`telegram.Chat`): Optional. Sender of the message when sent on behalf + of a chat. For example, the supergroup itself for messages sent by its anonymous + administrators or a linked channel for messages automatically forwarded to the + channel's discussion group. For backward compatibility, if the message was sent on + behalf of a chat, the field from contains a fake sender user in non-channel chats. + date (:class:`datetime.datetime`): Date the message was sent in Unix time. Converted to + :class:`datetime.datetime`. + + .. versionchanged:: 20.3 + |datetime_localization| + chat (:class:`telegram.Chat`): Conversation the message belongs to. + is_automatic_forward (:obj:`bool`): Optional. :obj:`True`, if the message is a channel + post that was automatically forwarded to the connected discussion group. + + .. versionadded:: 13.9 + reply_to_message (:class:`telegram.Message`): Optional. For replies, the original message. + Note that the Message object in this field will not contain further + ``reply_to_message`` fields even if it itself is a reply. + edit_date (:class:`datetime.datetime`): Optional. Date the message was last edited in Unix + time. Converted to :class:`datetime.datetime`. + + .. versionchanged:: 20.3 + |datetime_localization| + has_protected_content (:obj:`bool`): Optional. :obj:`True`, if the message can't be + forwarded. + + .. versionadded:: 13.9 + is_from_offline (:obj:`bool`): Optional. :obj:`True`, if the message was sent + by an implicit action, for example, as an away or a greeting business message, + or as a scheduled message. + + .. versionadded:: 21.1 + media_group_id (:obj:`str`): Optional. The unique identifier of a media message group this + message belongs to. + text (:obj:`str`): Optional. For text messages, the actual UTF-8 text of the message, + 0-:tg-const:`telegram.constants.MessageLimit.MAX_TEXT_LENGTH` characters. + entities (Tuple[:class:`telegram.MessageEntity`]): Optional. For text messages, special + entities like usernames, URLs, bot commands, etc. that appear in the text. See + :attr:`parse_entity` and :attr:`parse_entities` methods for how to use properly. + This list is empty if the message does not contain entities. + + .. versionchanged:: 20.0 + |tupleclassattrs| + + link_preview_options (:class:`telegram.LinkPreviewOptions`): Optional. Options used for + link preview generation for the message, if it is a text message and link preview + options were changed. + + .. versionadded:: 20.8 + + effect_id (:obj:`str`): Optional. Unique identifier of the message effect added to the + message. + + ..versionadded:: 21.3 + + caption_entities (Tuple[:class:`telegram.MessageEntity`]): Optional. For messages with a + Caption. Special entities like usernames, URLs, bot commands, etc. that appear in the + caption. See :attr:`Message.parse_caption_entity` and :attr:`parse_caption_entities` + methods for how to use properly. This list is empty if the message does not contain + caption entities. + + .. versionchanged:: 20.0 + |tupleclassattrs| + + show_caption_above_media (:obj:`bool`): Optional. |show_cap_above_med| + + .. versionadded:: 21.3 + audio (:class:`telegram.Audio`): Optional. Message is an audio file, information + about the file. + + .. seealso:: :wiki:`Working with Files and Media ` + document (:class:`telegram.Document`): Optional. Message is a general file, information + about the file. + + .. seealso:: :wiki:`Working with Files and Media ` + animation (:class:`telegram.Animation`): Optional. Message is an animation, information + about the animation. For backward compatibility, when this field is set, the document + field will also be set. + + .. seealso:: :wiki:`Working with Files and Media ` + game (:class:`telegram.Game`): Optional. Message is a game, information about the game. + :ref:`More about games >> `. + photo (Tuple[:class:`telegram.PhotoSize`]): Optional. Message is a photo, available + sizes of the photo. This list is empty if the message does not contain a photo. + + .. seealso:: :wiki:`Working with Files and Media ` + + .. versionchanged:: 20.0 + |tupleclassattrs| + + sticker (:class:`telegram.Sticker`): Optional. Message is a sticker, information + about the sticker. + + .. seealso:: :wiki:`Working with Files and Media ` + story (:class:`telegram.Story`): Optional. Message is a forwarded story. + + .. versionadded:: 20.5 + video (:class:`telegram.Video`): Optional. Message is a video, information about the + video. + + .. seealso:: :wiki:`Working with Files and Media ` + voice (:class:`telegram.Voice`): Optional. Message is a voice message, information about + the file. + + .. seealso:: :wiki:`Working with Files and Media ` + video_note (:class:`telegram.VideoNote`): Optional. Message is a + `video note `_, information + about the video message. + + .. seealso:: :wiki:`Working with Files and Media ` + new_chat_members (Tuple[:class:`telegram.User`]): Optional. New members that were added + to the group or supergroup and information about them (the bot itself may be one of + these members). This list is empty if the message does not contain new chat members. + + .. versionchanged:: 20.0 + |tupleclassattrs| + caption (:obj:`str`): Optional. Caption for the animation, audio, document, paid media, + photo, video + or voice, 0-:tg-const:`telegram.constants.MessageLimit.CAPTION_LENGTH` characters. + contact (:class:`telegram.Contact`): Optional. Message is a shared contact, information + about the contact. + location (:class:`telegram.Location`): Optional. Message is a shared location, information + about the location. + venue (:class:`telegram.Venue`): Optional. Message is a venue, information about the + venue. For backward compatibility, when this field is set, the location field will + also be set. + left_chat_member (:class:`telegram.User`): Optional. A member was removed from the group, + information about them (this member may be the bot itself). + new_chat_title (:obj:`str`): Optional. A chat title was changed to this value. + new_chat_photo (Tuple[:class:`telegram.PhotoSize`]): A chat photo was changed to + this value. This list is empty if the message does not contain a new chat photo. + + .. versionchanged:: 20.0 + |tupleclassattrs| + + delete_chat_photo (:obj:`bool`): Optional. Service message: The chat photo was deleted. + group_chat_created (:obj:`bool`): Optional. Service message: The group has been created. + supergroup_chat_created (:obj:`bool`): Optional. Service message: The supergroup has been + created. This field can't be received in a message coming through updates, because bot + can't be a member of a supergroup when it is created. It can only be found in + :attr:`reply_to_message` if someone replies to a very first message in a directly + created supergroup. + channel_chat_created (:obj:`bool`): Optional. Service message: The channel has been + created. This field can't be received in a message coming through updates, because bot + can't be a member of a channel when it is created. It can only be found in + :attr:`reply_to_message` if someone replies to a very first message in a channel. + message_auto_delete_timer_changed (:class:`telegram.MessageAutoDeleteTimerChanged`): + Optional. Service message: auto-delete timer settings changed in the chat. + + .. versionadded:: 13.4 + migrate_to_chat_id (:obj:`int`): Optional. The group has been migrated to a supergroup + with the specified identifier. + migrate_from_chat_id (:obj:`int`): Optional. The supergroup has been migrated from a group + with the specified identifier. + pinned_message (:class:`telegram.MaybeInaccessibleMessage`): Optional. Specified message + was pinned. Note that the Message object in this field will not contain further + :attr:`reply_to_message` fields even if it is itself a reply. + + .. versionchanged:: 20.8 + This attribute now is either :class:`telegram.Message` or + :class:`telegram.InaccessibleMessage`. + invoice (:class:`telegram.Invoice`): Optional. Message is an invoice for a payment, + information about the invoice. + :ref:`More about payments >> `. + successful_payment (:class:`telegram.SuccessfulPayment`): Optional. Message is a service + message about a successful payment, information about the payment. + :ref:`More about payments >> `. + connected_website (:obj:`str`): Optional. The domain name of the website on which the user + has logged in. + `More about Telegram Login >> `_. + author_signature (:obj:`str`): Optional. Signature of the post author for messages in + channels, or the custom title of an anonymous group administrator. + passport_data (:class:`telegram.PassportData`): Optional. Telegram Passport data. + + Examples: + :any:`Passport Bot ` + poll (:class:`telegram.Poll`): Optional. Message is a native poll, + information about the poll. + dice (:class:`telegram.Dice`): Optional. Message is a dice with random value. + via_bot (:class:`telegram.User`): Optional. Bot through which message was sent. + proximity_alert_triggered (:class:`telegram.ProximityAlertTriggered`): Optional. Service + message. A user in the chat triggered another user's proximity alert while sharing + Live Location. + video_chat_scheduled (:class:`telegram.VideoChatScheduled`): Optional. Service message: + video chat scheduled. + + .. versionadded:: 20.0 + video_chat_started (:class:`telegram.VideoChatStarted`): Optional. Service message: video + chat started. + + .. versionadded:: 20.0 + video_chat_ended (:class:`telegram.VideoChatEnded`): Optional. Service message: video chat + ended. + + .. versionadded:: 20.0 + video_chat_participants_invited (:class:`telegram.VideoChatParticipantsInvited`): Optional. + Service message: new participants invited to a video chat. + + .. versionadded:: 20.0 + web_app_data (:class:`telegram.WebAppData`): Optional. Service message: data sent by a Web + App. + + .. versionadded:: 20.0 + reply_markup (:class:`telegram.InlineKeyboardMarkup`): Optional. Inline keyboard attached + to the message. :paramref:`~telegram.InlineKeyboardButton.login_url` buttons are + represented as ordinary url buttons. + is_topic_message (:obj:`bool`): Optional. :obj:`True`, if the message is sent to a forum + topic. + + .. versionadded:: 20.0 + message_thread_id (:obj:`int`): Optional. Unique identifier of a message thread to which + the message belongs; for supergroups only. + + .. versionadded:: 20.0 + forum_topic_created (:class:`telegram.ForumTopicCreated`): Optional. Service message: + forum topic created. + + .. versionadded:: 20.0 + forum_topic_closed (:class:`telegram.ForumTopicClosed`): Optional. Service message: + forum topic closed. + + .. versionadded:: 20.0 + forum_topic_reopened (:class:`telegram.ForumTopicReopened`): Optional. Service message: + forum topic reopened. + + .. versionadded:: 20.0 + forum_topic_edited (:class:`telegram.ForumTopicEdited`): Optional. Service message: + forum topic edited. + + .. versionadded:: 20.0 + general_forum_topic_hidden (:class:`telegram.GeneralForumTopicHidden`): Optional. + Service message: General forum topic hidden. + + .. versionadded:: 20.0 + general_forum_topic_unhidden (:class:`telegram.GeneralForumTopicUnhidden`): Optional. + Service message: General forum topic unhidden. + + .. versionadded:: 20.0 + write_access_allowed (:class:`telegram.WriteAccessAllowed`): Optional. Service message: + the user allowed the bot added to the attachment menu to write messages. + + .. versionadded:: 20.0 + has_media_spoiler (:obj:`bool`): Optional. :obj:`True`, if the message media is covered + by a spoiler animation. + + .. versionadded:: 20.0 + users_shared (:class:`telegram.UsersShared`): Optional. Service message: users were shared + with the bot + + .. versionadded:: 20.8 + chat_shared (:class:`telegram.ChatShared`): Optional. Service message: a chat was shared + with the bot. + + .. versionadded:: 20.1 + giveaway_created (:class:`telegram.GiveawayCreated`): Optional. Service message: a + scheduled giveaway was created + + .. versionadded:: 20.8 + giveaway (:class:`telegram.Giveaway`): Optional. The message is a scheduled giveaway + message + + .. versionadded:: 20.8 + giveaway_winners (:class:`telegram.GiveawayWinners`): Optional. A giveaway with public + winners was completed + + .. versionadded:: 20.8 + giveaway_completed (:class:`telegram.GiveawayCompleted`): Optional. Service message: a + giveaway without public winners was completed + + .. versionadded:: 20.8 + external_reply (:class:`telegram.ExternalReplyInfo`): Optional. Information about the + message that is being replied to, which may come from another chat or forum topic. + + .. versionadded:: 20.8 + quote (:class:`telegram.TextQuote`): Optional. For replies that quote part of the original + message, the quoted part of the message. + + .. versionadded:: 20.8 + forward_origin (:class:`telegram.MessageOrigin`): Optional. Information about the original + message for forwarded messages + + .. versionadded:: 20.8 + reply_to_story (:class:`telegram.Story`): Optional. For replies to a story, the original + story. + + .. versionadded:: 21.0 + boost_added (:class:`telegram.ChatBoostAdded`): Optional. Service message: user boosted + the chat. + + .. versionadded:: 21.0 + sender_boost_count (:obj:`int`): Optional. If the sender of the + message boosted the chat, the number of boosts added by the user. + + .. versionadded:: 21.0 + + business_connection_id (:obj:`str`): Optional. Unique identifier of the business connection + from which the message was received. If non-empty, the message belongs to a chat of the + corresponding business account that is independent from any potential bot chat which + might share the same identifier. + + .. versionadded:: 21.1 + + sender_business_bot (:class:`telegram.User`): Optional. The bot that actually sent the + message on behalf of the business account. Available only for outgoing messages sent + on behalf of the connected business account. + + .. versionadded:: 21.1 + + chat_background_set (:class:`telegram.ChatBackground`): Optional. Service message: chat + background set + + .. versionadded:: 21.2 + paid_media (:class:`telegram.PaidMediaInfo`): Optional. Message contains paid media; + information about the paid media. + + .. versionadded:: 21.4 + refunded_payment (:class:`telegram.RefundedPayment`): Optional. Message is a service + message about a refunded payment, information about the payment. + + .. versionadded:: 21.4 + + .. |custom_emoji_no_md1_support| replace:: Since custom emoji entities are not supported by + :attr:`~telegram.constants.ParseMode.MARKDOWN`, this method now raises a + :exc:`ValueError` when encountering a custom emoji. + + .. |blockquote_no_md1_support| replace:: Since block quotation entities are not supported + by :attr:`~telegram.constants.ParseMode.MARKDOWN`, this method now raises a + :exc:`ValueError` when encountering a block quotation. + + .. |reply_same_thread| replace:: If :paramref:`message_thread_id` is not provided, + this will reply to the same thread (topic) of the original message. + """ + + # fmt: on + __slots__ = ( + "_effective_attachment", + "animation", + "audio", + "author_signature", + "boost_added", + "business_connection_id", + "caption", + "caption_entities", + "channel_chat_created", + "chat_background_set", + "chat_shared", + "connected_website", + "contact", + "delete_chat_photo", + "dice", + "document", + "edit_date", + "effect_id", + "entities", + "external_reply", + "forum_topic_closed", + "forum_topic_created", + "forum_topic_edited", + "forum_topic_reopened", + "forward_origin", + "from_user", + "game", + "general_forum_topic_hidden", + "general_forum_topic_unhidden", + "giveaway", + "giveaway_completed", + "giveaway_created", + "giveaway_winners", + "group_chat_created", + "has_media_spoiler", + "has_protected_content", + "invoice", + "is_automatic_forward", + "is_from_offline", + "is_topic_message", + "left_chat_member", + "link_preview_options", + "location", + "media_group_id", + "message_auto_delete_timer_changed", + "message_thread_id", + "migrate_from_chat_id", + "migrate_to_chat_id", + "new_chat_members", + "new_chat_photo", + "new_chat_title", + "paid_media", + "passport_data", + "photo", + "pinned_message", + "poll", + "proximity_alert_triggered", + "quote", + "refunded_payment", + "reply_markup", + "reply_to_message", + "reply_to_story", + "sender_boost_count", + "sender_business_bot", + "sender_chat", + "show_caption_above_media", + "sticker", + "story", + "successful_payment", + "supergroup_chat_created", + "text", + "users_shared", + "venue", + "via_bot", + "video", + "video_chat_ended", + "video_chat_participants_invited", + "video_chat_scheduled", + "video_chat_started", + "video_note", + "voice", + "web_app_data", + "write_access_allowed", + ) + + def __init__( + self, + message_id: int, + date: datetime.datetime, + chat: Chat, + from_user: Optional[User] = None, + reply_to_message: Optional["Message"] = None, + edit_date: Optional[datetime.datetime] = None, + text: Optional[str] = None, + entities: Optional[Sequence["MessageEntity"]] = None, + caption_entities: Optional[Sequence["MessageEntity"]] = None, + audio: Optional[Audio] = None, + document: Optional[Document] = None, + game: Optional[Game] = None, + photo: Optional[Sequence[PhotoSize]] = None, + sticker: Optional[Sticker] = None, + video: Optional[Video] = None, + voice: Optional[Voice] = None, + video_note: Optional[VideoNote] = None, + new_chat_members: Optional[Sequence[User]] = None, + caption: Optional[str] = None, + contact: Optional[Contact] = None, + location: Optional[Location] = None, + venue: Optional[Venue] = None, + left_chat_member: Optional[User] = None, + new_chat_title: Optional[str] = None, + new_chat_photo: Optional[Sequence[PhotoSize]] = None, + delete_chat_photo: Optional[bool] = None, + group_chat_created: Optional[bool] = None, + supergroup_chat_created: Optional[bool] = None, + channel_chat_created: Optional[bool] = None, + migrate_to_chat_id: Optional[int] = None, + migrate_from_chat_id: Optional[int] = None, + pinned_message: Optional[MaybeInaccessibleMessage] = None, + invoice: Optional[Invoice] = None, + successful_payment: Optional[SuccessfulPayment] = None, + author_signature: Optional[str] = None, + media_group_id: Optional[str] = None, + connected_website: Optional[str] = None, + animation: Optional[Animation] = None, + passport_data: Optional[PassportData] = None, + poll: Optional[Poll] = None, + reply_markup: Optional[InlineKeyboardMarkup] = None, + dice: Optional[Dice] = None, + via_bot: Optional[User] = None, + proximity_alert_triggered: Optional[ProximityAlertTriggered] = None, + sender_chat: Optional[Chat] = None, + video_chat_started: Optional[VideoChatStarted] = None, + video_chat_ended: Optional[VideoChatEnded] = None, + video_chat_participants_invited: Optional[VideoChatParticipantsInvited] = None, + message_auto_delete_timer_changed: Optional[MessageAutoDeleteTimerChanged] = None, + video_chat_scheduled: Optional[VideoChatScheduled] = None, + is_automatic_forward: Optional[bool] = None, + has_protected_content: Optional[bool] = None, + web_app_data: Optional[WebAppData] = None, + is_topic_message: Optional[bool] = None, + message_thread_id: Optional[int] = None, + forum_topic_created: Optional[ForumTopicCreated] = None, + forum_topic_closed: Optional[ForumTopicClosed] = None, + forum_topic_reopened: Optional[ForumTopicReopened] = None, + forum_topic_edited: Optional[ForumTopicEdited] = None, + general_forum_topic_hidden: Optional[GeneralForumTopicHidden] = None, + general_forum_topic_unhidden: Optional[GeneralForumTopicUnhidden] = None, + write_access_allowed: Optional[WriteAccessAllowed] = None, + has_media_spoiler: Optional[bool] = None, + chat_shared: Optional[ChatShared] = None, + story: Optional[Story] = None, + giveaway: Optional["Giveaway"] = None, + giveaway_completed: Optional["GiveawayCompleted"] = None, + giveaway_created: Optional["GiveawayCreated"] = None, + giveaway_winners: Optional["GiveawayWinners"] = None, + users_shared: Optional[UsersShared] = None, + link_preview_options: Optional[LinkPreviewOptions] = None, + external_reply: Optional["ExternalReplyInfo"] = None, + quote: Optional["TextQuote"] = None, + forward_origin: Optional["MessageOrigin"] = None, + reply_to_story: Optional[Story] = None, + boost_added: Optional[ChatBoostAdded] = None, + sender_boost_count: Optional[int] = None, + business_connection_id: Optional[str] = None, + sender_business_bot: Optional[User] = None, + is_from_offline: Optional[bool] = None, + chat_background_set: Optional[ChatBackground] = None, + effect_id: Optional[str] = None, + show_caption_above_media: Optional[bool] = None, + paid_media: Optional[PaidMediaInfo] = None, + refunded_payment: Optional[RefundedPayment] = None, + *, + api_kwargs: Optional[JSONDict] = None, + ): + super().__init__(chat=chat, message_id=message_id, date=date, api_kwargs=api_kwargs) + + with self._unfrozen(): + # Required + self.message_id: int = message_id + # Optionals + self.from_user: Optional[User] = from_user + self.sender_chat: Optional[Chat] = sender_chat + self.date: datetime.datetime = date + self.chat: Chat = chat + self.is_automatic_forward: Optional[bool] = is_automatic_forward + self.reply_to_message: Optional[Message] = reply_to_message + self.edit_date: Optional[datetime.datetime] = edit_date + self.has_protected_content: Optional[bool] = has_protected_content + self.text: Optional[str] = text + self.entities: Tuple[MessageEntity, ...] = parse_sequence_arg(entities) + self.caption_entities: Tuple[MessageEntity, ...] = parse_sequence_arg(caption_entities) + self.audio: Optional[Audio] = audio + self.game: Optional[Game] = game + self.document: Optional[Document] = document + self.photo: Tuple[PhotoSize, ...] = parse_sequence_arg(photo) + self.sticker: Optional[Sticker] = sticker + self.video: Optional[Video] = video + self.voice: Optional[Voice] = voice + self.video_note: Optional[VideoNote] = video_note + self.caption: Optional[str] = caption + self.contact: Optional[Contact] = contact + self.location: Optional[Location] = location + self.venue: Optional[Venue] = venue + self.new_chat_members: Tuple[User, ...] = parse_sequence_arg(new_chat_members) + self.left_chat_member: Optional[User] = left_chat_member + self.new_chat_title: Optional[str] = new_chat_title + self.new_chat_photo: Tuple[PhotoSize, ...] = parse_sequence_arg(new_chat_photo) + self.delete_chat_photo: Optional[bool] = bool(delete_chat_photo) + self.group_chat_created: Optional[bool] = bool(group_chat_created) + self.supergroup_chat_created: Optional[bool] = bool(supergroup_chat_created) + self.migrate_to_chat_id: Optional[int] = migrate_to_chat_id + self.migrate_from_chat_id: Optional[int] = migrate_from_chat_id + self.channel_chat_created: Optional[bool] = bool(channel_chat_created) + self.message_auto_delete_timer_changed: Optional[MessageAutoDeleteTimerChanged] = ( + message_auto_delete_timer_changed + ) + self.pinned_message: Optional[MaybeInaccessibleMessage] = pinned_message + self.invoice: Optional[Invoice] = invoice + self.successful_payment: Optional[SuccessfulPayment] = successful_payment + self.connected_website: Optional[str] = connected_website + self.author_signature: Optional[str] = author_signature + self.media_group_id: Optional[str] = media_group_id + self.animation: Optional[Animation] = animation + self.passport_data: Optional[PassportData] = passport_data + self.poll: Optional[Poll] = poll + self.dice: Optional[Dice] = dice + self.via_bot: Optional[User] = via_bot + self.proximity_alert_triggered: Optional[ProximityAlertTriggered] = ( + proximity_alert_triggered + ) + self.video_chat_scheduled: Optional[VideoChatScheduled] = video_chat_scheduled + self.video_chat_started: Optional[VideoChatStarted] = video_chat_started + self.video_chat_ended: Optional[VideoChatEnded] = video_chat_ended + self.video_chat_participants_invited: Optional[VideoChatParticipantsInvited] = ( + video_chat_participants_invited + ) + self.reply_markup: Optional[InlineKeyboardMarkup] = reply_markup + self.web_app_data: Optional[WebAppData] = web_app_data + self.is_topic_message: Optional[bool] = is_topic_message + self.message_thread_id: Optional[int] = message_thread_id + self.forum_topic_created: Optional[ForumTopicCreated] = forum_topic_created + self.forum_topic_closed: Optional[ForumTopicClosed] = forum_topic_closed + self.forum_topic_reopened: Optional[ForumTopicReopened] = forum_topic_reopened + self.forum_topic_edited: Optional[ForumTopicEdited] = forum_topic_edited + self.general_forum_topic_hidden: Optional[GeneralForumTopicHidden] = ( + general_forum_topic_hidden + ) + self.general_forum_topic_unhidden: Optional[GeneralForumTopicUnhidden] = ( + general_forum_topic_unhidden + ) + self.write_access_allowed: Optional[WriteAccessAllowed] = write_access_allowed + self.has_media_spoiler: Optional[bool] = has_media_spoiler + self.users_shared: Optional[UsersShared] = users_shared + self.chat_shared: Optional[ChatShared] = chat_shared + self.story: Optional[Story] = story + self.giveaway: Optional[Giveaway] = giveaway + self.giveaway_completed: Optional[GiveawayCompleted] = giveaway_completed + self.giveaway_created: Optional[GiveawayCreated] = giveaway_created + self.giveaway_winners: Optional[GiveawayWinners] = giveaway_winners + self.link_preview_options: Optional[LinkPreviewOptions] = link_preview_options + self.external_reply: Optional[ExternalReplyInfo] = external_reply + self.quote: Optional[TextQuote] = quote + self.forward_origin: Optional[MessageOrigin] = forward_origin + self.reply_to_story: Optional[Story] = reply_to_story + self.boost_added: Optional[ChatBoostAdded] = boost_added + self.sender_boost_count: Optional[int] = sender_boost_count + self.business_connection_id: Optional[str] = business_connection_id + self.sender_business_bot: Optional[User] = sender_business_bot + self.is_from_offline: Optional[bool] = is_from_offline + self.chat_background_set: Optional[ChatBackground] = chat_background_set + self.effect_id: Optional[str] = effect_id + self.show_caption_above_media: Optional[bool] = show_caption_above_media + self.paid_media: Optional[PaidMediaInfo] = paid_media + self.refunded_payment: Optional[RefundedPayment] = refunded_payment + + self._effective_attachment = DEFAULT_NONE + + self._id_attrs = (self.message_id, self.chat) + + @property + def chat_id(self) -> int: + """:obj:`int`: Shortcut for :attr:`telegram.Chat.id` for :attr:`chat`.""" + return self.chat.id + + @property + def id(self) -> int: + """ + :obj:`int`: Shortcut for :attr:`message_id`. + + .. versionadded:: 20.0 + """ + return self.message_id + + @property + def link(self) -> Optional[str]: + """:obj:`str`: Convenience property. If the chat of the message is not + a private chat or normal group, returns a t.me link of the message. + + .. versionchanged:: 20.3 + For messages that are replies or part of a forum topic, the link now points + to the corresponding thread view. + """ + if self.chat.type not in [Chat.PRIVATE, Chat.GROUP]: + # the else block gets rid of leading -100 for supergroups: + to_link = self.chat.username if self.chat.username else f"c/{str(self.chat.id)[4:]}" + baselink = f"https://t.me/{to_link}/{self.message_id}" + + # adds the thread for topics and replies + if (self.is_topic_message and self.message_thread_id) or self.reply_to_message: + baselink = f"{baselink}?thread={self.message_thread_id}" + return baselink + return None + + @classmethod + def de_json(cls, data: Optional[JSONDict], bot: Optional["Bot"] = None) -> Optional["Message"]: + """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["from_user"] = User.de_json(data.pop("from", None), bot) + data["sender_chat"] = Chat.de_json(data.get("sender_chat"), bot) + data["entities"] = MessageEntity.de_list(data.get("entities"), bot) + data["caption_entities"] = MessageEntity.de_list(data.get("caption_entities"), bot) + data["reply_to_message"] = Message.de_json(data.get("reply_to_message"), bot) + data["edit_date"] = from_timestamp(data.get("edit_date"), tzinfo=loc_tzinfo) + data["audio"] = Audio.de_json(data.get("audio"), bot) + data["document"] = Document.de_json(data.get("document"), bot) + data["animation"] = Animation.de_json(data.get("animation"), bot) + data["game"] = Game.de_json(data.get("game"), bot) + data["photo"] = PhotoSize.de_list(data.get("photo"), bot) + data["sticker"] = Sticker.de_json(data.get("sticker"), bot) + data["story"] = Story.de_json(data.get("story"), bot) + data["video"] = Video.de_json(data.get("video"), bot) + data["voice"] = Voice.de_json(data.get("voice"), bot) + data["video_note"] = VideoNote.de_json(data.get("video_note"), bot) + data["contact"] = Contact.de_json(data.get("contact"), bot) + data["location"] = Location.de_json(data.get("location"), bot) + data["venue"] = Venue.de_json(data.get("venue"), bot) + data["new_chat_members"] = User.de_list(data.get("new_chat_members"), bot) + data["left_chat_member"] = User.de_json(data.get("left_chat_member"), bot) + data["new_chat_photo"] = PhotoSize.de_list(data.get("new_chat_photo"), bot) + data["message_auto_delete_timer_changed"] = MessageAutoDeleteTimerChanged.de_json( + data.get("message_auto_delete_timer_changed"), bot + ) + data["pinned_message"] = MaybeInaccessibleMessage.de_json(data.get("pinned_message"), bot) + data["invoice"] = Invoice.de_json(data.get("invoice"), bot) + data["successful_payment"] = SuccessfulPayment.de_json(data.get("successful_payment"), bot) + data["passport_data"] = PassportData.de_json(data.get("passport_data"), bot) + data["poll"] = Poll.de_json(data.get("poll"), bot) + data["dice"] = Dice.de_json(data.get("dice"), bot) + data["via_bot"] = User.de_json(data.get("via_bot"), bot) + data["proximity_alert_triggered"] = ProximityAlertTriggered.de_json( + data.get("proximity_alert_triggered"), bot + ) + data["reply_markup"] = InlineKeyboardMarkup.de_json(data.get("reply_markup"), bot) + data["video_chat_scheduled"] = VideoChatScheduled.de_json( + data.get("video_chat_scheduled"), bot + ) + data["video_chat_started"] = VideoChatStarted.de_json(data.get("video_chat_started"), bot) + data["video_chat_ended"] = VideoChatEnded.de_json(data.get("video_chat_ended"), bot) + data["video_chat_participants_invited"] = VideoChatParticipantsInvited.de_json( + data.get("video_chat_participants_invited"), bot + ) + data["web_app_data"] = WebAppData.de_json(data.get("web_app_data"), bot) + data["forum_topic_closed"] = ForumTopicClosed.de_json(data.get("forum_topic_closed"), bot) + data["forum_topic_created"] = ForumTopicCreated.de_json( + data.get("forum_topic_created"), bot + ) + data["forum_topic_reopened"] = ForumTopicReopened.de_json( + data.get("forum_topic_reopened"), bot + ) + data["forum_topic_edited"] = ForumTopicEdited.de_json(data.get("forum_topic_edited"), bot) + data["general_forum_topic_hidden"] = GeneralForumTopicHidden.de_json( + data.get("general_forum_topic_hidden"), bot + ) + data["general_forum_topic_unhidden"] = GeneralForumTopicUnhidden.de_json( + data.get("general_forum_topic_unhidden"), bot + ) + data["write_access_allowed"] = WriteAccessAllowed.de_json( + data.get("write_access_allowed"), bot + ) + data["users_shared"] = UsersShared.de_json(data.get("users_shared"), bot) + data["chat_shared"] = ChatShared.de_json(data.get("chat_shared"), bot) + data["chat_background_set"] = ChatBackground.de_json(data.get("chat_background_set"), bot) + data["paid_media"] = PaidMediaInfo.de_json(data.get("paid_media"), bot) + data["refunded_payment"] = RefundedPayment.de_json(data.get("refunded_payment"), bot) + + # Unfortunately, this needs to be here due to cyclic imports + from telegram._giveaway import ( # pylint: disable=import-outside-toplevel + Giveaway, + GiveawayCompleted, + GiveawayCreated, + GiveawayWinners, + ) + from telegram._messageorigin import ( # pylint: disable=import-outside-toplevel + MessageOrigin, + ) + from telegram._reply import ( # pylint: disable=import-outside-toplevel + ExternalReplyInfo, + TextQuote, + ) + + data["giveaway"] = Giveaway.de_json(data.get("giveaway"), bot) + data["giveaway_completed"] = GiveawayCompleted.de_json(data.get("giveaway_completed"), bot) + data["giveaway_created"] = GiveawayCreated.de_json(data.get("giveaway_created"), bot) + data["giveaway_winners"] = GiveawayWinners.de_json(data.get("giveaway_winners"), bot) + data["link_preview_options"] = LinkPreviewOptions.de_json( + data.get("link_preview_options"), bot + ) + data["external_reply"] = ExternalReplyInfo.de_json(data.get("external_reply"), bot) + data["quote"] = TextQuote.de_json(data.get("quote"), bot) + data["forward_origin"] = MessageOrigin.de_json(data.get("forward_origin"), bot) + data["reply_to_story"] = Story.de_json(data.get("reply_to_story"), bot) + data["boost_added"] = ChatBoostAdded.de_json(data.get("boost_added"), bot) + data["sender_business_bot"] = User.de_json(data.get("sender_business_bot"), bot) + + 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 + for key in ( + "user_shared", + "forward_from", + "forward_from_chat", + "forward_from_message_id", + "forward_signature", + "forward_sender_name", + "forward_date", + ): + if entry := data.get(key): + api_kwargs = {key: entry} + + return super()._de_json( # type: ignore[return-value] + data=data, bot=bot, api_kwargs=api_kwargs + ) + + @property + def effective_attachment( + self, + ) -> Union[ + Animation, + Audio, + Contact, + Dice, + Document, + Game, + Invoice, + Location, + PassportData, + Sequence[PhotoSize], + PaidMediaInfo, + Poll, + Sticker, + Story, + SuccessfulPayment, + Venue, + Video, + VideoNote, + Voice, + None, + ]: + """If the message is a user generated content which is not a plain text message, this + property is set to this content. It may be one of + + * :class:`telegram.Audio` + * :class:`telegram.Dice` + * :class:`telegram.Contact` + * :class:`telegram.Document` + * :class:`telegram.Animation` + * :class:`telegram.Game` + * :class:`telegram.Invoice` + * :class:`telegram.Location` + * :class:`telegram.PassportData` + * List[:class:`telegram.PhotoSize`] + * :class:`telegram.PaidMediaInfo` + * :class:`telegram.Poll` + * :class:`telegram.Sticker` + * :class:`telegram.Story` + * :class:`telegram.SuccessfulPayment` + * :class:`telegram.Venue` + * :class:`telegram.Video` + * :class:`telegram.VideoNote` + * :class:`telegram.Voice` + + Otherwise :obj:`None` is returned. + + .. seealso:: :wiki:`Working with Files and Media ` + + .. versionchanged:: 20.0 + :attr:`dice`, :attr:`passport_data` and :attr:`poll` are now also considered to be an + attachment. + + .. versionchanged:: 21.4 + :attr:`paid_media` is now also considered to be an attachment. + + .. deprecated:: 21.4 + :attr:`successful_payment` will be removed in future major versions. + + """ + if not isinstance(self._effective_attachment, DefaultValue): + return self._effective_attachment + + for attachment_type in MessageAttachmentType: + if self[attachment_type]: + self._effective_attachment = self[attachment_type] # type: ignore[assignment] + if attachment_type == MessageAttachmentType.SUCCESSFUL_PAYMENT: + warn( + PTBDeprecationWarning( + "21.4", + "successful_payment will no longer be considered an attachment in" + " future major versions", + ), + stacklevel=2, + ) + break + else: + self._effective_attachment = None + + return self._effective_attachment # type: ignore[return-value] + + def _quote( + self, quote: Optional[bool], reply_to_message_id: Optional[int] = None + ) -> Optional[ReplyParameters]: + """Modify kwargs for replying with or without quoting.""" + if reply_to_message_id is not None: + return ReplyParameters(reply_to_message_id) + + if quote is not None: + if quote: + return ReplyParameters(self.message_id) + + else: + # Unfortunately we need some ExtBot logic here because it's hard to move shortcut + # logic into ExtBot + if hasattr(self.get_bot(), "defaults") and self.get_bot().defaults: # type: ignore + default_quote = self.get_bot().defaults.quote # type: ignore[attr-defined] + else: + default_quote = None + if (default_quote is None and self.chat.type != Chat.PRIVATE) or default_quote: + return ReplyParameters(self.message_id) + + return None + + def compute_quote_position_and_entities( + self, quote: str, index: Optional[int] = None + ) -> Tuple[int, Optional[Tuple[MessageEntity, ...]]]: + """ + Use this function to compute position and entities of a quote in the message text or + caption. Useful for filling the parameters + :paramref:`~telegram.ReplyParameters.quote_position` and + :paramref:`~telegram.ReplyParameters.quote_entities` of :class:`telegram.ReplyParameters` + when replying to a message. + + Example: + + Given a message with the text ``"Hello, world! Hello, world!"``, the following code + will return the position and entities of the second occurrence of ``"Hello, world!"``. + + .. code-block:: python + + message.compute_quote_position_and_entities("Hello, world!", 1) + + .. versionadded:: 20.8 + + Args: + quote (:obj:`str`): Part of the message which is to be quoted. This is + expected to have plain text without formatting entities. + index (:obj:`int`, optional): 0-based index of the occurrence of the quote in the + message. If not specified, the first occurrence is used. + + Returns: + Tuple[:obj:`int`, :obj:`None` | Tuple[:class:`~telegram.MessageEntity`, ...]]: On + success, a tuple containing information about quote position and entities is returned. + + Raises: + RuntimeError: If the message has neither :attr:`text` nor :attr:`caption`. + ValueError: If the requested index of quote doesn't exist in the message. + """ + if not (text := (self.text or self.caption)): + raise RuntimeError("This message has neither text nor caption.") + + # Telegram wants the position in UTF-16 code units, so we have to calculate in that space + utf16_text = text.encode(TextEncoding.UTF_16_LE) + utf16_quote = quote.encode(TextEncoding.UTF_16_LE) + effective_index = index or 0 + + matches = list(re.finditer(re.escape(utf16_quote), utf16_text)) + if (length := len(matches)) < effective_index + 1: + raise ValueError( + f"You requested the {index}-th occurrence of '{quote}', but this text appears " + f"only {length} times." + ) + + position = len(utf16_text[: matches[effective_index].start()]) // 2 + length = len(utf16_quote) // 2 + end_position = position + length + + entities = [] + for entity in self.entities or self.caption_entities: + if position <= entity.offset + entity.length and entity.offset <= end_position: + # shift the offset by the position of the quote + offset = max(0, entity.offset - position) + # trim the entity length to the length of the overlap with the quote + e_length = min(end_position, entity.offset + entity.length) - max( + position, entity.offset + ) + if e_length <= 0: + continue + + # create a new entity with the correct offset and length + # looping over slots rather manually accessing the attributes + # is more future-proof + kwargs = {attr: getattr(entity, attr) for attr in entity.__slots__} + kwargs["offset"] = offset + kwargs["length"] = e_length + entities.append(MessageEntity(**kwargs)) + + return position, tuple(entities) or None + + def build_reply_arguments( + self, + quote: Optional[str] = None, + quote_index: Optional[int] = None, + target_chat_id: Optional[Union[int, str]] = None, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: ODVInput[int] = DEFAULT_NONE, + ) -> _ReplyKwargs: + """ + Builds a dictionary with the keys ``chat_id`` and ``reply_parameters``. This dictionary can + be used to reply to a message with the given quote and target chat. + + Examples: + + Usage with :meth:`telegram.Bot.send_message`: + + .. code-block:: python + + await bot.send_message( + text="This is a reply", + **message.build_reply_arguments(quote="Quoted Text") + ) + + Usage with :meth:`reply_text`, replying in the same chat: + + .. code-block:: python + + await message.reply_text( + "This is a reply", + do_quote=message.build_reply_arguments(quote="Quoted Text") + ) + + Usage with :meth:`reply_text`, replying in a different chat: + + .. code-block:: python + + await message.reply_text( + "This is a reply", + do_quote=message.build_reply_arguments( + quote="Quoted Text", + target_chat_id=-100123456789 + ) + ) + + .. versionadded:: 20.8 + + Args: + quote (:obj:`str`, optional): Passed in :meth:`compute_quote_position_and_entities` + as parameter :paramref:`~compute_quote_position_and_entities.quote` to compute + quote entities. Defaults to :obj:`None`. + quote_index (:obj:`int`, optional): Passed in + :meth:`compute_quote_position_and_entities` as parameter + :paramref:`~compute_quote_position_and_entities.quote_index` to compute quote + position. Defaults to :obj:`None`. + target_chat_id (:obj:`int` | :obj:`str`, optional): |chat_id_channel| + Defaults to :attr:`chat_id`. + allow_sending_without_reply (:obj:`bool`, optional): |allow_sending_without_reply| + Will be applied only if the reply happens in the same chat and forum topic. + message_thread_id (:obj:`int`, optional): |message_thread_id| + + Returns: + :obj:`dict`: + """ + target_chat_is_self = target_chat_id in (None, self.chat_id, f"@{self.chat.username}") + + if target_chat_is_self and message_thread_id in ( + None, + self.message_thread_id, + ): + # defaults handling will take place in `Bot._insert_defaults` + effective_aswr: ODVInput[bool] = allow_sending_without_reply + else: + effective_aswr = None + + quote_position, quote_entities = ( + self.compute_quote_position_and_entities(quote, quote_index) if quote else (None, None) + ) + return { # type: ignore[typeddict-item] + "reply_parameters": ReplyParameters( + chat_id=None if target_chat_is_self else self.chat_id, + message_id=self.message_id, + quote=quote, + quote_position=quote_position, + quote_entities=quote_entities, + allow_sending_without_reply=effective_aswr, + ), + "chat_id": target_chat_id or self.chat_id, + } + + async def _parse_quote_arguments( + self, + do_quote: Optional[Union[bool, _ReplyKwargs]], + quote: Optional[bool], + reply_to_message_id: Optional[int], + reply_parameters: Optional["ReplyParameters"], + ) -> Tuple[Union[str, int], ReplyParameters]: + if quote and do_quote: + raise ValueError("The arguments `quote` and `do_quote` are mutually exclusive") + + if reply_to_message_id is not None and reply_parameters is not None: + raise ValueError( + "`reply_to_message_id` and `reply_parameters` are mutually exclusive." + ) + + if quote is not None: + warn( + PTBDeprecationWarning( + "20.8", + "The `quote` parameter is deprecated in favor of the `do_quote` parameter. " + "Please update your code to use `do_quote` instead.", + ), + stacklevel=2, + ) + + effective_do_quote = quote or do_quote + chat_id: Union[str, int] = self.chat_id + + # reply_parameters and reply_to_message_id overrule the do_quote parameter + if reply_parameters is not None: + effective_reply_parameters = reply_parameters + elif reply_to_message_id is not None: + effective_reply_parameters = ReplyParameters(message_id=reply_to_message_id) + elif isinstance(effective_do_quote, dict): + effective_reply_parameters = effective_do_quote["reply_parameters"] + chat_id = effective_do_quote["chat_id"] + else: + effective_reply_parameters = self._quote(effective_do_quote) + + return chat_id, effective_reply_parameters + + def _parse_message_thread_id( + self, + chat_id: Union[str, int], + message_thread_id: ODVInput[int] = DEFAULT_NONE, + ) -> Optional[int]: + # values set by user have the highest priority + if not isinstance(message_thread_id, DefaultValue): + return message_thread_id + + # self.message_thread_id can be used for send_*.param.message_thread_id only if the + # thread is a forum topic. It does not work if the thread is a chain of replies to a + # message in a normal group. In that case, self.message_thread_id is just the message_id + # of the first message in the chain. + if not self.is_topic_message: + return None + + # Setting message_thread_id=self.message_thread_id only makes sense if we're replying in + # the same chat. + return self.message_thread_id if chat_id in {self.chat_id, self.chat.username} else None + + async def reply_text( + self, + text: str, + parse_mode: ODVInput[str] = DEFAULT_NONE, + disable_notification: ODVInput[bool] = DEFAULT_NONE, + reply_markup: Optional[ReplyMarkup] = None, + entities: Optional[Sequence["MessageEntity"]] = None, + protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: ODVInput[int] = DEFAULT_NONE, + link_preview_options: ODVInput["LinkPreviewOptions"] = DEFAULT_NONE, + reply_parameters: Optional["ReplyParameters"] = None, + message_effect_id: Optional[str] = None, + *, + reply_to_message_id: Optional[int] = None, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + disable_web_page_preview: Optional[bool] = None, + quote: Optional[bool] = None, + do_quote: Optional[Union[bool, _ReplyKwargs]] = None, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: Optional[JSONDict] = None, + ) -> "Message": + """Shortcut for:: + + await bot.send_message( + update.effective_message.chat_id, + message_thread_id=update.effective_message.message_thread_id, + business_connection_id=self.business_connection_id, + *args, + **kwargs, + ) + + For the documentation of the arguments, please see :meth:`telegram.Bot.send_message`. + + .. versionchanged:: 21.1 + |reply_same_thread| + + Keyword Args: + quote (:obj:`bool`, optional): |reply_quote| + + .. deprecated:: 20.8 + This argument is deprecated in favor of :paramref:`do_quote` + do_quote (:obj:`bool` | :obj:`dict`, optional): |do_quote| + Mutually exclusive with :paramref:`quote`. + + .. versionadded:: 20.8 + + Returns: + :class:`telegram.Message`: On success, instance representing the message posted. + + """ + chat_id, effective_reply_parameters = await self._parse_quote_arguments( + do_quote, quote, reply_to_message_id, reply_parameters + ) + message_thread_id = self._parse_message_thread_id(chat_id, message_thread_id) + return await self.get_bot().send_message( + chat_id=chat_id, + text=text, + parse_mode=parse_mode, + disable_web_page_preview=disable_web_page_preview, + link_preview_options=link_preview_options, + disable_notification=disable_notification, + reply_parameters=effective_reply_parameters, + reply_markup=reply_markup, + allow_sending_without_reply=allow_sending_without_reply, + entities=entities, + protect_content=protect_content, + message_thread_id=message_thread_id, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + business_connection_id=self.business_connection_id, + message_effect_id=message_effect_id, + ) + + async def reply_markdown( + self, + text: str, + disable_notification: ODVInput[bool] = DEFAULT_NONE, + reply_markup: Optional[ReplyMarkup] = None, + entities: Optional[Sequence["MessageEntity"]] = None, + protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: ODVInput[int] = DEFAULT_NONE, + link_preview_options: ODVInput["LinkPreviewOptions"] = DEFAULT_NONE, + reply_parameters: Optional["ReplyParameters"] = None, + message_effect_id: Optional[str] = None, + *, + reply_to_message_id: Optional[int] = None, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + disable_web_page_preview: Optional[bool] = None, + quote: Optional[bool] = None, + do_quote: Optional[Union[bool, _ReplyKwargs]] = None, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: Optional[JSONDict] = None, + ) -> "Message": + """Shortcut for:: + + await bot.send_message( + update.effective_message.chat_id, + message_thread_id=update.effective_message.message_thread_id, + parse_mode=ParseMode.MARKDOWN, + business_connection_id=self.business_connection_id, + *args, + **kwargs, + ) + + Sends a message with Markdown version 1 formatting. + + For the documentation of the arguments, please see :meth:`telegram.Bot.send_message`. + + .. versionchanged:: 21.1 + |reply_same_thread| + + Note: + :tg-const:`telegram.constants.ParseMode.MARKDOWN` is a legacy mode, retained by + Telegram for backward compatibility. You should use :meth:`reply_markdown_v2` instead. + + Keyword Args: + quote (:obj:`bool`, optional): |reply_quote| + + .. deprecated:: 20.8 + This argument is deprecated in favor of :paramref:`do_quote` + do_quote (:obj:`bool` | :obj:`dict`, optional): |do_quote| + Mutually exclusive with :paramref:`quote`. + + .. versionadded:: 20.8 + + Returns: + :class:`telegram.Message`: On success, instance representing the message posted. + """ + chat_id, effective_reply_parameters = await self._parse_quote_arguments( + do_quote, quote, reply_to_message_id, reply_parameters + ) + message_thread_id = self._parse_message_thread_id(chat_id, message_thread_id) + return await self.get_bot().send_message( + chat_id=chat_id, + text=text, + parse_mode=ParseMode.MARKDOWN, + disable_web_page_preview=disable_web_page_preview, + link_preview_options=link_preview_options, + disable_notification=disable_notification, + reply_parameters=effective_reply_parameters, + reply_markup=reply_markup, + allow_sending_without_reply=allow_sending_without_reply, + entities=entities, + protect_content=protect_content, + message_thread_id=message_thread_id, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + business_connection_id=self.business_connection_id, + message_effect_id=message_effect_id, + ) + + async def reply_markdown_v2( + self, + text: str, + disable_notification: ODVInput[bool] = DEFAULT_NONE, + reply_markup: Optional[ReplyMarkup] = None, + entities: Optional[Sequence["MessageEntity"]] = None, + protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: ODVInput[int] = DEFAULT_NONE, + link_preview_options: ODVInput["LinkPreviewOptions"] = DEFAULT_NONE, + reply_parameters: Optional["ReplyParameters"] = None, + message_effect_id: Optional[str] = None, + *, + reply_to_message_id: Optional[int] = None, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + disable_web_page_preview: Optional[bool] = None, + quote: Optional[bool] = None, + do_quote: Optional[Union[bool, _ReplyKwargs]] = None, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: Optional[JSONDict] = None, + ) -> "Message": + """Shortcut for:: + + await bot.send_message( + update.effective_message.chat_id, + message_thread_id=update.effective_message.message_thread_id, + parse_mode=ParseMode.MARKDOWN_V2, + business_connection_id=self.business_connection_id, + *args, + **kwargs, + ) + + Sends a message with markdown version 2 formatting. + + For the documentation of the arguments, please see :meth:`telegram.Bot.send_message`. + + .. versionchanged:: 21.1 + |reply_same_thread| + + Keyword Args: + quote (:obj:`bool`, optional): |reply_quote| + + .. deprecated:: 20.8 + This argument is deprecated in favor of :paramref:`do_quote` + do_quote (:obj:`bool` | :obj:`dict`, optional): |do_quote| + Mutually exclusive with :paramref:`quote`. + + .. versionadded:: 20.8 + + Returns: + :class:`telegram.Message`: On success, instance representing the message posted. + """ + chat_id, effective_reply_parameters = await self._parse_quote_arguments( + do_quote, quote, reply_to_message_id, reply_parameters + ) + message_thread_id = self._parse_message_thread_id(chat_id, message_thread_id) + return await self.get_bot().send_message( + chat_id=chat_id, + text=text, + parse_mode=ParseMode.MARKDOWN_V2, + disable_web_page_preview=disable_web_page_preview, + link_preview_options=link_preview_options, + disable_notification=disable_notification, + reply_parameters=effective_reply_parameters, + reply_markup=reply_markup, + allow_sending_without_reply=allow_sending_without_reply, + entities=entities, + protect_content=protect_content, + message_thread_id=message_thread_id, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + business_connection_id=self.business_connection_id, + message_effect_id=message_effect_id, + ) + + async def reply_html( + self, + text: str, + disable_notification: ODVInput[bool] = DEFAULT_NONE, + reply_markup: Optional[ReplyMarkup] = None, + entities: Optional[Sequence["MessageEntity"]] = None, + protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: ODVInput[int] = DEFAULT_NONE, + link_preview_options: ODVInput["LinkPreviewOptions"] = DEFAULT_NONE, + reply_parameters: Optional["ReplyParameters"] = None, + message_effect_id: Optional[str] = None, + *, + reply_to_message_id: Optional[int] = None, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + disable_web_page_preview: Optional[bool] = None, + quote: Optional[bool] = None, + do_quote: Optional[Union[bool, _ReplyKwargs]] = None, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: Optional[JSONDict] = None, + ) -> "Message": + """Shortcut for:: + + await bot.send_message( + update.effective_message.chat_id, + message_thread_id=update.effective_message.message_thread_id, + parse_mode=ParseMode.HTML, + business_connection_id=self.business_connection_id, + *args, + **kwargs, + ) + + Sends a message with HTML formatting. + + For the documentation of the arguments, please see :meth:`telegram.Bot.send_message`. + + .. versionchanged:: 21.1 + |reply_same_thread| + + Keyword Args: + quote (:obj:`bool`, optional): |reply_quote| + + .. deprecated:: 20.8 + This argument is deprecated in favor of :paramref:`do_quote` + do_quote (:obj:`bool` | :obj:`dict`, optional): |do_quote| + Mutually exclusive with :paramref:`quote`. + + .. versionadded:: 20.8 + + Returns: + :class:`telegram.Message`: On success, instance representing the message posted. + """ + chat_id, effective_reply_parameters = await self._parse_quote_arguments( + do_quote, quote, reply_to_message_id, reply_parameters + ) + message_thread_id = self._parse_message_thread_id(chat_id, message_thread_id) + return await self.get_bot().send_message( + chat_id=chat_id, + text=text, + parse_mode=ParseMode.HTML, + disable_web_page_preview=disable_web_page_preview, + link_preview_options=link_preview_options, + disable_notification=disable_notification, + reply_parameters=effective_reply_parameters, + reply_markup=reply_markup, + allow_sending_without_reply=allow_sending_without_reply, + entities=entities, + protect_content=protect_content, + message_thread_id=message_thread_id, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + business_connection_id=self.business_connection_id, + message_effect_id=message_effect_id, + ) + + async def reply_media_group( + self, + media: Sequence[ + Union["InputMediaAudio", "InputMediaDocument", "InputMediaPhoto", "InputMediaVideo"] + ], + disable_notification: ODVInput[bool] = DEFAULT_NONE, + protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: ODVInput[int] = DEFAULT_NONE, + reply_parameters: Optional["ReplyParameters"] = None, + message_effect_id: Optional[str] = None, + *, + reply_to_message_id: Optional[int] = None, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + quote: Optional[bool] = None, + do_quote: Optional[Union[bool, _ReplyKwargs]] = None, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: Optional[JSONDict] = None, + caption: Optional[str] = None, + parse_mode: ODVInput[str] = DEFAULT_NONE, + caption_entities: Optional[Sequence["MessageEntity"]] = None, + ) -> Tuple["Message", ...]: + """Shortcut for:: + + await bot.send_media_group( + update.effective_message.chat_id, + message_thread_id=update.effective_message.message_thread_id, + business_connection_id=self.business_connection_id, + *args, + **kwargs, + ) + + For the documentation of the arguments, please see :meth:`telegram.Bot.send_media_group`. + + .. versionchanged:: 21.1 + |reply_same_thread| + + Keyword Args: + quote (:obj:`bool`, optional): |reply_quote| + + .. deprecated:: 20.8 + This argument is deprecated in favor of :paramref:`do_quote` + do_quote (:obj:`bool` | :obj:`dict`, optional): |do_quote| + Mutually exclusive with :paramref:`quote`. + + .. versionadded:: 20.8 + + Returns: + Tuple[:class:`telegram.Message`]: An array of the sent Messages. + + Raises: + :class:`telegram.error.TelegramError` + """ + chat_id, effective_reply_parameters = await self._parse_quote_arguments( + do_quote, quote, reply_to_message_id, reply_parameters + ) + message_thread_id = self._parse_message_thread_id(chat_id, message_thread_id) + return await self.get_bot().send_media_group( + chat_id=chat_id, + media=media, + disable_notification=disable_notification, + reply_parameters=effective_reply_parameters, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + allow_sending_without_reply=allow_sending_without_reply, + protect_content=protect_content, + message_thread_id=message_thread_id, + caption=caption, + parse_mode=parse_mode, + caption_entities=caption_entities, + business_connection_id=self.business_connection_id, + message_effect_id=message_effect_id, + ) + + async def reply_photo( + self, + photo: Union[FileInput, "PhotoSize"], + caption: Optional[str] = None, + disable_notification: ODVInput[bool] = DEFAULT_NONE, + reply_markup: Optional[ReplyMarkup] = None, + parse_mode: ODVInput[str] = DEFAULT_NONE, + caption_entities: Optional[Sequence["MessageEntity"]] = None, + protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: ODVInput[int] = DEFAULT_NONE, + has_spoiler: Optional[bool] = None, + reply_parameters: Optional["ReplyParameters"] = None, + message_effect_id: Optional[str] = None, + show_caption_above_media: Optional[bool] = None, + *, + reply_to_message_id: Optional[int] = None, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + filename: Optional[str] = None, + quote: Optional[bool] = None, + do_quote: Optional[Union[bool, _ReplyKwargs]] = None, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: Optional[JSONDict] = None, + ) -> "Message": + """Shortcut for:: + + await bot.send_photo( + update.effective_message.chat_id, + message_thread_id=update.effective_message.message_thread_id, + business_connection_id=self.business_connection_id, + *args, + **kwargs, + ) + + For the documentation of the arguments, please see :meth:`telegram.Bot.send_photo`. + + .. versionchanged:: 21.1 + |reply_same_thread| + + Keyword Args: + quote (:obj:`bool`, optional): |reply_quote| + + .. deprecated:: 20.8 + This argument is deprecated in favor of :paramref:`do_quote` + do_quote (:obj:`bool` | :obj:`dict`, optional): |do_quote| + Mutually exclusive with :paramref:`quote`. + + .. versionadded:: 20.8 + + Returns: + :class:`telegram.Message`: On success, instance representing the message posted. + + """ + chat_id, effective_reply_parameters = await self._parse_quote_arguments( + do_quote, quote, reply_to_message_id, reply_parameters + ) + message_thread_id = self._parse_message_thread_id(chat_id, message_thread_id) + return await self.get_bot().send_photo( + chat_id=chat_id, + photo=photo, + caption=caption, + disable_notification=disable_notification, + reply_parameters=effective_reply_parameters, + reply_markup=reply_markup, + parse_mode=parse_mode, + allow_sending_without_reply=allow_sending_without_reply, + caption_entities=caption_entities, + filename=filename, + protect_content=protect_content, + message_thread_id=message_thread_id, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + has_spoiler=has_spoiler, + business_connection_id=self.business_connection_id, + message_effect_id=message_effect_id, + show_caption_above_media=show_caption_above_media, + ) + + async def reply_audio( + self, + audio: Union[FileInput, "Audio"], + duration: Optional[int] = None, + performer: Optional[str] = None, + title: Optional[str] = None, + caption: Optional[str] = None, + disable_notification: ODVInput[bool] = DEFAULT_NONE, + reply_markup: Optional[ReplyMarkup] = None, + parse_mode: ODVInput[str] = DEFAULT_NONE, + caption_entities: Optional[Sequence["MessageEntity"]] = None, + protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: ODVInput[int] = DEFAULT_NONE, + thumbnail: Optional[FileInput] = None, + reply_parameters: Optional["ReplyParameters"] = None, + message_effect_id: Optional[str] = None, + *, + reply_to_message_id: Optional[int] = None, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + filename: Optional[str] = None, + quote: Optional[bool] = None, + do_quote: Optional[Union[bool, _ReplyKwargs]] = None, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: Optional[JSONDict] = None, + ) -> "Message": + """Shortcut for:: + + await bot.send_audio( + update.effective_message.chat_id, + message_thread_id=update.effective_message.message_thread_id, + business_connection_id=self.business_connection_id, + *args, + **kwargs, + ) + + For the documentation of the arguments, please see :meth:`telegram.Bot.send_audio`. + + .. versionchanged:: 21.1 + |reply_same_thread| + + Keyword Args: + quote (:obj:`bool`, optional): |reply_quote| + + .. deprecated:: 20.8 + This argument is deprecated in favor of :paramref:`do_quote` + do_quote (:obj:`bool` | :obj:`dict`, optional): |do_quote| + Mutually exclusive with :paramref:`quote`. + + .. versionadded:: 20.8 + + Returns: + :class:`telegram.Message`: On success, instance representing the message posted. + + """ + chat_id, effective_reply_parameters = await self._parse_quote_arguments( + do_quote, quote, reply_to_message_id, reply_parameters + ) + message_thread_id = self._parse_message_thread_id(chat_id, message_thread_id) + return await self.get_bot().send_audio( + chat_id=chat_id, + audio=audio, + duration=duration, + performer=performer, + title=title, + caption=caption, + disable_notification=disable_notification, + reply_parameters=effective_reply_parameters, + reply_markup=reply_markup, + parse_mode=parse_mode, + allow_sending_without_reply=allow_sending_without_reply, + caption_entities=caption_entities, + filename=filename, + protect_content=protect_content, + message_thread_id=message_thread_id, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + thumbnail=thumbnail, + business_connection_id=self.business_connection_id, + message_effect_id=message_effect_id, + ) + + async def reply_document( + self, + document: Union[FileInput, "Document"], + caption: Optional[str] = None, + disable_notification: ODVInput[bool] = DEFAULT_NONE, + reply_markup: Optional[ReplyMarkup] = None, + parse_mode: ODVInput[str] = DEFAULT_NONE, + disable_content_type_detection: Optional[bool] = None, + caption_entities: Optional[Sequence["MessageEntity"]] = None, + protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: ODVInput[int] = DEFAULT_NONE, + thumbnail: Optional[FileInput] = None, + reply_parameters: Optional["ReplyParameters"] = None, + message_effect_id: Optional[str] = None, + *, + reply_to_message_id: Optional[int] = None, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + filename: Optional[str] = None, + quote: Optional[bool] = None, + do_quote: Optional[Union[bool, _ReplyKwargs]] = None, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: Optional[JSONDict] = None, + ) -> "Message": + """Shortcut for:: + + await bot.send_document( + update.effective_message.chat_id, + message_thread_id=update.effective_message.message_thread_id, + business_connection_id=self.business_connection_id, + *args, + **kwargs, + ) + + For the documentation of the arguments, please see :meth:`telegram.Bot.send_document`. + + .. versionchanged:: 21.1 + |reply_same_thread| + + Keyword Args: + quote (:obj:`bool`, optional): |reply_quote| + + .. deprecated:: 20.8 + This argument is deprecated in favor of :paramref:`do_quote` + do_quote (:obj:`bool` | :obj:`dict`, optional): |do_quote| + Mutually exclusive with :paramref:`quote`. + + .. versionadded:: 20.8 + + Returns: + :class:`telegram.Message`: On success, instance representing the message posted. + + """ + chat_id, effective_reply_parameters = await self._parse_quote_arguments( + do_quote, quote, reply_to_message_id, reply_parameters + ) + message_thread_id = self._parse_message_thread_id(chat_id, message_thread_id) + return await self.get_bot().send_document( + chat_id=chat_id, + document=document, + filename=filename, + caption=caption, + disable_notification=disable_notification, + reply_parameters=effective_reply_parameters, + reply_markup=reply_markup, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + parse_mode=parse_mode, + api_kwargs=api_kwargs, + disable_content_type_detection=disable_content_type_detection, + allow_sending_without_reply=allow_sending_without_reply, + caption_entities=caption_entities, + protect_content=protect_content, + message_thread_id=message_thread_id, + thumbnail=thumbnail, + business_connection_id=self.business_connection_id, + message_effect_id=message_effect_id, + ) + + async def reply_animation( + self, + animation: Union[FileInput, "Animation"], + duration: Optional[int] = None, + width: Optional[int] = None, + height: Optional[int] = None, + caption: Optional[str] = None, + parse_mode: ODVInput[str] = DEFAULT_NONE, + disable_notification: ODVInput[bool] = DEFAULT_NONE, + reply_markup: Optional[ReplyMarkup] = None, + caption_entities: Optional[Sequence["MessageEntity"]] = None, + protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: ODVInput[int] = DEFAULT_NONE, + has_spoiler: Optional[bool] = None, + thumbnail: Optional[FileInput] = None, + reply_parameters: Optional["ReplyParameters"] = None, + message_effect_id: Optional[str] = None, + show_caption_above_media: Optional[bool] = None, + *, + reply_to_message_id: Optional[int] = None, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + filename: Optional[str] = None, + quote: Optional[bool] = None, + do_quote: Optional[Union[bool, _ReplyKwargs]] = None, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: Optional[JSONDict] = None, + ) -> "Message": + """Shortcut for:: + + await bot.send_animation( + update.effective_message.chat_id, + message_thread_id=update.effective_message.message_thread_id, + business_connection_id=self.business_connection_id, + *args, + **kwargs, + ) + + For the documentation of the arguments, please see :meth:`telegram.Bot.send_animation`. + + .. versionchanged:: 21.1 + |reply_same_thread| + + Keyword Args: + quote (:obj:`bool`, optional): |reply_quote| + + .. deprecated:: 20.8 + This argument is deprecated in favor of :paramref:`do_quote` + do_quote (:obj:`bool` | :obj:`dict`, optional): |do_quote| + Mutually exclusive with :paramref:`quote`. + + .. versionadded:: 20.8 + + Returns: + :class:`telegram.Message`: On success, instance representing the message posted. + + """ + chat_id, effective_reply_parameters = await self._parse_quote_arguments( + do_quote, quote, reply_to_message_id, reply_parameters + ) + message_thread_id = self._parse_message_thread_id(chat_id, message_thread_id) + return await self.get_bot().send_animation( + chat_id=chat_id, + animation=animation, + duration=duration, + width=width, + height=height, + caption=caption, + parse_mode=parse_mode, + disable_notification=disable_notification, + reply_parameters=effective_reply_parameters, + reply_markup=reply_markup, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + allow_sending_without_reply=allow_sending_without_reply, + caption_entities=caption_entities, + filename=filename, + protect_content=protect_content, + message_thread_id=message_thread_id, + has_spoiler=has_spoiler, + thumbnail=thumbnail, + business_connection_id=self.business_connection_id, + message_effect_id=message_effect_id, + show_caption_above_media=show_caption_above_media, + ) + + async def reply_sticker( + self, + sticker: Union[FileInput, "Sticker"], + disable_notification: ODVInput[bool] = DEFAULT_NONE, + reply_markup: Optional[ReplyMarkup] = None, + protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: ODVInput[int] = DEFAULT_NONE, + emoji: Optional[str] = None, + reply_parameters: Optional["ReplyParameters"] = None, + message_effect_id: Optional[str] = None, + *, + reply_to_message_id: Optional[int] = None, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + quote: Optional[bool] = None, + do_quote: Optional[Union[bool, _ReplyKwargs]] = None, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: Optional[JSONDict] = None, + ) -> "Message": + """Shortcut for:: + + await bot.send_sticker( + update.effective_message.chat_id, + message_thread_id=update.effective_message.message_thread_id, + business_connection_id=self.business_connection_id, + *args, + **kwargs, + ) + + For the documentation of the arguments, please see :meth:`telegram.Bot.send_sticker`. + + .. versionchanged:: 21.1 + |reply_same_thread| + + Keyword Args: + quote (:obj:`bool`, optional): |reply_quote| + + .. deprecated:: 20.8 + This argument is deprecated in favor of :paramref:`do_quote` + do_quote (:obj:`bool` | :obj:`dict`, optional): |do_quote| + Mutually exclusive with :paramref:`quote`. + + .. versionadded:: 20.8 + + Returns: + :class:`telegram.Message`: On success, instance representing the message posted. + + """ + chat_id, effective_reply_parameters = await self._parse_quote_arguments( + do_quote, quote, reply_to_message_id, reply_parameters + ) + message_thread_id = self._parse_message_thread_id(chat_id, message_thread_id) + return await self.get_bot().send_sticker( + chat_id=chat_id, + sticker=sticker, + disable_notification=disable_notification, + reply_parameters=effective_reply_parameters, + reply_markup=reply_markup, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + allow_sending_without_reply=allow_sending_without_reply, + protect_content=protect_content, + message_thread_id=message_thread_id, + emoji=emoji, + business_connection_id=self.business_connection_id, + message_effect_id=message_effect_id, + ) + + async def reply_video( + self, + video: Union[FileInput, "Video"], + duration: Optional[int] = None, + caption: Optional[str] = None, + disable_notification: ODVInput[bool] = DEFAULT_NONE, + reply_markup: Optional[ReplyMarkup] = None, + width: Optional[int] = None, + height: Optional[int] = None, + parse_mode: ODVInput[str] = DEFAULT_NONE, + supports_streaming: Optional[bool] = None, + caption_entities: Optional[Sequence["MessageEntity"]] = None, + protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: ODVInput[int] = DEFAULT_NONE, + has_spoiler: Optional[bool] = None, + thumbnail: Optional[FileInput] = None, + reply_parameters: Optional["ReplyParameters"] = None, + message_effect_id: Optional[str] = None, + show_caption_above_media: Optional[bool] = None, + *, + reply_to_message_id: Optional[int] = None, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + filename: Optional[str] = None, + quote: Optional[bool] = None, + do_quote: Optional[Union[bool, _ReplyKwargs]] = None, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: Optional[JSONDict] = None, + ) -> "Message": + """Shortcut for:: + + await bot.send_video( + update.effective_message.chat_id, + message_thread_id=update.effective_message.message_thread_id, + business_connection_id=self.business_connection_id, + *args, + **kwargs, + ) + + For the documentation of the arguments, please see :meth:`telegram.Bot.send_video`. + + .. versionchanged:: 21.1 + |reply_same_thread| + + Keyword Args: + quote (:obj:`bool`, optional): |reply_quote| + + .. deprecated:: 20.8 + This argument is deprecated in favor of :paramref:`do_quote` + do_quote (:obj:`bool` | :obj:`dict`, optional): |do_quote| + Mutually exclusive with :paramref:`quote`. + + .. versionadded:: 20.8 + + Returns: + :class:`telegram.Message`: On success, instance representing the message posted. + + """ + chat_id, effective_reply_parameters = await self._parse_quote_arguments( + do_quote, quote, reply_to_message_id, reply_parameters + ) + message_thread_id = self._parse_message_thread_id(chat_id, message_thread_id) + return await self.get_bot().send_video( + chat_id=chat_id, + video=video, + duration=duration, + caption=caption, + disable_notification=disable_notification, + reply_parameters=effective_reply_parameters, + reply_markup=reply_markup, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + width=width, + height=height, + parse_mode=parse_mode, + supports_streaming=supports_streaming, + api_kwargs=api_kwargs, + allow_sending_without_reply=allow_sending_without_reply, + caption_entities=caption_entities, + filename=filename, + protect_content=protect_content, + message_thread_id=message_thread_id, + has_spoiler=has_spoiler, + thumbnail=thumbnail, + business_connection_id=self.business_connection_id, + message_effect_id=message_effect_id, + show_caption_above_media=show_caption_above_media, + ) + + async def reply_video_note( + self, + video_note: Union[FileInput, "VideoNote"], + duration: Optional[int] = None, + length: Optional[int] = None, + disable_notification: ODVInput[bool] = DEFAULT_NONE, + reply_markup: Optional[ReplyMarkup] = None, + protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: ODVInput[int] = DEFAULT_NONE, + thumbnail: Optional[FileInput] = None, + reply_parameters: Optional["ReplyParameters"] = None, + message_effect_id: Optional[str] = None, + *, + reply_to_message_id: Optional[int] = None, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + filename: Optional[str] = None, + quote: Optional[bool] = None, + do_quote: Optional[Union[bool, _ReplyKwargs]] = None, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: Optional[JSONDict] = None, + ) -> "Message": + """Shortcut for:: + + await bot.send_video_note( + update.effective_message.chat_id, + message_thread_id=update.effective_message.message_thread_id, + business_connection_id=self.business_connection_id, + *args, + **kwargs, + ) + + For the documentation of the arguments, please see :meth:`telegram.Bot.send_video_note`. + + .. versionchanged:: 21.1 + |reply_same_thread| + + Keyword Args: + quote (:obj:`bool`, optional): |reply_quote| + + .. deprecated:: 20.8 + This argument is deprecated in favor of :paramref:`do_quote` + do_quote (:obj:`bool` | :obj:`dict`, optional): |do_quote| + Mutually exclusive with :paramref:`quote`. + + .. versionadded:: 20.8 + + Returns: + :class:`telegram.Message`: On success, instance representing the message posted. + + """ + chat_id, effective_reply_parameters = await self._parse_quote_arguments( + do_quote, quote, reply_to_message_id, reply_parameters + ) + message_thread_id = self._parse_message_thread_id(chat_id, message_thread_id) + return await self.get_bot().send_video_note( + chat_id=chat_id, + video_note=video_note, + duration=duration, + length=length, + disable_notification=disable_notification, + reply_parameters=effective_reply_parameters, + reply_markup=reply_markup, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + allow_sending_without_reply=allow_sending_without_reply, + filename=filename, + protect_content=protect_content, + message_thread_id=message_thread_id, + thumbnail=thumbnail, + business_connection_id=self.business_connection_id, + message_effect_id=message_effect_id, + ) + + async def reply_voice( + self, + voice: Union[FileInput, "Voice"], + duration: Optional[int] = None, + caption: Optional[str] = None, + disable_notification: ODVInput[bool] = DEFAULT_NONE, + reply_markup: Optional[ReplyMarkup] = None, + parse_mode: ODVInput[str] = DEFAULT_NONE, + caption_entities: Optional[Sequence["MessageEntity"]] = None, + protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: ODVInput[int] = DEFAULT_NONE, + reply_parameters: Optional["ReplyParameters"] = None, + message_effect_id: Optional[str] = None, + *, + reply_to_message_id: Optional[int] = None, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + filename: Optional[str] = None, + quote: Optional[bool] = None, + do_quote: Optional[Union[bool, _ReplyKwargs]] = None, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: Optional[JSONDict] = None, + ) -> "Message": + """Shortcut for:: + + await bot.send_voice( + update.effective_message.chat_id, + message_thread_id=update.effective_message.message_thread_id, + business_connection_id=self.business_connection_id, + *args, + **kwargs, + ) + + For the documentation of the arguments, please see :meth:`telegram.Bot.send_voice`. + + .. versionchanged:: 21.1 + |reply_same_thread| + + Keyword Args: + quote (:obj:`bool`, optional): |reply_quote| + + .. deprecated:: 20.8 + This argument is deprecated in favor of :paramref:`do_quote` + do_quote (:obj:`bool` | :obj:`dict`, optional): |do_quote| + Mutually exclusive with :paramref:`quote`. + + .. versionadded:: 20.8 + + Returns: + :class:`telegram.Message`: On success, instance representing the message posted. + + """ + chat_id, effective_reply_parameters = await self._parse_quote_arguments( + do_quote, quote, reply_to_message_id, reply_parameters + ) + message_thread_id = self._parse_message_thread_id(chat_id, message_thread_id) + return await self.get_bot().send_voice( + chat_id=chat_id, + voice=voice, + duration=duration, + caption=caption, + disable_notification=disable_notification, + reply_parameters=effective_reply_parameters, + reply_markup=reply_markup, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + parse_mode=parse_mode, + api_kwargs=api_kwargs, + allow_sending_without_reply=allow_sending_without_reply, + caption_entities=caption_entities, + filename=filename, + protect_content=protect_content, + message_thread_id=message_thread_id, + business_connection_id=self.business_connection_id, + message_effect_id=message_effect_id, + ) + + async def reply_location( + self, + latitude: Optional[float] = None, + longitude: Optional[float] = None, + disable_notification: ODVInput[bool] = DEFAULT_NONE, + reply_markup: Optional[ReplyMarkup] = None, + live_period: Optional[int] = None, + horizontal_accuracy: Optional[float] = None, + heading: Optional[int] = None, + proximity_alert_radius: Optional[int] = None, + protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: ODVInput[int] = DEFAULT_NONE, + reply_parameters: Optional["ReplyParameters"] = None, + message_effect_id: Optional[str] = None, + *, + reply_to_message_id: Optional[int] = None, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + location: Optional[Location] = None, + quote: Optional[bool] = None, + do_quote: Optional[Union[bool, _ReplyKwargs]] = None, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: Optional[JSONDict] = None, + ) -> "Message": + """Shortcut for:: + + await bot.send_location( + update.effective_message.chat_id, + message_thread_id=update.effective_message.message_thread_id, + business_connection_id=self.business_connection_id, + *args, + **kwargs, + ) + + For the documentation of the arguments, please see :meth:`telegram.Bot.send_location`. + + .. versionchanged:: 21.1 + |reply_same_thread| + + Keyword Args: + quote (:obj:`bool`, optional): |reply_quote| + + .. deprecated:: 20.8 + This argument is deprecated in favor of :paramref:`do_quote` + do_quote (:obj:`bool` | :obj:`dict`, optional): |do_quote| + Mutually exclusive with :paramref:`quote`. + + .. versionadded:: 20.8 + + Returns: + :class:`telegram.Message`: On success, instance representing the message posted. + + """ + chat_id, effective_reply_parameters = await self._parse_quote_arguments( + do_quote, quote, reply_to_message_id, reply_parameters + ) + message_thread_id = self._parse_message_thread_id(chat_id, message_thread_id) + return await self.get_bot().send_location( + chat_id=chat_id, + latitude=latitude, + longitude=longitude, + disable_notification=disable_notification, + reply_parameters=effective_reply_parameters, + reply_markup=reply_markup, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + location=location, + live_period=live_period, + api_kwargs=api_kwargs, + horizontal_accuracy=horizontal_accuracy, + heading=heading, + proximity_alert_radius=proximity_alert_radius, + allow_sending_without_reply=allow_sending_without_reply, + protect_content=protect_content, + message_thread_id=message_thread_id, + business_connection_id=self.business_connection_id, + message_effect_id=message_effect_id, + ) + + async def reply_venue( + self, + latitude: Optional[float] = None, + longitude: Optional[float] = None, + title: Optional[str] = None, + address: Optional[str] = None, + foursquare_id: Optional[str] = None, + disable_notification: ODVInput[bool] = DEFAULT_NONE, + reply_markup: Optional[ReplyMarkup] = None, + foursquare_type: Optional[str] = None, + google_place_id: Optional[str] = None, + google_place_type: Optional[str] = None, + protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: ODVInput[int] = DEFAULT_NONE, + reply_parameters: Optional["ReplyParameters"] = None, + message_effect_id: Optional[str] = None, + *, + reply_to_message_id: Optional[int] = None, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + venue: Optional[Venue] = None, + quote: Optional[bool] = None, + do_quote: Optional[Union[bool, _ReplyKwargs]] = None, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: Optional[JSONDict] = None, + ) -> "Message": + """Shortcut for:: + + await bot.send_venue( + update.effective_message.chat_id, + message_thread_id=update.effective_message.message_thread_id, + business_connection_id=self.business_connection_id, + *args, + **kwargs, + ) + + For the documentation of the arguments, please see :meth:`telegram.Bot.send_venue`. + + .. versionchanged:: 21.1 + |reply_same_thread| + + Keyword Args: + quote (:obj:`bool`, optional): |reply_quote| + + .. deprecated:: 20.8 + This argument is deprecated in favor of :paramref:`do_quote` + do_quote (:obj:`bool` | :obj:`dict`, optional): |do_quote| + Mutually exclusive with :paramref:`quote`. + + .. versionadded:: 20.8 + + Returns: + :class:`telegram.Message`: On success, instance representing the message posted. + + """ + chat_id, effective_reply_parameters = await self._parse_quote_arguments( + do_quote, quote, reply_to_message_id, reply_parameters + ) + message_thread_id = self._parse_message_thread_id(chat_id, message_thread_id) + return await self.get_bot().send_venue( + chat_id=chat_id, + latitude=latitude, + longitude=longitude, + title=title, + address=address, + foursquare_id=foursquare_id, + disable_notification=disable_notification, + reply_parameters=effective_reply_parameters, + reply_markup=reply_markup, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + venue=venue, + foursquare_type=foursquare_type, + api_kwargs=api_kwargs, + google_place_id=google_place_id, + google_place_type=google_place_type, + allow_sending_without_reply=allow_sending_without_reply, + protect_content=protect_content, + message_thread_id=message_thread_id, + business_connection_id=self.business_connection_id, + message_effect_id=message_effect_id, + ) + + async def reply_contact( + self, + phone_number: Optional[str] = None, + first_name: Optional[str] = None, + last_name: Optional[str] = None, + disable_notification: ODVInput[bool] = DEFAULT_NONE, + reply_markup: Optional[ReplyMarkup] = None, + vcard: Optional[str] = None, + protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: ODVInput[int] = DEFAULT_NONE, + reply_parameters: Optional["ReplyParameters"] = None, + message_effect_id: Optional[str] = None, + *, + reply_to_message_id: Optional[int] = None, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + contact: Optional[Contact] = None, + quote: Optional[bool] = None, + do_quote: Optional[Union[bool, _ReplyKwargs]] = None, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: Optional[JSONDict] = None, + ) -> "Message": + """Shortcut for:: + + await bot.send_contact( + update.effective_message.chat_id, + message_thread_id=update.effective_message.message_thread_id, + business_connection_id=self.business_connection_id, + *args, + **kwargs, + ) + + For the documentation of the arguments, please see :meth:`telegram.Bot.send_contact`. + + .. versionchanged:: 21.1 + |reply_same_thread| + + Keyword Args: + quote (:obj:`bool`, optional): |reply_quote| + + .. deprecated:: 20.8 + This argument is deprecated in favor of :paramref:`do_quote` + do_quote (:obj:`bool` | :obj:`dict`, optional): |do_quote| + Mutually exclusive with :paramref:`quote`. + + .. versionadded:: 20.8 + + Returns: + :class:`telegram.Message`: On success, instance representing the message posted. + + """ + chat_id, effective_reply_parameters = await self._parse_quote_arguments( + do_quote, quote, reply_to_message_id, reply_parameters + ) + message_thread_id = self._parse_message_thread_id(chat_id, message_thread_id) + return await self.get_bot().send_contact( + chat_id=chat_id, + phone_number=phone_number, + first_name=first_name, + last_name=last_name, + disable_notification=disable_notification, + reply_parameters=effective_reply_parameters, + reply_markup=reply_markup, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + contact=contact, + vcard=vcard, + api_kwargs=api_kwargs, + allow_sending_without_reply=allow_sending_without_reply, + protect_content=protect_content, + message_thread_id=message_thread_id, + business_connection_id=self.business_connection_id, + message_effect_id=message_effect_id, + ) + + async def reply_poll( + self, + question: str, + options: Sequence[Union[str, "InputPollOption"]], + is_anonymous: Optional[bool] = None, + type: Optional[str] = None, # pylint: disable=redefined-builtin + allows_multiple_answers: Optional[bool] = None, + correct_option_id: Optional[CorrectOptionID] = None, + is_closed: Optional[bool] = None, + disable_notification: ODVInput[bool] = DEFAULT_NONE, + 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.datetime]] = None, + explanation_entities: Optional[Sequence["MessageEntity"]] = None, + protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: ODVInput[int] = DEFAULT_NONE, + reply_parameters: Optional["ReplyParameters"] = None, + question_parse_mode: ODVInput[str] = DEFAULT_NONE, + question_entities: Optional[Sequence["MessageEntity"]] = None, + message_effect_id: Optional[str] = None, + *, + reply_to_message_id: Optional[int] = None, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + quote: Optional[bool] = None, + do_quote: Optional[Union[bool, _ReplyKwargs]] = None, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: Optional[JSONDict] = None, + ) -> "Message": + """Shortcut for:: + + await bot.send_poll( + update.effective_message.chat_id, + message_thread_id=update.effective_message.message_thread_id, + business_connection_id=self.business_connection_id, + *args, + **kwargs, + ) + + For the documentation of the arguments, please see :meth:`telegram.Bot.send_poll`. + + .. versionchanged:: 21.1 + |reply_same_thread| + + Keyword Args: + quote (:obj:`bool`, optional): |reply_quote| + + .. deprecated:: 20.8 + This argument is deprecated in favor of :paramref:`do_quote` + do_quote (:obj:`bool` | :obj:`dict`, optional): |do_quote| + Mutually exclusive with :paramref:`quote`. + + .. versionadded:: 20.8 + + Returns: + :class:`telegram.Message`: On success, instance representing the message posted. + + """ + chat_id, effective_reply_parameters = await self._parse_quote_arguments( + do_quote, quote, reply_to_message_id, reply_parameters + ) + message_thread_id = self._parse_message_thread_id(chat_id, message_thread_id) + return await self.get_bot().send_poll( + chat_id=chat_id, + question=question, + options=options, + is_anonymous=is_anonymous, + type=type, + allows_multiple_answers=allows_multiple_answers, + correct_option_id=correct_option_id, + is_closed=is_closed, + disable_notification=disable_notification, + reply_parameters=effective_reply_parameters, + reply_markup=reply_markup, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + explanation=explanation, + explanation_parse_mode=explanation_parse_mode, + open_period=open_period, + close_date=close_date, + api_kwargs=api_kwargs, + allow_sending_without_reply=allow_sending_without_reply, + explanation_entities=explanation_entities, + protect_content=protect_content, + message_thread_id=message_thread_id, + business_connection_id=self.business_connection_id, + question_parse_mode=question_parse_mode, + question_entities=question_entities, + message_effect_id=message_effect_id, + ) + + async def reply_dice( + self, + disable_notification: ODVInput[bool] = DEFAULT_NONE, + reply_markup: Optional[ReplyMarkup] = None, + emoji: Optional[str] = None, + protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: ODVInput[int] = DEFAULT_NONE, + reply_parameters: Optional["ReplyParameters"] = None, + message_effect_id: Optional[str] = None, + *, + reply_to_message_id: Optional[int] = None, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + quote: Optional[bool] = None, + do_quote: Optional[Union[bool, _ReplyKwargs]] = None, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: Optional[JSONDict] = None, + ) -> "Message": + """Shortcut for:: + + await bot.send_dice( + update.effective_message.chat_id, + message_thread_id=update.effective_message.message_thread_id, + business_connection_id=self.business_connection_id, + *args, + **kwargs, + ) + + For the documentation of the arguments, please see :meth:`telegram.Bot.send_dice`. + + .. versionchanged:: 21.1 + |reply_same_thread| + + Keyword Args: + quote (:obj:`bool`, optional): |reply_quote| + + .. deprecated:: 20.8 + This argument is deprecated in favor of :paramref:`do_quote` + do_quote (:obj:`bool` | :obj:`dict`, optional): |do_quote| + Mutually exclusive with :paramref:`quote`. + + .. versionadded:: 20.8 + + Returns: + :class:`telegram.Message`: On success, instance representing the message posted. + + """ + chat_id, effective_reply_parameters = await self._parse_quote_arguments( + do_quote, quote, reply_to_message_id, reply_parameters + ) + message_thread_id = self._parse_message_thread_id(chat_id, message_thread_id) + return await self.get_bot().send_dice( + chat_id=chat_id, + disable_notification=disable_notification, + reply_parameters=effective_reply_parameters, + reply_markup=reply_markup, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + emoji=emoji, + api_kwargs=api_kwargs, + allow_sending_without_reply=allow_sending_without_reply, + protect_content=protect_content, + message_thread_id=message_thread_id, + business_connection_id=self.business_connection_id, + message_effect_id=message_effect_id, + ) + + async def reply_chat_action( + self, + action: str, + message_thread_id: ODVInput[int] = DEFAULT_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_chat_action( + update.effective_message.chat_id, + message_thread_id=update.effective_message.message_thread_id, + business_connection_id=self.business_connection_id, + *args, + **kwargs, + ) + + For the documentation of the arguments, please see :meth:`telegram.Bot.send_chat_action`. + + .. versionchanged:: 21.1 + |reply_same_thread| + + .. versionadded:: 13.2 + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + """ + return await self.get_bot().send_chat_action( + chat_id=self.chat_id, + message_thread_id=self._parse_message_thread_id(self.chat_id, message_thread_id), + action=action, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + business_connection_id=self.business_connection_id, + ) + + async def reply_game( + self, + game_short_name: str, + disable_notification: ODVInput[bool] = DEFAULT_NONE, + reply_markup: Optional["InlineKeyboardMarkup"] = None, + protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: ODVInput[int] = DEFAULT_NONE, + reply_parameters: Optional["ReplyParameters"] = None, + message_effect_id: Optional[str] = None, + *, + reply_to_message_id: Optional[int] = None, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + quote: Optional[bool] = None, + do_quote: Optional[Union[bool, _ReplyKwargs]] = None, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: Optional[JSONDict] = None, + ) -> "Message": + """Shortcut for:: + + await bot.send_game( + update.effective_message.chat_id, + message_thread_id=update.effective_message.message_thread_id, + business_connection_id=self.business_connection_id, + *args, + **kwargs, + ) + + For the documentation of the arguments, please see :meth:`telegram.Bot.send_game`. + + .. versionchanged:: 21.1 + |reply_same_thread| + + Keyword Args: + quote (:obj:`bool`, optional): |reply_quote| + + .. deprecated:: 20.8 + This argument is deprecated in favor of :paramref:`do_quote` + do_quote (:obj:`bool` | :obj:`dict`, optional): |do_quote| + Mutually exclusive with :paramref:`quote`. + + .. versionadded:: 20.8 + + .. versionadded:: 13.2 + + Returns: + :class:`telegram.Message`: On success, instance representing the message posted. + + """ + chat_id, effective_reply_parameters = await self._parse_quote_arguments( + do_quote, quote, reply_to_message_id, reply_parameters + ) + message_thread_id = self._parse_message_thread_id(chat_id, message_thread_id) + return await self.get_bot().send_game( + chat_id=chat_id, # type: ignore[arg-type] + game_short_name=game_short_name, + disable_notification=disable_notification, + reply_parameters=effective_reply_parameters, + reply_markup=reply_markup, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + allow_sending_without_reply=allow_sending_without_reply, + protect_content=protect_content, + message_thread_id=message_thread_id, + business_connection_id=self.business_connection_id, + message_effect_id=message_effect_id, + ) + + async def reply_invoice( + self, + title: str, + description: str, + payload: str, + provider_token: Optional[str], + currency: str, + prices: Sequence["LabeledPrice"], + start_parameter: Optional[str] = None, + photo_url: Optional[str] = None, + photo_size: Optional[int] = None, + photo_width: Optional[int] = None, + photo_height: Optional[int] = None, + need_name: Optional[bool] = None, + need_phone_number: Optional[bool] = None, + need_email: Optional[bool] = None, + need_shipping_address: Optional[bool] = None, + is_flexible: Optional[bool] = None, + disable_notification: ODVInput[bool] = DEFAULT_NONE, + reply_markup: Optional["InlineKeyboardMarkup"] = None, + provider_data: Optional[Union[str, object]] = None, + send_phone_number_to_provider: Optional[bool] = None, + send_email_to_provider: Optional[bool] = None, + max_tip_amount: Optional[int] = None, + suggested_tip_amounts: Optional[Sequence[int]] = None, + protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: ODVInput[int] = DEFAULT_NONE, + reply_parameters: Optional["ReplyParameters"] = None, + message_effect_id: Optional[str] = None, + *, + reply_to_message_id: Optional[int] = None, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + quote: Optional[bool] = None, + do_quote: Optional[Union[bool, _ReplyKwargs]] = None, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: Optional[JSONDict] = None, + ) -> "Message": + """Shortcut for:: + + await bot.send_invoice( + update.effective_message.chat_id, + message_thread_id=update.effective_message.message_thread_id, + *args, + **kwargs, + ) + + For the documentation of the arguments, please see :meth:`telegram.Bot.send_invoice`. + + .. versionchanged:: 21.1 + |reply_same_thread| + + Warning: + As of API 5.2 :paramref:`start_parameter ` + is an optional argument and therefore the + order of the arguments had to be changed. Use keyword arguments to make sure that the + arguments are passed correctly. + + .. versionadded:: 13.2 + + .. versionchanged:: 13.5 + As of Bot API 5.2, the parameter + :paramref:`start_parameter ` is optional. + + Keyword Args: + quote (:obj:`bool`, optional): |reply_quote| + + .. deprecated:: 20.8 + This argument is deprecated in favor of :paramref:`do_quote` + do_quote (:obj:`bool` | :obj:`dict`, optional): |do_quote| + Mutually exclusive with :paramref:`quote`. + + .. versionadded:: 20.8 + + Returns: + :class:`telegram.Message`: On success, instance representing the message posted. + + """ + chat_id, effective_reply_parameters = await self._parse_quote_arguments( + do_quote, quote, reply_to_message_id, reply_parameters + ) + message_thread_id = self._parse_message_thread_id(chat_id, message_thread_id) + return await self.get_bot().send_invoice( + chat_id=chat_id, + title=title, + description=description, + payload=payload, + provider_token=provider_token, + currency=currency, + prices=prices, + start_parameter=start_parameter, + photo_url=photo_url, + photo_size=photo_size, + photo_width=photo_width, + photo_height=photo_height, + need_name=need_name, + need_phone_number=need_phone_number, + need_email=need_email, + need_shipping_address=need_shipping_address, + is_flexible=is_flexible, + disable_notification=disable_notification, + reply_parameters=effective_reply_parameters, + reply_markup=reply_markup, + provider_data=provider_data, + send_phone_number_to_provider=send_phone_number_to_provider, + send_email_to_provider=send_email_to_provider, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + allow_sending_without_reply=allow_sending_without_reply, + max_tip_amount=max_tip_amount, + suggested_tip_amounts=suggested_tip_amounts, + protect_content=protect_content, + message_thread_id=message_thread_id, + message_effect_id=message_effect_id, + ) + + async def forward( + self, + chat_id: Union[int, str], + disable_notification: ODVInput[bool] = DEFAULT_NONE, + protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_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, + ) -> "Message": + """Shortcut for:: + + await bot.forward_message( + from_chat_id=update.effective_message.chat_id, + message_id=update.effective_message.message_id, + *args, + **kwargs + ) + + For the documentation of the arguments, please see :meth:`telegram.Bot.forward_message`. + + Note: + Since the release of Bot API 5.5 it can be impossible to forward messages from + some chats. Use the attributes :attr:`telegram.Message.has_protected_content` and + :attr:`telegram.ChatFullInfo.has_protected_content` to check this. + + As a workaround, it is still possible to use :meth:`copy`. However, this + behaviour is undocumented and might be changed by Telegram. + + Returns: + :class:`telegram.Message`: On success, instance representing the message forwarded. + + """ + return await self.get_bot().forward_message( + chat_id=chat_id, + from_chat_id=self.chat_id, + message_id=self.message_id, + disable_notification=disable_notification, + protect_content=protect_content, + message_thread_id=message_thread_id, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def copy( + self, + chat_id: Union[int, str], + caption: Optional[str] = None, + parse_mode: ODVInput[str] = DEFAULT_NONE, + caption_entities: Optional[Sequence["MessageEntity"]] = None, + disable_notification: ODVInput[bool] = DEFAULT_NONE, + reply_markup: Optional[ReplyMarkup] = None, + protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: Optional[int] = None, + reply_parameters: Optional["ReplyParameters"] = None, + show_caption_above_media: Optional[bool] = None, + *, + reply_to_message_id: Optional[int] = None, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_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, + ) -> "MessageId": + """Shortcut for:: + + await bot.copy_message( + chat_id=chat_id, + from_chat_id=update.effective_message.chat_id, + message_id=update.effective_message.message_id, + *args, + **kwargs + ) + + For the documentation of the arguments, please see :meth:`telegram.Bot.copy_message`. + + Returns: + :class:`telegram.MessageId`: On success, returns the MessageId of the sent message. + + """ + return await self.get_bot().copy_message( + chat_id=chat_id, + from_chat_id=self.chat_id, + message_id=self.message_id, + caption=caption, + parse_mode=parse_mode, + caption_entities=caption_entities, + disable_notification=disable_notification, + reply_to_message_id=reply_to_message_id, + reply_parameters=reply_parameters, + allow_sending_without_reply=allow_sending_without_reply, + reply_markup=reply_markup, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + protect_content=protect_content, + message_thread_id=message_thread_id, + show_caption_above_media=show_caption_above_media, + ) + + async def reply_copy( + self, + from_chat_id: Union[str, int], + message_id: int, + caption: Optional[str] = None, + parse_mode: ODVInput[str] = DEFAULT_NONE, + caption_entities: Optional[Sequence["MessageEntity"]] = None, + disable_notification: ODVInput[bool] = DEFAULT_NONE, + reply_markup: Optional[ReplyMarkup] = None, + protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: ODVInput[int] = DEFAULT_NONE, + reply_parameters: Optional["ReplyParameters"] = None, + show_caption_above_media: Optional[bool] = None, + *, + reply_to_message_id: Optional[int] = None, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + quote: Optional[bool] = None, + do_quote: Optional[Union[bool, _ReplyKwargs]] = None, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: Optional[JSONDict] = None, + ) -> "MessageId": + """Shortcut for:: + + await bot.copy_message( + chat_id=message.chat.id, + message_thread_id=update.effective_message.message_thread_id, + message_id=message_id, + *args, + **kwargs + ) + + For the documentation of the arguments, please see :meth:`telegram.Bot.copy_message`. + + .. versionchanged:: 21.1 + |reply_same_thread| + + Keyword Args: + quote (:obj:`bool`, optional): |reply_quote| + + .. versionadded:: 13.1 + .. deprecated:: 20.8 + This argument is deprecated in favor of :paramref:`do_quote` + do_quote (:obj:`bool` | :obj:`dict`, optional): |do_quote| + Mutually exclusive with :paramref:`quote`. + + .. versionadded:: 20.8 + + Returns: + :class:`telegram.MessageId`: On success, returns the MessageId of the sent message. + + """ + chat_id, effective_reply_parameters = await self._parse_quote_arguments( + do_quote, quote, reply_to_message_id, reply_parameters + ) + message_thread_id = self._parse_message_thread_id(chat_id, message_thread_id) + return await self.get_bot().copy_message( + chat_id=chat_id, + from_chat_id=from_chat_id, + message_id=message_id, + caption=caption, + parse_mode=parse_mode, + caption_entities=caption_entities, + disable_notification=disable_notification, + reply_parameters=effective_reply_parameters, + allow_sending_without_reply=allow_sending_without_reply, + reply_markup=reply_markup, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + protect_content=protect_content, + message_thread_id=message_thread_id, + show_caption_above_media=show_caption_above_media, + ) + + async def edit_text( + self, + text: str, + parse_mode: ODVInput[str] = DEFAULT_NONE, + reply_markup: Optional["InlineKeyboardMarkup"] = None, + entities: Optional[Sequence["MessageEntity"]] = None, + link_preview_options: ODVInput["LinkPreviewOptions"] = DEFAULT_NONE, + *, + disable_web_page_preview: 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, + ) -> Union["Message", bool]: + """Shortcut for:: + + await bot.edit_message_text( + chat_id=message.chat_id, + message_id=message.message_id, + business_connection_id=message.business_connection_id, + *args, **kwargs + ) + + For the documentation of the arguments, please see :meth:`telegram.Bot.edit_message_text`. + + Note: + You can only edit messages that the bot sent itself (i.e. of the ``bot.send_*`` family + of methods) or channel posts, if the bot is an admin in that channel. However, this + behaviour is undocumented and might be changed by Telegram. + + .. versionchanged:: 21.4 + Now also passes :attr:`business_connection_id`. + + Returns: + :class:`telegram.Message`: On success, if edited message is sent by the bot, the + edited Message is returned, otherwise ``True`` is returned. + + """ + return await self.get_bot().edit_message_text( + chat_id=self.chat_id, + message_id=self.message_id, + text=text, + parse_mode=parse_mode, + disable_web_page_preview=disable_web_page_preview, + link_preview_options=link_preview_options, + reply_markup=reply_markup, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + entities=entities, + inline_message_id=None, + business_connection_id=self.business_connection_id, + ) + + async def edit_caption( + self, + caption: Optional[str] = None, + reply_markup: Optional["InlineKeyboardMarkup"] = None, + parse_mode: ODVInput[str] = DEFAULT_NONE, + caption_entities: Optional[Sequence["MessageEntity"]] = None, + show_caption_above_media: 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, + ) -> Union["Message", bool]: + """Shortcut for:: + + await bot.edit_message_caption( + chat_id=message.chat_id, + message_id=message.message_id, + business_connection_id=message.business_connection_id, + *args, **kwargs + ) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.edit_message_caption`. + + Note: + You can only edit messages that the bot sent itself (i.e. of the ``bot.send_*`` family + of methods) or channel posts, if the bot is an admin in that channel. However, this + behaviour is undocumented and might be changed by Telegram. + + .. versionchanged:: 21.4 + Now also passes :attr:`business_connection_id`. + + Returns: + :class:`telegram.Message`: On success, if edited message is sent by the bot, the + edited Message is returned, otherwise ``True`` is returned. + + """ + return await self.get_bot().edit_message_caption( + chat_id=self.chat_id, + message_id=self.message_id, + caption=caption, + reply_markup=reply_markup, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + parse_mode=parse_mode, + api_kwargs=api_kwargs, + caption_entities=caption_entities, + inline_message_id=None, + show_caption_above_media=show_caption_above_media, + business_connection_id=self.business_connection_id, + ) + + async def edit_media( + self, + media: "InputMedia", + reply_markup: Optional["InlineKeyboardMarkup"] = 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, + ) -> Union["Message", bool]: + """Shortcut for:: + + await bot.edit_message_media( + chat_id=message.chat_id, + message_id=message.message_id, + business_connection_id=message.business_connection_id, + *args, **kwargs + ) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.edit_message_media`. + + Note: + You can only edit messages that the bot sent itself(i.e. of the ``bot.send_*`` family + of methods) or channel posts, if the bot is an admin in that channel. However, this + behaviour is undocumented and might be changed by Telegram. + + .. versionchanged:: 21.4 + Now also passes :attr:`business_connection_id`. + + Returns: + :class:`telegram.Message`: On success, if edited message is not an inline message, the + edited Message is returned, otherwise ``True`` is returned. + + """ + return await self.get_bot().edit_message_media( + media=media, + chat_id=self.chat_id, + message_id=self.message_id, + reply_markup=reply_markup, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + inline_message_id=None, + business_connection_id=self.business_connection_id, + ) + + async def edit_reply_markup( + self, + reply_markup: Optional["InlineKeyboardMarkup"] = 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, + ) -> Union["Message", bool]: + """Shortcut for:: + + await bot.edit_message_reply_markup( + chat_id=message.chat_id, + message_id=message.message_id, + business_connection_id=message.business_connection_id, + *args, **kwargs + ) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.edit_message_reply_markup`. + + Note: + You can only edit messages that the bot sent itself (i.e. of the ``bot.send_*`` family + of methods) or channel posts, if the bot is an admin in that channel. However, this + behaviour is undocumented and might be changed by Telegram. + + .. versionchanged:: 21.4 + Now also passes :attr:`business_connection_id`. + + Returns: + :class:`telegram.Message`: On success, if edited message is sent by the bot, the + edited Message is returned, otherwise ``True`` is returned. + """ + return await self.get_bot().edit_message_reply_markup( + chat_id=self.chat_id, + message_id=self.message_id, + reply_markup=reply_markup, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + inline_message_id=None, + business_connection_id=self.business_connection_id, + ) + + async def edit_live_location( + self, + latitude: Optional[float] = None, + longitude: Optional[float] = None, + reply_markup: Optional["InlineKeyboardMarkup"] = None, + horizontal_accuracy: Optional[float] = None, + heading: Optional[int] = None, + proximity_alert_radius: Optional[int] = None, + live_period: Optional[int] = None, + *, + location: Optional[Location] = 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, + ) -> Union["Message", bool]: + """Shortcut for:: + + await bot.edit_message_live_location( + chat_id=message.chat_id, + message_id=message.message_id, + business_connection_id=message.business_connection_id, + *args, **kwargs + ) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.edit_message_live_location`. + + Note: + You can only edit messages that the bot sent itself (i.e. of the ``bot.send_*`` family + of methods) or channel posts, if the bot is an admin in that channel. However, this + behaviour is undocumented and might be changed by Telegram. + + .. versionchanged:: 21.4 + Now also passes :attr:`business_connection_id`. + + Returns: + :class:`telegram.Message`: On success, if edited message is sent by the bot, the + edited Message is returned, otherwise :obj:`True` is returned. + """ + return await self.get_bot().edit_message_live_location( + chat_id=self.chat_id, + message_id=self.message_id, + latitude=latitude, + longitude=longitude, + location=location, + reply_markup=reply_markup, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + horizontal_accuracy=horizontal_accuracy, + heading=heading, + proximity_alert_radius=proximity_alert_radius, + live_period=live_period, + inline_message_id=None, + business_connection_id=self.business_connection_id, + ) + + async def stop_live_location( + self, + reply_markup: Optional["InlineKeyboardMarkup"] = 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, + ) -> Union["Message", bool]: + """Shortcut for:: + + await bot.stop_message_live_location( + chat_id=message.chat_id, + message_id=message.message_id, + business_connection_id=message.business_connection_id, + *args, **kwargs + ) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.stop_message_live_location`. + + Note: + You can only edit messages that the bot sent itself (i.e. of the ``bot.send_*`` family + of methods) or channel posts, if the bot is an admin in that channel. However, this + behaviour is undocumented and might be changed by Telegram. + + .. versionchanged:: 21.4 + Now also passes :attr:`business_connection_id`. + + Returns: + :class:`telegram.Message`: On success, if edited message is sent by the bot, the + edited Message is returned, otherwise :obj:`True` is returned. + """ + return await self.get_bot().stop_message_live_location( + chat_id=self.chat_id, + message_id=self.message_id, + reply_markup=reply_markup, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + inline_message_id=None, + business_connection_id=self.business_connection_id, + ) + + async def set_game_score( + self, + user_id: int, + score: int, + force: Optional[bool] = None, + disable_edit_message: 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, + ) -> Union["Message", bool]: + """Shortcut for:: + + await bot.set_game_score( + chat_id=message.chat_id, message_id=message.message_id, *args, **kwargs + ) + + For the documentation of the arguments, please see :meth:`telegram.Bot.set_game_score`. + + Note: + You can only edit messages that the bot sent itself (i.e. of the ``bot.send_*`` family + of methods) or channel posts, if the bot is an admin in that channel. However, this + behaviour is undocumented and might be changed by Telegram. + + Returns: + :class:`telegram.Message`: On success, if edited message is sent by the bot, the + edited Message is returned, otherwise :obj:`True` is returned. + """ + return await self.get_bot().set_game_score( + chat_id=self.chat_id, + message_id=self.message_id, + user_id=user_id, + score=score, + force=force, + disable_edit_message=disable_edit_message, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + inline_message_id=None, + ) + + async def get_game_high_scores( + 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, + ) -> Tuple["GameHighScore", ...]: + """Shortcut for:: + + await bot.get_game_high_scores( + chat_id=message.chat_id, message_id=message.message_id, *args, **kwargs + ) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.get_game_high_scores`. + + Note: + You can only edit messages that the bot sent itself (i.e. of the ``bot.send_*`` family + of methods) or channel posts, if the bot is an admin in that channel. However, this + behaviour is undocumented and might be changed by Telegram. + + Returns: + Tuple[:class:`telegram.GameHighScore`] + """ + return await self.get_bot().get_game_high_scores( + chat_id=self.chat_id, + message_id=self.message_id, + user_id=user_id, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + inline_message_id=None, + ) + + async def delete( + 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.delete_message( + chat_id=message.chat_id, message_id=message.message_id, *args, **kwargs + ) + + For the documentation of the arguments, please see :meth:`telegram.Bot.delete_message`. + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + """ + return await self.get_bot().delete_message( + chat_id=self.chat_id, + message_id=self.message_id, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def stop_poll( + self, + reply_markup: Optional["InlineKeyboardMarkup"] = 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, + ) -> Poll: + """Shortcut for:: + + await bot.stop_poll( + chat_id=message.chat_id, + message_id=message.message_id, + business_connection_id=message.business_connection_id, + *args, **kwargs + ) + + For the documentation of the arguments, please see :meth:`telegram.Bot.stop_poll`. + + .. versionchanged:: 21.4 + Now also passes :attr:`business_connection_id`. + + Returns: + :class:`telegram.Poll`: On success, the stopped Poll with the final results is + returned. + + """ + return await self.get_bot().stop_poll( + chat_id=self.chat_id, + message_id=self.message_id, + reply_markup=reply_markup, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + business_connection_id=self.business_connection_id, + ) + + async def pin( + self, + disable_notification: ODVInput[bool] = DEFAULT_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.pin_chat_message( + chat_id=message.chat_id, + message_id=message.message_id, + business_connection_id=message.business_connection_id, + *args, **kwargs + ) + + For the documentation of the arguments, please see :meth:`telegram.Bot.pin_chat_message`. + + .. versionchanged:: 21.5 + Now also passes :attr:`business_connection_id` to + :meth:`telegram.Bot.pin_chat_message`. + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + """ + return await self.get_bot().pin_chat_message( + chat_id=self.chat_id, + message_id=self.message_id, + business_connection_id=self.business_connection_id, + disable_notification=disable_notification, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def unpin( + 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.unpin_chat_message( + chat_id=message.chat_id, + message_id=message.message_id, + business_connection_id=message.business_connection_id, + *args, **kwargs + ) + + For the documentation of the arguments, please see :meth:`telegram.Bot.unpin_chat_message`. + + .. versionchanged:: 21.5 + Now also passes :attr:`business_connection_id` to + :meth:`telegram.Bot.pin_chat_message`. + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + """ + return await self.get_bot().unpin_chat_message( + chat_id=self.chat_id, + message_id=self.message_id, + business_connection_id=self.business_connection_id, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def edit_forum_topic( + self, + name: Optional[str] = None, + icon_custom_emoji_id: 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.edit_forum_topic( + chat_id=message.chat_id, message_thread_id=message.message_thread_id, *args, + **kwargs + ) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.edit_forum_topic`. + + .. versionadded:: 20.0 + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + """ + return await self.get_bot().edit_forum_topic( + chat_id=self.chat_id, + message_thread_id=self.message_thread_id, + name=name, + icon_custom_emoji_id=icon_custom_emoji_id, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def close_forum_topic( + 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.close_forum_topic( + chat_id=message.chat_id, message_thread_id=message.message_thread_id, *args, + **kwargs + ) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.close_forum_topic`. + + .. versionadded:: 20.0 + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + """ + return await self.get_bot().close_forum_topic( + chat_id=self.chat_id, + message_thread_id=self.message_thread_id, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def reopen_forum_topic( + 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.reopen_forum_topic( + chat_id=message.chat_id, message_thread_id=message.message_thread_id, *args, + **kwargs + ) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.reopen_forum_topic`. + + .. versionadded:: 20.0 + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + """ + return await self.get_bot().reopen_forum_topic( + chat_id=self.chat_id, + message_thread_id=self.message_thread_id, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def delete_forum_topic( + 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.delete_forum_topic( + chat_id=message.chat_id, message_thread_id=message.message_thread_id, *args, + **kwargs + ) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.delete_forum_topic`. + + .. versionadded:: 20.0 + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + """ + return await self.get_bot().delete_forum_topic( + chat_id=self.chat_id, + message_thread_id=self.message_thread_id, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def unpin_all_forum_topic_messages( + 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.unpin_all_forum_topic_messages( + chat_id=message.chat_id, message_thread_id=message.message_thread_id, *args, + **kwargs + ) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.unpin_all_forum_topic_messages`. + + .. versionadded:: 20.0 + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + """ + return await self.get_bot().unpin_all_forum_topic_messages( + chat_id=self.chat_id, + message_thread_id=self.message_thread_id, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def set_reaction( + self, + reaction: Optional[ + Union[Sequence["ReactionType"], "ReactionType", Sequence[str], str] + ] = None, + is_big: 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.set_message_reaction(chat_id=message.chat_id, message_id=message.message_id, + *args, **kwargs) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.set_message_reaction`. + + .. versionadded:: 20.8 + + Returns: + :obj:`bool` On success, :obj:`True` is returned. + """ + return await self.get_bot().set_message_reaction( + chat_id=self.chat_id, + message_id=self.message_id, + reaction=reaction, + is_big=is_big, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + def parse_entity(self, entity: MessageEntity) -> str: + """Returns the text from a given :class:`telegram.MessageEntity`. + + Note: + This method is present because Telegram calculates the offset and length in + UTF-16 codepoint pairs, which some versions of Python don't handle automatically. + (That is, you can't just slice ``Message.text`` with the offset and length.) + + Args: + entity (:class:`telegram.MessageEntity`): The entity to extract the text from. It must + be an entity that belongs to this message. + + Returns: + :obj:`str`: The text of the given entity. + + Raises: + RuntimeError: If the message has no text. + + """ + if not self.text: + raise RuntimeError("This Message has no 'text'.") + + return parse_message_entity(self.text, entity) + + def parse_caption_entity(self, entity: MessageEntity) -> str: + """Returns the text from a given :class:`telegram.MessageEntity`. + + Note: + This method is present because Telegram calculates the offset and length in + UTF-16 codepoint pairs, which some versions of Python don't handle automatically. + (That is, you can't just slice ``Message.caption`` with the offset and length.) + + Args: + entity (:class:`telegram.MessageEntity`): The entity to extract the text from. It must + be an entity that belongs to this message. + + Returns: + :obj:`str`: The text of the given entity. + + Raises: + RuntimeError: If the message has no caption. + + """ + if not self.caption: + raise RuntimeError("This Message has no 'caption'.") + + return parse_message_entity(self.caption, entity) + + def parse_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 + :attr:`telegram.MessageEntity.type` attribute as the key, and the text that each entity + belongs to as the value of the :obj:`dict`. + + Note: + This method should always be used instead of the :attr:`entities` attribute, since it + calculates the correct substring from the message text based on UTF-16 codepoints. + See :attr:`parse_entity` for more info. + + Args: + types (List[:obj:`str`], optional): List of :class:`telegram.MessageEntity` types as + strings. If the ``type`` attribute of an entity is contained in this list, it will + be returned. Defaults to a list of all types. All types can be found as constants + in :class:`telegram.MessageEntity`. + + Returns: + Dict[:class:`telegram.MessageEntity`, :obj:`str`]: A dictionary of entities mapped to + the text that belongs to them, calculated based on UTF-16 codepoints. + + """ + return parse_message_entities(self.text, self.entities, types=types) + + def parse_caption_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's caption filtered by their + :attr:`telegram.MessageEntity.type` attribute as the key, and the text that each entity + belongs to as the value of the :obj:`dict`. + + Note: + This method should always be used instead of the :attr:`caption_entities` attribute, + since it calculates the correct substring from the message text based on UTF-16 + codepoints. See :attr:`parse_entity` for more info. + + Args: + types (List[:obj:`str`], optional): List of :class:`telegram.MessageEntity` types as + strings. If the ``type`` attribute of an entity is contained in this list, it will + be returned. Defaults to a list of all types. All types can be found as constants + in :class:`telegram.MessageEntity`. + + Returns: + Dict[:class:`telegram.MessageEntity`, :obj:`str`]: A dictionary of entities mapped to + the text that belongs to them, calculated based on UTF-16 codepoints. + + """ + return parse_message_entities(self.caption, self.caption_entities, types=types) + + @classmethod + def _parse_html( + cls, + message_text: Optional[str], + entities: Dict[MessageEntity, str], + urled: bool = False, + offset: int = 0, + ) -> Optional[str]: + if message_text is None: + return None + + utf_16_text = message_text.encode(TextEncoding.UTF_16_LE) + html_text = "" + last_offset = 0 + + sorted_entities = sorted(entities.items(), key=lambda item: item[0].offset) + parsed_entities = [] + + for entity, text in sorted_entities: + if entity in parsed_entities: + continue + + nested_entities = { + e: t + for (e, t) in sorted_entities + if e.offset >= entity.offset + and e.offset + e.length <= entity.offset + entity.length + and e != entity + } + parsed_entities.extend(list(nested_entities.keys())) + + if nested_entities: + escaped_text = cls._parse_html( + text, nested_entities, urled=urled, offset=entity.offset + ) + else: + escaped_text = escape(text) + + if entity.type == MessageEntity.TEXT_LINK: + insert = f'{escaped_text}' + elif entity.type == MessageEntity.TEXT_MENTION and entity.user: + insert = f'{escaped_text}' + elif entity.type == MessageEntity.URL and urled: + insert = f'{escaped_text}' + elif entity.type == MessageEntity.BLOCKQUOTE: + insert = f"
{escaped_text}
" + elif entity.type == MessageEntity.EXPANDABLE_BLOCKQUOTE: + insert = f"
{escaped_text}
" + elif entity.type == MessageEntity.BOLD: + insert = f"{escaped_text}" + elif entity.type == MessageEntity.ITALIC: + insert = f"{escaped_text}" + elif entity.type == MessageEntity.CODE: + insert = f"{escaped_text}" + elif entity.type == MessageEntity.PRE: + if entity.language: + insert = f'
{escaped_text}
' + else: + insert = f"
{escaped_text}
" + elif entity.type == MessageEntity.UNDERLINE: + insert = f"{escaped_text}" + elif entity.type == MessageEntity.STRIKETHROUGH: + insert = f"{escaped_text}" + elif entity.type == MessageEntity.SPOILER: + insert = f'{escaped_text}' + elif entity.type == MessageEntity.CUSTOM_EMOJI: + insert = f'{escaped_text}' + else: + insert = escaped_text + + # Make sure to escape the text that is not part of the entity + # if we're in a nested entity, this is still required, since in that case this + # text is part of the parent entity + html_text += ( + escape( + utf_16_text[last_offset * 2 : (entity.offset - offset) * 2].decode( + TextEncoding.UTF_16_LE + ) + ) + + insert + ) + + last_offset = entity.offset - offset + entity.length + + # see comment above + html_text += escape(utf_16_text[last_offset * 2 :].decode(TextEncoding.UTF_16_LE)) + + return html_text + + @property + def text_html(self) -> str: + """Creates an HTML-formatted string from the markup entities found in the message. + + Use this if you want to retrieve the message text with the entities formatted as HTML in + the same way the original message was formatted. + + Warning: + |text_html| + + .. versionchanged:: 13.10 + Spoiler entities are now formatted as HTML. + + .. versionchanged:: 20.3 + Custom emoji entities are now supported. + + .. versionchanged:: 20.8 + Blockquote entities are now supported. + + Returns: + :obj:`str`: Message text with entities formatted as HTML. + + """ + return self._parse_html(self.text, self.parse_entities(), urled=False) + + @property + def text_html_urled(self) -> str: + """Creates an HTML-formatted string from the markup entities found in the message. + + Use this if you want to retrieve the message text with the entities formatted as HTML. + This also formats :attr:`telegram.MessageEntity.URL` as a hyperlink. + + Warning: + |text_html| + + .. versionchanged:: 13.10 + Spoiler entities are now formatted as HTML. + + .. versionchanged:: 20.3 + Custom emoji entities are now supported. + + .. versionchanged:: 20.8 + Blockquote entities are now supported. + + Returns: + :obj:`str`: Message text with entities formatted as HTML. + + """ + return self._parse_html(self.text, self.parse_entities(), urled=True) + + @property + def caption_html(self) -> str: + """Creates an HTML-formatted string from the markup entities found in the message's + caption. + + Use this if you want to retrieve the message caption with the caption entities formatted as + HTML in the same way the original message was formatted. + + Warning: + |text_html| + + .. versionchanged:: 13.10 + Spoiler entities are now formatted as HTML. + + .. versionchanged:: 20.3 + Custom emoji entities are now supported. + + .. versionchanged:: 20.8 + Blockquote entities are now supported. + + Returns: + :obj:`str`: Message caption with caption entities formatted as HTML. + """ + return self._parse_html(self.caption, self.parse_caption_entities(), urled=False) + + @property + def caption_html_urled(self) -> str: + """Creates an HTML-formatted string from the markup entities found in the message's + caption. + + Use this if you want to retrieve the message caption with the caption entities formatted as + HTML. This also formats :attr:`telegram.MessageEntity.URL` as a hyperlink. + + Warning: + |text_html| + + .. versionchanged:: 13.10 + Spoiler entities are now formatted as HTML. + + .. versionchanged:: 20.3 + Custom emoji entities are now supported. + + .. versionchanged:: 20.8 + Blockquote entities are now supported. + + Returns: + :obj:`str`: Message caption with caption entities formatted as HTML. + """ + return self._parse_html(self.caption, self.parse_caption_entities(), urled=True) + + @classmethod + def _parse_markdown( + cls, + message_text: Optional[str], + entities: Dict[MessageEntity, str], + urled: bool = False, + version: MarkdownVersion = 1, + offset: int = 0, + ) -> Optional[str]: + if version == 1: + for entity_type in ( + MessageEntity.EXPANDABLE_BLOCKQUOTE, + MessageEntity.BLOCKQUOTE, + MessageEntity.CUSTOM_EMOJI, + MessageEntity.SPOILER, + MessageEntity.STRIKETHROUGH, + MessageEntity.UNDERLINE, + ): + if any(entity.type == entity_type for entity in entities): + name = entity_type.name.title().replace("_", " ") # type:ignore[attr-defined] + raise ValueError(f"{name} entities are not supported for Markdown version 1") + + if message_text is None: + return None + + utf_16_text = message_text.encode(TextEncoding.UTF_16_LE) + markdown_text = "" + last_offset = 0 + + sorted_entities = sorted(entities.items(), key=lambda item: item[0].offset) + parsed_entities = [] + + for entity, text in sorted_entities: + if entity in parsed_entities: + continue + + nested_entities = { + e: t + for (e, t) in sorted_entities + if e.offset >= entity.offset + and e.offset + e.length <= entity.offset + entity.length + and e != entity + } + parsed_entities.extend(list(nested_entities.keys())) + + if nested_entities: + if version < 2: + raise ValueError("Nested entities are not supported for Markdown version 1") + + escaped_text = cls._parse_markdown( + text, + nested_entities, + urled=urled, + offset=entity.offset, + version=version, + ) + else: + escaped_text = escape_markdown(text, version=version) + + if entity.type == MessageEntity.TEXT_LINK: + if version == 1: + url = entity.url + else: + # Links need special escaping. Also can't have entities nested within + url = escape_markdown( + entity.url, version=version, entity_type=MessageEntity.TEXT_LINK + ) + insert = f"[{escaped_text}]({url})" + elif entity.type == MessageEntity.TEXT_MENTION and entity.user: + insert = f"[{escaped_text}](tg://user?id={entity.user.id})" + elif entity.type == MessageEntity.URL and urled: + link = text if version == 1 else escaped_text + insert = f"[{link}]({text})" + elif entity.type == MessageEntity.BOLD: + insert = f"*{escaped_text}*" + elif entity.type == MessageEntity.ITALIC: + insert = f"_{escaped_text}_" + elif entity.type == MessageEntity.CODE: + # Monospace needs special escaping. Also can't have entities nested within + insert = f"`{escape_markdown(text, version, MessageEntity.CODE)}`" + elif entity.type == MessageEntity.PRE: + # Monospace needs special escaping. Also can't have entities nested within + code = escape_markdown(text, version=version, entity_type=MessageEntity.PRE) + if entity.language: + prefix = f"```{entity.language}\n" + elif code.startswith("\\"): + prefix = "```" + else: + prefix = "```\n" + insert = f"{prefix}{code}```" + elif entity.type == MessageEntity.UNDERLINE: + insert = f"__{escaped_text}__" + elif entity.type == MessageEntity.STRIKETHROUGH: + insert = f"~{escaped_text}~" + elif entity.type == MessageEntity.SPOILER: + insert = f"||{escaped_text}||" + elif entity.type in (MessageEntity.BLOCKQUOTE, MessageEntity.EXPANDABLE_BLOCKQUOTE): + insert = ">" + "\n>".join(escaped_text.splitlines()) + if entity.type == MessageEntity.EXPANDABLE_BLOCKQUOTE: + insert = f"{insert}||" + elif entity.type == MessageEntity.CUSTOM_EMOJI: + # This should never be needed because ids are numeric but the documentation + # specifically mentions it so here we are + custom_emoji_id = escape_markdown( + entity.custom_emoji_id, + version=version, + entity_type=MessageEntity.CUSTOM_EMOJI, + ) + insert = f"![{escaped_text}](tg://emoji?id={custom_emoji_id})" + else: + insert = escaped_text + + # Make sure to escape the text that is not part of the entity + # if we're in a nested entity, this is still required, since in that case this + # text is part of the parent entity + markdown_text += ( + escape_markdown( + utf_16_text[last_offset * 2 : (entity.offset - offset) * 2].decode( + TextEncoding.UTF_16_LE + ), + version=version, + ) + + insert + ) + + last_offset = entity.offset - offset + entity.length + + # see comment above + markdown_text += escape_markdown( + utf_16_text[last_offset * 2 :].decode(TextEncoding.UTF_16_LE), + version=version, + ) + + return markdown_text + + @property + def text_markdown(self) -> str: + """Creates an Markdown-formatted string from the markup entities found in the message + using :class:`telegram.constants.ParseMode.MARKDOWN`. + + Use this if you want to retrieve the message text with the entities formatted as Markdown + in the same way the original message was formatted. + + Warning: + |text_markdown| + + Note: + :tg-const:`telegram.constants.ParseMode.MARKDOWN` is a legacy mode, retained by + Telegram for backward compatibility. You should use :meth:`text_markdown_v2` instead. + + .. versionchanged:: 20.5 + |custom_emoji_no_md1_support| + + .. versionchanged:: 20.8 + |blockquote_no_md1_support| + + Returns: + :obj:`str`: Message text with entities formatted as Markdown. + + Raises: + :exc:`ValueError`: If the message contains underline, strikethrough, spoiler, + blockquote or nested entities. + + """ + return self._parse_markdown(self.text, self.parse_entities(), urled=False) + + @property + def text_markdown_v2(self) -> str: + """Creates an Markdown-formatted string from the markup entities found in the message + using :class:`telegram.constants.ParseMode.MARKDOWN_V2`. + + Use this if you want to retrieve the message text with the entities formatted as Markdown + in the same way the original message was formatted. + + Warning: + |text_markdown| + + .. versionchanged:: 13.10 + Spoiler entities are now formatted as Markdown V2. + + .. versionchanged:: 20.3 + Custom emoji entities are now supported. + + .. versionchanged:: 20.8 + Blockquote entities are now supported. + + Returns: + :obj:`str`: Message text with entities formatted as Markdown. + """ + return self._parse_markdown(self.text, self.parse_entities(), urled=False, version=2) + + @property + def text_markdown_urled(self) -> str: + """Creates an Markdown-formatted string from the markup entities found in the message + using :class:`telegram.constants.ParseMode.MARKDOWN`. + + Use this if you want to retrieve the message text with the entities formatted as Markdown. + This also formats :attr:`telegram.MessageEntity.URL` as a hyperlink. + + Warning: + |text_markdown| + + Note: + :tg-const:`telegram.constants.ParseMode.MARKDOWN` is a legacy mode, retained by + Telegram for backward compatibility. You should use :meth:`text_markdown_v2_urled` + instead. + + .. versionchanged:: 20.5 + |custom_emoji_no_md1_support| + + .. versionchanged:: 20.8 + |blockquote_no_md1_support| + + Returns: + :obj:`str`: Message text with entities formatted as Markdown. + + Raises: + :exc:`ValueError`: If the message contains underline, strikethrough, spoiler, + blockquote or nested entities. + + """ + return self._parse_markdown(self.text, self.parse_entities(), urled=True) + + @property + def text_markdown_v2_urled(self) -> str: + """Creates an Markdown-formatted string from the markup entities found in the message + using :class:`telegram.constants.ParseMode.MARKDOWN_V2`. + + Use this if you want to retrieve the message text with the entities formatted as Markdown. + This also formats :attr:`telegram.MessageEntity.URL` as a hyperlink. + + Warning: + |text_markdown| + + .. versionchanged:: 13.10 + Spoiler entities are now formatted as Markdown V2. + + .. versionchanged:: 20.3 + Custom emoji entities are now supported. + + .. versionchanged:: 20.8 + Blockquote entities are now supported. + + Returns: + :obj:`str`: Message text with entities formatted as Markdown. + """ + return self._parse_markdown(self.text, self.parse_entities(), urled=True, version=2) + + @property + def caption_markdown(self) -> str: + """Creates an Markdown-formatted string from the markup entities found in the message's + caption using :class:`telegram.constants.ParseMode.MARKDOWN`. + + Use this if you want to retrieve the message caption with the caption entities formatted as + Markdown in the same way the original message was formatted. + + Warning: + |text_markdown| + + Note: + :tg-const:`telegram.constants.ParseMode.MARKDOWN` is a legacy mode, retained by + Telegram for backward compatibility. You should use :meth:`caption_markdown_v2` + .. versionchanged:: 20.5 + |custom_emoji_no_md1_support| + + .. versionchanged:: 20.8 + |blockquote_no_md1_support| + + Returns: + :obj:`str`: Message caption with caption entities formatted as Markdown. + + Raises: + :exc:`ValueError`: If the message contains underline, strikethrough, spoiler, + blockquote or nested entities. + + """ + return self._parse_markdown(self.caption, self.parse_caption_entities(), urled=False) + + @property + def caption_markdown_v2(self) -> str: + """Creates an Markdown-formatted string from the markup entities found in the message's + caption using :class:`telegram.constants.ParseMode.MARKDOWN_V2`. + + Use this if you want to retrieve the message caption with the caption entities formatted as + Markdown in the same way the original message was formatted. + + Warning: + |text_markdown| + + .. versionchanged:: 13.10 + Spoiler entities are now formatted as Markdown V2. + + .. versionchanged:: 20.3 + Custom emoji entities are now supported. + + .. versionchanged:: 20.8 + Blockquote entities are now supported. + + Returns: + :obj:`str`: Message caption with caption entities formatted as Markdown. + """ + return self._parse_markdown( + self.caption, self.parse_caption_entities(), urled=False, version=2 + ) + + @property + def caption_markdown_urled(self) -> str: + """Creates an Markdown-formatted string from the markup entities found in the message's + caption using :class:`telegram.constants.ParseMode.MARKDOWN`. + + Use this if you want to retrieve the message caption with the caption entities formatted as + Markdown. This also formats :attr:`telegram.MessageEntity.URL` as a hyperlink. + + Warning: + |text_markdown| + + Note: + :tg-const:`telegram.constants.ParseMode.MARKDOWN` is a legacy mode, retained by + Telegram for backward compatibility. You should use + :meth:`caption_markdown_v2_urled` instead. + + .. versionchanged:: 20.5 + |custom_emoji_no_md1_support| + + .. versionchanged:: 20.8 + |blockquote_no_md1_support| + + Returns: + :obj:`str`: Message caption with caption entities formatted as Markdown. + + Raises: + :exc:`ValueError`: If the message contains underline, strikethrough, spoiler, + blockquote or nested entities. + + """ + return self._parse_markdown(self.caption, self.parse_caption_entities(), urled=True) + + @property + def caption_markdown_v2_urled(self) -> str: + """Creates an Markdown-formatted string from the markup entities found in the message's + caption using :class:`telegram.constants.ParseMode.MARKDOWN_V2`. + + Use this if you want to retrieve the message caption with the caption entities formatted as + Markdown. This also formats :attr:`telegram.MessageEntity.URL` as a hyperlink. + + Warning: + |text_markdown| + + .. versionchanged:: 13.10 + Spoiler entities are now formatted as Markdown V2. + + .. versionchanged:: 20.3 + Custom emoji entities are now supported. + + .. versionchanged:: 20.8 + Blockquote entities are now supported. + + Returns: + :obj:`str`: Message caption with caption entities formatted as Markdown. + """ + return self._parse_markdown( + self.caption, self.parse_caption_entities(), urled=True, version=2 + ) diff --git a/_messageautodeletetimerchanged.py b/_messageautodeletetimerchanged.py new file mode 100644 index 0000000000000000000000000000000000000000..0d9f136d9f05ef3f8420aca1ccf99e312a92f3a0 --- /dev/null +++ b/_messageautodeletetimerchanged.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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 change in the Telegram message auto +deletion. +""" + +from typing import Optional + +from telegram._telegramobject import TelegramObject +from telegram._utils.types import JSONDict + + +class MessageAutoDeleteTimerChanged(TelegramObject): + """This object represents a service message about a change in auto-delete timer settings. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`message_auto_delete_time` is equal. + + .. versionadded:: 13.4 + + Args: + message_auto_delete_time (:obj:`int`): New auto-delete time for messages in the + chat. + + Attributes: + message_auto_delete_time (:obj:`int`): New auto-delete time for messages in the + chat. + + """ + + __slots__ = ("message_auto_delete_time",) + + def __init__( + self, + message_auto_delete_time: int, + *, + api_kwargs: Optional[JSONDict] = None, + ): + super().__init__(api_kwargs=api_kwargs) + self.message_auto_delete_time: int = message_auto_delete_time + + self._id_attrs = (self.message_auto_delete_time,) + + self._freeze() diff --git a/_messageentity.py b/_messageentity.py new file mode 100644 index 0000000000000000000000000000000000000000..ae675e8e9fdb80b9ec2965d8d938eed13df70e96 --- /dev/null +++ b/_messageentity.py @@ -0,0 +1,411 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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 MessageEntity.""" + +import copy +import itertools +from typing import TYPE_CHECKING, Dict, Final, List, Optional, Sequence, Tuple, Union + +from telegram import constants +from telegram._telegramobject import TelegramObject +from telegram._user import User +from telegram._utils import enum +from telegram._utils.strings import TextEncoding +from telegram._utils.types import JSONDict + +if TYPE_CHECKING: + from telegram import Bot + +_SEM = Sequence["MessageEntity"] + + +class MessageEntity(TelegramObject): + """ + This object represents one special entity in a text message. For example, hashtags, + usernames, URLs, etc. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`type`, :attr:`offset` and :attr:`length` are equal. + + Args: + type (:obj:`str`): Type of the entity. Can be :attr:`MENTION` (@username), + :attr:`HASHTAG` (#hashtag), :attr:`CASHTAG` ($USD), :attr:`BOT_COMMAND` + (/start@jobs_bot), :attr:`URL` (https://telegram.org), + :attr:`EMAIL` (do-not-reply@telegram.org), :attr:`PHONE_NUMBER` (+1-212-555-0123), + :attr:`BOLD` (**bold text**), :attr:`ITALIC` (*italic text*), :attr:`UNDERLINE` + (underlined text), :attr:`STRIKETHROUGH`, :attr:`SPOILER` (spoiler message), + :attr:`BLOCKQUOTE` (block quotation), :attr:`CODE` (monowidth string), :attr:`PRE` + (monowidth block), :attr:`TEXT_LINK` (for clickable text URLs), :attr:`TEXT_MENTION` + (for users without usernames), :attr:`CUSTOM_EMOJI` (for inline custom emoji stickers). + + .. versionadded:: 20.0 + Added inline custom emoji + + .. versionadded:: 20.8 + Added block quotation + offset (:obj:`int`): Offset in UTF-16 code units to the start of the entity. + length (:obj:`int`): Length of the entity in UTF-16 code units. + url (:obj:`str`, optional): For :attr:`TEXT_LINK` only, url that will be opened after + user taps on the text. + user (:class:`telegram.User`, optional): For :attr:`TEXT_MENTION` only, the mentioned + user. + language (:obj:`str`, optional): For :attr:`PRE` only, the programming language of + the entity text. + custom_emoji_id (:obj:`str`, optional): For :attr:`CUSTOM_EMOJI` only, unique identifier + of the custom emoji. Use :meth:`telegram.Bot.get_custom_emoji_stickers` to get full + information about the sticker. + + .. versionadded:: 20.0 + Attributes: + type (:obj:`str`): Type of the entity. Can be :attr:`MENTION` (@username), + :attr:`HASHTAG` (#hashtag), :attr:`CASHTAG` ($USD), :attr:`BOT_COMMAND` + (/start@jobs_bot), :attr:`URL` (https://telegram.org), + :attr:`EMAIL` (do-not-reply@telegram.org), :attr:`PHONE_NUMBER` (+1-212-555-0123), + :attr:`BOLD` (**bold text**), :attr:`ITALIC` (*italic text*), :attr:`UNDERLINE` + (underlined text), :attr:`STRIKETHROUGH`, :attr:`SPOILER` (spoiler message), + :attr:`BLOCKQUOTE` (block quotation), :attr:`CODE` (monowidth string), :attr:`PRE` + (monowidth block), :attr:`TEXT_LINK` (for clickable text URLs), :attr:`TEXT_MENTION` + (for users without usernames), :attr:`CUSTOM_EMOJI` (for inline custom emoji stickers). + + .. versionadded:: 20.0 + Added inline custom emoji + + .. versionadded:: 20.8 + Added block quotation + offset (:obj:`int`): Offset in UTF-16 code units to the start of the entity. + length (:obj:`int`): Length of the entity in UTF-16 code units. + url (:obj:`str`): Optional. For :attr:`TEXT_LINK` only, url that will be opened after + user taps on the text. + user (:class:`telegram.User`): Optional. For :attr:`TEXT_MENTION` only, the mentioned + user. + language (:obj:`str`): Optional. For :attr:`PRE` only, the programming language of + the entity text. + custom_emoji_id (:obj:`str`): Optional. For :attr:`CUSTOM_EMOJI` only, unique identifier + of the custom emoji. Use :meth:`telegram.Bot.get_custom_emoji_stickers` to get full + information about the sticker. + + .. versionadded:: 20.0 + + """ + + __slots__ = ("custom_emoji_id", "language", "length", "offset", "type", "url", "user") + + def __init__( + self, + type: str, # pylint: disable=redefined-builtin + offset: int, + length: int, + url: Optional[str] = None, + user: Optional[User] = None, + language: Optional[str] = None, + custom_emoji_id: Optional[str] = None, + *, + api_kwargs: Optional[JSONDict] = None, + ): + super().__init__(api_kwargs=api_kwargs) + # Required + self.type: str = enum.get_member(constants.MessageEntityType, type, type) + self.offset: int = offset + self.length: int = length + # Optionals + self.url: Optional[str] = url + self.user: Optional[User] = user + self.language: Optional[str] = language + self.custom_emoji_id: Optional[str] = custom_emoji_id + + self._id_attrs = (self.type, self.offset, self.length) + + self._freeze() + + @classmethod + def de_json( + cls, data: Optional[JSONDict], bot: Optional["Bot"] = None + ) -> Optional["MessageEntity"]: + """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) + + return super().de_json(data=data, bot=bot) + + @staticmethod + def adjust_message_entities_to_utf_16(text: str, entities: _SEM) -> _SEM: + """Utility functionality for converting the offset and length of entities from + Unicode (:obj:`str`) to UTF-16 (``utf-16-le`` encoded :obj:`bytes`). + + Tip: + Only the offsets and lengths calulated in UTF-16 is acceptable by the Telegram Bot API. + If they are calculated using the Unicode string (:obj:`str` object), errors may occur + when the text contains characters that are not in the Basic Multilingual Plane (BMP). + For more information, see `Unicode `_ and + `Plane (Unicode) `_. + + .. versionadded:: 21.4 + + Examples: + Below is a snippet of code that demonstrates how to use this function to convert + entities from Unicode to UTF-16 space. The ``unicode_entities`` are calculated in + Unicode and the `utf_16_entities` are calculated in UTF-16. + + .. code-block:: python + + text = "𠌕 bold 𝄢 italic underlined: 𝛙𝌢𑁍" + unicode_entities = [ + MessageEntity(offset=2, length=4, type=MessageEntity.BOLD), + MessageEntity(offset=9, length=6, type=MessageEntity.ITALIC), + MessageEntity(offset=28, length=3, type=MessageEntity.UNDERLINE), + ] + utf_16_entities = MessageEntity.adjust_message_entities_to_utf_16( + text, unicode_entities + ) + await bot.send_message( + chat_id=123, + text=text, + entities=utf_16_entities, + ) + # utf_16_entities[0]: offset=3, length=4 + # utf_16_entities[1]: offset=11, length=6 + # utf_16_entities[2]: offset=30, length=6 + + Args: + text (:obj:`str`): The text that the entities belong to + entities (Sequence[:class:`telegram.MessageEntity`]): Sequence of entities + with offset and length calculated in Unicode + + Returns: + Sequence[:class:`telegram.MessageEntity`]: Sequence of entities + with offset and length calculated in UTF-16 encoding + """ + # get sorted positions + positions = sorted(itertools.chain(*((x.offset, x.offset + x.length) for x in entities))) + accumulated_length = 0 + # calculate the length of each slice text[:position] in utf-16 accordingly, + # store the position translations + position_translation: Dict[int, int] = {} + for i, position in enumerate(positions): + last_position = positions[i - 1] if i > 0 else 0 + text_slice = text[last_position:position] + accumulated_length += len(text_slice.encode(TextEncoding.UTF_16_LE)) // 2 + position_translation[position] = accumulated_length + # get the final output entities + out = [] + for entity in entities: + translated_positions = position_translation[entity.offset] + translated_length = ( + position_translation[entity.offset + entity.length] - translated_positions + ) + new_entity = copy.copy(entity) + with new_entity._unfrozen(): + new_entity.offset = translated_positions + new_entity.length = translated_length + out.append(new_entity) + return out + + @staticmethod + def shift_entities(by: Union[str, int], entities: _SEM) -> _SEM: + """Utility functionality for shifting the offset of entities by a given amount. + + Examples: + Shifting by an integer amount: + + .. code-block:: python + + text = "Hello, world!" + entities = [ + MessageEntity(offset=0, length=5, type=MessageEntity.BOLD), + MessageEntity(offset=7, length=5, type=MessageEntity.ITALIC), + ] + shifted_entities = MessageEntity.shift_entities(1, entities) + await bot.send_message( + chat_id=123, + text="!" + text, + entities=shifted_entities, + ) + + Shifting using a string: + + .. code-block:: python + + text = "Hello, world!" + prefix = "𝄢" + entities = [ + MessageEntity(offset=0, length=5, type=MessageEntity.BOLD), + MessageEntity(offset=7, length=5, type=MessageEntity.ITALIC), + ] + shifted_entities = MessageEntity.shift_entities(prefix, entities) + await bot.send_message( + chat_id=123, + text=prefix + text, + entities=shifted_entities, + ) + + Tip: + The :paramref:`entities` are *not* modified in place. The function returns a sequence + of new objects. + + .. versionadded:: 21.5 + + Args: + by (:obj:`str` | :obj:`int`): Either the amount to shift the offset by or + a string whose length will be used as the amount to shift the offset by. In this + case, UTF-16 encoding will be used to calculate the length. + entities (Sequence[:class:`telegram.MessageEntity`]): Sequence of entities + + Returns: + Sequence[:class:`telegram.MessageEntity`]: Sequence of entities with the offset shifted + """ + effective_shift = by if isinstance(by, int) else len(by.encode("utf-16-le")) // 2 + + out = [] + for entity in entities: + new_entity = copy.copy(entity) + with new_entity._unfrozen(): + new_entity.offset += effective_shift + out.append(new_entity) + return out + + @classmethod + def concatenate( + cls, + *args: Union[Tuple[str, _SEM], Tuple[str, _SEM, bool]], + ) -> Tuple[str, _SEM]: + """Utility functionality for concatenating two text along with their formatting entities. + + Tip: + This function is useful for prefixing an already formatted text with a new text and its + formatting entities. In particular, it automatically correctly handles UTF-16 encoding. + + Examples: + This example shows a callback function that can be used to add a prefix and suffix to + the message in a :class:`~telegram.ext.CallbackQueryHandler`: + + .. code-block:: python + + async def prefix_message(update: Update, context: ContextTypes.DEFAULT_TYPE): + prefix = "𠌕 bold 𝄢 italic underlined: 𝛙𝌢𑁍 | " + prefix_entities = [ + MessageEntity(offset=2, length=4, type=MessageEntity.BOLD), + MessageEntity(offset=9, length=6, type=MessageEntity.ITALIC), + MessageEntity(offset=28, length=3, type=MessageEntity.UNDERLINE), + ] + suffix = " | 𠌕 bold 𝄢 italic underlined: 𝛙𝌢𑁍" + suffix_entities = [ + MessageEntity(offset=5, length=4, type=MessageEntity.BOLD), + MessageEntity(offset=12, length=6, type=MessageEntity.ITALIC), + MessageEntity(offset=31, length=3, type=MessageEntity.UNDERLINE), + ] + + message = update.effective_message + first = (prefix, prefix_entities, True) + second = (message.text, message.entities) + third = (suffix, suffix_entities, True) + + new_text, new_entities = MessageEntity.concatenate(first, second, third) + await update.callback_query.edit_message_text( + text=new_text, + entities=new_entities, + ) + + Hint: + The entities are *not* modified in place. The function returns a + new sequence of objects. + + .. versionadded:: 21.5 + + Args: + *args (Tuple[:obj:`str`, Sequence[:class:`telegram.MessageEntity`]] | \ + Tuple[:obj:`str`, Sequence[:class:`telegram.MessageEntity`], :obj:`bool`]): + Arbitrary number of tuples containing the text and its entities to concatenate. + If the last element of the tuple is a :obj:`bool`, it is used to determine whether + to adjust the entities to UTF-16 via + :meth:`adjust_message_entities_to_utf_16`. UTF-16 adjustment is disabled by + default. + + Returns: + Tuple[:obj:`str`, Sequence[:class:`telegram.MessageEntity`]]: The concatenated text + and its entities + """ + output_text = "" + output_entities: List[MessageEntity] = [] + for arg in args: + text, entities = arg[0], arg[1] + + if len(arg) > 2 and arg[2] is True: + entities = cls.adjust_message_entities_to_utf_16(text, entities) + + output_entities.extend(cls.shift_entities(output_text, entities)) + output_text += text + + return output_text, output_entities + + ALL_TYPES: Final[List[str]] = list(constants.MessageEntityType) + """List[:obj:`str`]: A list of all available message entity types.""" + BLOCKQUOTE: Final[str] = constants.MessageEntityType.BLOCKQUOTE + """:const:`telegram.constants.MessageEntityType.BLOCKQUOTE` + + .. versionadded:: 20.8 + """ + BOLD: Final[str] = constants.MessageEntityType.BOLD + """:const:`telegram.constants.MessageEntityType.BOLD`""" + BOT_COMMAND: Final[str] = constants.MessageEntityType.BOT_COMMAND + """:const:`telegram.constants.MessageEntityType.BOT_COMMAND`""" + CASHTAG: Final[str] = constants.MessageEntityType.CASHTAG + """:const:`telegram.constants.MessageEntityType.CASHTAG`""" + CODE: Final[str] = constants.MessageEntityType.CODE + """:const:`telegram.constants.MessageEntityType.CODE`""" + CUSTOM_EMOJI: Final[str] = constants.MessageEntityType.CUSTOM_EMOJI + """:const:`telegram.constants.MessageEntityType.CUSTOM_EMOJI` + + .. versionadded:: 20.0 + """ + EMAIL: Final[str] = constants.MessageEntityType.EMAIL + """:const:`telegram.constants.MessageEntityType.EMAIL`""" + EXPANDABLE_BLOCKQUOTE: Final[str] = constants.MessageEntityType.EXPANDABLE_BLOCKQUOTE + """:const:`telegram.constants.MessageEntityType.EXPANDABLE_BLOCKQUOTE` + + .. versionadded:: 21.3 + """ + HASHTAG: Final[str] = constants.MessageEntityType.HASHTAG + """:const:`telegram.constants.MessageEntityType.HASHTAG`""" + ITALIC: Final[str] = constants.MessageEntityType.ITALIC + """:const:`telegram.constants.MessageEntityType.ITALIC`""" + MENTION: Final[str] = constants.MessageEntityType.MENTION + """:const:`telegram.constants.MessageEntityType.MENTION`""" + PHONE_NUMBER: Final[str] = constants.MessageEntityType.PHONE_NUMBER + """:const:`telegram.constants.MessageEntityType.PHONE_NUMBER`""" + PRE: Final[str] = constants.MessageEntityType.PRE + """:const:`telegram.constants.MessageEntityType.PRE`""" + SPOILER: Final[str] = constants.MessageEntityType.SPOILER + """:const:`telegram.constants.MessageEntityType.SPOILER` + + .. versionadded:: 13.10 + """ + STRIKETHROUGH: Final[str] = constants.MessageEntityType.STRIKETHROUGH + """:const:`telegram.constants.MessageEntityType.STRIKETHROUGH`""" + TEXT_LINK: Final[str] = constants.MessageEntityType.TEXT_LINK + """:const:`telegram.constants.MessageEntityType.TEXT_LINK`""" + TEXT_MENTION: Final[str] = constants.MessageEntityType.TEXT_MENTION + """:const:`telegram.constants.MessageEntityType.TEXT_MENTION`""" + UNDERLINE: Final[str] = constants.MessageEntityType.UNDERLINE + """:const:`telegram.constants.MessageEntityType.UNDERLINE`""" + URL: Final[str] = constants.MessageEntityType.URL + """:const:`telegram.constants.MessageEntityType.URL`""" diff --git a/_messageid.py b/_messageid.py new file mode 100644 index 0000000000000000000000000000000000000000..bbfedf4703740737cd9c7136b3764f0b888eba2d --- /dev/null +++ b/_messageid.py @@ -0,0 +1,48 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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 an instance of a Telegram MessageId.""" + +from typing import Optional + +from telegram._telegramobject import TelegramObject +from telegram._utils.types import JSONDict + + +class MessageId(TelegramObject): + """This object represents a unique message identifier. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`message_id` is equal. + + Args: + message_id (:obj:`int`): Unique message identifier. + + Attributes: + message_id (:obj:`int`): Unique message identifier. + """ + + __slots__ = ("message_id",) + + def __init__(self, message_id: int, *, api_kwargs: Optional[JSONDict] = None): + super().__init__(api_kwargs=api_kwargs) + self.message_id: int = message_id + + self._id_attrs = (self.message_id,) + + self._freeze() diff --git a/_messageorigin.py b/_messageorigin.py new file mode 100644 index 0000000000000000000000000000000000000000..534583adb8beced78e84cf228f9adf039eacba70 --- /dev/null +++ b/_messageorigin.py @@ -0,0 +1,286 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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 the classes that represent Telegram MessageOigin.""" +import datetime +from typing import TYPE_CHECKING, Dict, Final, Optional, Type + +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.datetime import extract_tzinfo_from_defaults, from_timestamp +from telegram._utils.types import JSONDict + +if TYPE_CHECKING: + from telegram import Bot + + +class MessageOrigin(TelegramObject): + """ + Base class for telegram MessageOrigin object, it can be one of: + + * :class:`MessageOriginUser` + * :class:`MessageOriginHiddenUser` + * :class:`MessageOriginChat` + * :class:`MessageOriginChannel` + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`type` and :attr:`date` are equal. + + .. versionadded:: 20.8 + + Args: + type (:obj:`str`): Type of the message origin, can be on of: + :attr:`~telegram.MessageOrigin.USER`, :attr:`~telegram.MessageOrigin.HIDDEN_USER`, + :attr:`~telegram.MessageOrigin.CHAT`, or :attr:`~telegram.MessageOrigin.CHANNEL`. + date (:obj:`datetime.datetime`): Date the message was sent originally. + |datetime_localization| + + Attributes: + type (:obj:`str`): Type of the message origin, can be on of: + :attr:`~telegram.MessageOrigin.USER`, :attr:`~telegram.MessageOrigin.HIDDEN_USER`, + :attr:`~telegram.MessageOrigin.CHAT`, or :attr:`~telegram.MessageOrigin.CHANNEL`. + date (:obj:`datetime.datetime`): Date the message was sent originally. + |datetime_localization| + """ + + __slots__ = ( + "date", + "type", + ) + + USER: Final[str] = constants.MessageOriginType.USER + """:const:`telegram.constants.MessageOriginType.USER`""" + HIDDEN_USER: Final[str] = constants.MessageOriginType.HIDDEN_USER + """:const:`telegram.constants.MessageOriginType.HIDDEN_USER`""" + CHAT: Final[str] = constants.MessageOriginType.CHAT + """:const:`telegram.constants.MessageOriginType.CHAT`""" + CHANNEL: Final[str] = constants.MessageOriginType.CHANNEL + """:const:`telegram.constants.MessageOriginType.CHANNEL`""" + + def __init__( + self, + type: str, # pylint: disable=W0622 + date: datetime.datetime, + *, + api_kwargs: Optional[JSONDict] = None, + ): + super().__init__(api_kwargs=api_kwargs) + # Required by all subclasses + self.type: str = enum.get_member(constants.MessageOriginType, type, type) + self.date: datetime.datetime = date + + self._id_attrs = ( + self.type, + self.date, + ) + self._freeze() + + @classmethod + def de_json( + cls, data: Optional[JSONDict], bot: Optional["Bot"] = None + ) -> Optional["MessageOrigin"]: + """Converts JSON data to the appropriate :class:`MessageOrigin` object, i.e. takes + care of selecting the correct subclass. + """ + data = cls._parse_data(data) + + if not data: + return None + + _class_mapping: Dict[str, Type[MessageOrigin]] = { + cls.USER: MessageOriginUser, + cls.HIDDEN_USER: MessageOriginHiddenUser, + cls.CHAT: MessageOriginChat, + cls.CHANNEL: MessageOriginChannel, + } + if cls is MessageOrigin and data.get("type") in _class_mapping: + return _class_mapping[data.pop("type")].de_json(data=data, bot=bot) + + loc_tzinfo = extract_tzinfo_from_defaults(bot) + data["date"] = from_timestamp(data.get("date"), tzinfo=loc_tzinfo) + + if "sender_user" in data: + data["sender_user"] = User.de_json(data.get("sender_user"), bot) + + if "sender_chat" in data: + data["sender_chat"] = Chat.de_json(data.get("sender_chat"), bot) + + if "chat" in data: + data["chat"] = Chat.de_json(data.get("chat"), bot) + + return super().de_json(data=data, bot=bot) + + +class MessageOriginUser(MessageOrigin): + """ + The message was originally sent by a known user. + + .. versionadded:: 20.8 + + Args: + date (:obj:`datetime.datetime`): Date the message was sent originally. + |datetime_localization| + sender_user (:class:`telegram.User`): User that sent the message originally. + + Attributes: + type (:obj:`str`): Type of the message origin. Always + :tg-const:`~telegram.MessageOrigin.USER`. + date (:obj:`datetime.datetime`): Date the message was sent originally. + |datetime_localization| + sender_user (:class:`telegram.User`): User that sent the message originally. + """ + + __slots__ = ("sender_user",) + + def __init__( + self, + date: datetime.datetime, + sender_user: User, + *, + api_kwargs: Optional[JSONDict] = None, + ): + super().__init__(type=self.USER, date=date, api_kwargs=api_kwargs) + + with self._unfrozen(): + self.sender_user: User = sender_user + + +class MessageOriginHiddenUser(MessageOrigin): + """ + The message was originally sent by an unknown user. + + .. versionadded:: 20.8 + + Args: + date (:obj:`datetime.datetime`): Date the message was sent originally. + |datetime_localization| + sender_user_name (:obj:`str`): Name of the user that sent the message originally. + + Attributes: + type (:obj:`str`): Type of the message origin. Always + :tg-const:`~telegram.MessageOrigin.HIDDEN_USER`. + date (:obj:`datetime.datetime`): Date the message was sent originally. + |datetime_localization| + sender_user_name (:obj:`str`): Name of the user that sent the message originally. + """ + + __slots__ = ("sender_user_name",) + + def __init__( + self, + date: datetime.datetime, + sender_user_name: str, + *, + api_kwargs: Optional[JSONDict] = None, + ): + super().__init__(type=self.HIDDEN_USER, date=date, api_kwargs=api_kwargs) + + with self._unfrozen(): + self.sender_user_name: str = sender_user_name + + +class MessageOriginChat(MessageOrigin): + """ + The message was originally sent on behalf of a chat to a group chat. + + .. versionadded:: 20.8 + + Args: + date (:obj:`datetime.datetime`): Date the message was sent originally. + |datetime_localization| + sender_chat (:class:`telegram.Chat`): Chat that sent the message originally. + author_signature (:obj:`str`, optional): For messages originally sent by an anonymous chat + administrator, original message author signature + + Attributes: + type (:obj:`str`): Type of the message origin. Always + :tg-const:`~telegram.MessageOrigin.CHAT`. + date (:obj:`datetime.datetime`): Date the message was sent originally. + |datetime_localization| + sender_chat (:class:`telegram.Chat`): Chat that sent the message originally. + author_signature (:obj:`str`): Optional. For messages originally sent by an anonymous chat + administrator, original message author signature + """ + + __slots__ = ( + "author_signature", + "sender_chat", + ) + + def __init__( + self, + date: datetime.datetime, + sender_chat: Chat, + author_signature: Optional[str] = None, + *, + api_kwargs: Optional[JSONDict] = None, + ): + super().__init__(type=self.CHAT, date=date, api_kwargs=api_kwargs) + + with self._unfrozen(): + self.sender_chat: Chat = sender_chat + self.author_signature: Optional[str] = author_signature + + +class MessageOriginChannel(MessageOrigin): + """ + The message was originally sent to a channel chat. + + .. versionadded:: 20.8 + + Args: + date (:obj:`datetime.datetime`): Date the message was sent originally. + |datetime_localization| + chat (:class:`telegram.Chat`): Channel chat to which the message was originally sent. + message_id (:obj:`int`): Unique message identifier inside the chat. + author_signature (:obj:`str`, optional): Signature of the original post author. + + Attributes: + type (:obj:`str`): Type of the message origin. Always + :tg-const:`~telegram.MessageOrigin.CHANNEL`. + date (:obj:`datetime.datetime`): Date the message was sent originally. + |datetime_localization| + chat (:class:`telegram.Chat`): Channel chat to which the message was originally sent. + message_id (:obj:`int`): Unique message identifier inside the chat. + author_signature (:obj:`str`): Optional. Signature of the original post author. + """ + + __slots__ = ( + "author_signature", + "chat", + "message_id", + ) + + def __init__( + self, + date: datetime.datetime, + chat: Chat, + message_id: int, + author_signature: Optional[str] = None, + *, + api_kwargs: Optional[JSONDict] = None, + ): + super().__init__(type=self.CHANNEL, date=date, api_kwargs=api_kwargs) + + with self._unfrozen(): + self.chat: Chat = chat + self.message_id: int = message_id + self.author_signature: Optional[str] = author_signature diff --git a/_messagereactionupdated.py b/_messagereactionupdated.py new file mode 100644 index 0000000000000000000000000000000000000000..d4d4033a647bee2b3db93c80ecc398b69595f729 --- /dev/null +++ b/_messagereactionupdated.py @@ -0,0 +1,208 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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 MessageReaction Update.""" +from datetime import datetime +from typing import TYPE_CHECKING, Optional, Sequence, Tuple + +from telegram._chat import Chat +from telegram._reaction import ReactionCount, ReactionType +from telegram._telegramobject import TelegramObject +from telegram._user import User +from telegram._utils.argumentparsing import parse_sequence_arg +from telegram._utils.datetime import extract_tzinfo_from_defaults, from_timestamp +from telegram._utils.types import JSONDict + +if TYPE_CHECKING: + from telegram import Bot + + +class MessageReactionCountUpdated(TelegramObject): + """This class represents reaction changes on a message with anonymous reactions. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if the :attr:`chat`, :attr:`message_id`, :attr:`date` and :attr:`reactions` + is equal. + + .. versionadded:: 20.8 + + Args: + chat (:class:`telegram.Chat`): The chat containing the message. + message_id (:obj:`int`): Unique message identifier inside the chat. + date (:class:`datetime.datetime`): Date of the change in Unix time + |datetime_localization| + reactions (Sequence[:class:`telegram.ReactionCount`]): List of reactions that are present + on the message + + Attributes: + chat (:class:`telegram.Chat`): The chat containing the message. + message_id (:obj:`int`): Unique message identifier inside the chat. + date (:class:`datetime.datetime`): Date of the change in Unix time + |datetime_localization| + reactions (Tuple[:class:`telegram.ReactionCount`]): List of reactions that are present on + the message + """ + + __slots__ = ( + "chat", + "date", + "message_id", + "reactions", + ) + + def __init__( + self, + chat: Chat, + message_id: int, + date: datetime, + reactions: Sequence[ReactionCount], + *, + api_kwargs: Optional[JSONDict] = None, + ): + super().__init__(api_kwargs=api_kwargs) + # Required + self.chat: Chat = chat + self.message_id: int = message_id + self.date: datetime = date + self.reactions: Tuple[ReactionCount, ...] = parse_sequence_arg(reactions) + + self._id_attrs = (self.chat, self.message_id, self.date, self.reactions) + self._freeze() + + @classmethod + def de_json( + cls, data: Optional[JSONDict], bot: Optional["Bot"] = None + ) -> Optional["MessageReactionCountUpdated"]: + """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["chat"] = Chat.de_json(data.get("chat"), bot) + data["reactions"] = ReactionCount.de_list(data.get("reactions"), bot) + + return super().de_json(data=data, bot=bot) + + +class MessageReactionUpdated(TelegramObject): + """This class represents a change of a reaction on a message performed by a user. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if the :attr:`chat`, :attr:`message_id`, :attr:`date`, :attr:`old_reaction` + and :attr:`new_reaction` is equal. + + .. versionadded:: 20.8 + + Args: + chat (:class:`telegram.Chat`): The chat containing the message. + message_id (:obj:`int`): Unique message identifier inside the chat. + date (:class:`datetime.datetime`): Date of the change in Unix time. + |datetime_localization| + old_reaction (Sequence[:class:`telegram.ReactionType`]): Previous list of reaction types + that were set by the user. + new_reaction (Sequence[:class:`telegram.ReactionType`]): New list of reaction types that + were set by the user. + user (:class:`telegram.User`, optional): The user that changed the reaction, if the user + isn't anonymous. + actor_chat (:class:`telegram.Chat`, optional): The chat on behalf of which the reaction was + changed, if the user is anonymous. + + Attributes: + chat (:class:`telegram.Chat`): The chat containing the message. + message_id (:obj:`int`): Unique message identifier inside the chat. + date (:class:`datetime.datetime`): Date of the change in Unix time. + |datetime_localization| + old_reaction (Tuple[:class:`telegram.ReactionType`]): Previous list of reaction types + that were set by the user. + new_reaction (Tuple[:class:`telegram.ReactionType`]): New list of reaction types that + were set by the user. + user (:class:`telegram.User`): Optional. The user that changed the reaction, if the user + isn't anonymous. + actor_chat (:class:`telegram.Chat`): Optional. The chat on behalf of which the reaction was + changed, if the user is anonymous. + """ + + __slots__ = ( + "actor_chat", + "chat", + "date", + "message_id", + "new_reaction", + "old_reaction", + "user", + ) + + def __init__( + self, + chat: Chat, + message_id: int, + date: datetime, + old_reaction: Sequence[ReactionType], + new_reaction: Sequence[ReactionType], + user: Optional[User] = None, + actor_chat: Optional[Chat] = None, + *, + api_kwargs: Optional[JSONDict] = None, + ): + super().__init__(api_kwargs=api_kwargs) + # Required + self.chat: Chat = chat + self.message_id: int = message_id + self.date: datetime = date + self.old_reaction: Tuple[ReactionType, ...] = parse_sequence_arg(old_reaction) + self.new_reaction: Tuple[ReactionType, ...] = parse_sequence_arg(new_reaction) + + # Optional + self.user: Optional[User] = user + self.actor_chat: Optional[Chat] = actor_chat + + self._id_attrs = ( + self.chat, + self.message_id, + self.date, + self.old_reaction, + self.new_reaction, + ) + self._freeze() + + @classmethod + def de_json( + cls, data: Optional[JSONDict], bot: Optional["Bot"] = None + ) -> Optional["MessageReactionUpdated"]: + """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["chat"] = Chat.de_json(data.get("chat"), bot) + data["old_reaction"] = ReactionType.de_list(data.get("old_reaction"), bot) + data["new_reaction"] = ReactionType.de_list(data.get("new_reaction"), bot) + data["user"] = User.de_json(data.get("user"), bot) + data["actor_chat"] = Chat.de_json(data.get("actor_chat"), bot) + + return super().de_json(data=data, bot=bot) diff --git a/_paidmedia.py b/_paidmedia.py new file mode 100644 index 0000000000000000000000000000000000000000..fe78cca28e07af350930decfbe64aafa6be57572 --- /dev/null +++ b/_paidmedia.py @@ -0,0 +1,290 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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 objects that represent paid media in Telegram.""" + +from typing import TYPE_CHECKING, Dict, Final, Optional, Sequence, Tuple, Type + +from telegram import constants +from telegram._files.photosize import PhotoSize +from telegram._files.video import Video +from telegram._telegramobject import TelegramObject +from telegram._utils import enum +from telegram._utils.argumentparsing import parse_sequence_arg +from telegram._utils.types import JSONDict + +if TYPE_CHECKING: + from telegram import Bot + + +class PaidMedia(TelegramObject): + """Describes the paid media added to a message. Currently, it can be one of: + + * :class:`telegram.PaidMediaPreview` + * :class:`telegram.PaidMediaPhoto` + * :class:`telegram.PaidMediaVideo` + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`type` is equal. + + .. versionadded:: 21.4 + + Args: + type (:obj:`str`): Type of the paid media. + + Attributes: + type (:obj:`str`): Type of the paid media. + """ + + __slots__ = ("type",) + + PREVIEW: Final[str] = constants.PaidMediaType.PREVIEW + """:const:`telegram.constants.PaidMediaType.PREVIEW`""" + PHOTO: Final[str] = constants.PaidMediaType.PHOTO + """:const:`telegram.constants.PaidMediaType.PHOTO`""" + VIDEO: Final[str] = constants.PaidMediaType.VIDEO + """:const:`telegram.constants.PaidMediaType.VIDEO`""" + + def __init__( + self, + type: str, # pylint: disable=redefined-builtin + *, + api_kwargs: Optional[JSONDict] = None, + ) -> None: + super().__init__(api_kwargs=api_kwargs) + self.type: str = enum.get_member(constants.PaidMediaType, type, type) + + self._id_attrs = (self.type,) + self._freeze() + + @classmethod + def de_json( + cls, data: Optional[JSONDict], bot: Optional["Bot"] = None + ) -> Optional["PaidMedia"]: + """Converts JSON data to the appropriate :class:`PaidMedia` object, i.e. takes + care of selecting the correct subclass. + + Args: + data (Dict[:obj:`str`, ...]): The JSON data. + bot (:class:`telegram.Bot`, optional): The bot associated with this object. + + Returns: + The Telegram object. + + """ + data = cls._parse_data(data) + + if data is None: + return None + + if not data and cls is PaidMedia: + return None + + _class_mapping: Dict[str, Type[PaidMedia]] = { + cls.PREVIEW: PaidMediaPreview, + cls.PHOTO: PaidMediaPhoto, + cls.VIDEO: PaidMediaVideo, + } + + if cls is PaidMedia and data.get("type") in _class_mapping: + return _class_mapping[data.pop("type")].de_json(data=data, bot=bot) + + return super().de_json(data=data, bot=bot) + + +class PaidMediaPreview(PaidMedia): + """The paid media isn't available before the payment. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`width`, :attr:`height`, and :attr:`duration` + are equal. + + .. versionadded:: 21.4 + + Args: + type (:obj:`str`): Type of the paid media, always :tg-const:`telegram.PaidMedia.PREVIEW`. + width (:obj:`int`, optional): Media width as defined by the sender. + height (:obj:`int`, optional): Media height as defined by the sender. + duration (:obj:`int`, optional): Duration of the media in seconds as defined by the sender. + + Attributes: + type (:obj:`str`): Type of the paid media, always :tg-const:`telegram.PaidMedia.PREVIEW`. + width (:obj:`int`): Optional. Media width as defined by the sender. + height (:obj:`int`): Optional. Media height as defined by the sender. + duration (:obj:`int`): Optional. Duration of the media in seconds as defined by the sender. + """ + + __slots__ = ("duration", "height", "width") + + def __init__( + self, + width: Optional[int] = None, + height: Optional[int] = None, + duration: Optional[int] = None, + *, + api_kwargs: Optional[JSONDict] = None, + ) -> None: + super().__init__(type=PaidMedia.PREVIEW, api_kwargs=api_kwargs) + + with self._unfrozen(): + self.width: Optional[int] = width + self.height: Optional[int] = height + self.duration: Optional[int] = duration + + self._id_attrs = (self.type, self.width, self.height, self.duration) + + +class PaidMediaPhoto(PaidMedia): + """ + The paid media is a photo. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`photo` are equal. + + .. versionadded:: 21.4 + + Args: + type (:obj:`str`): Type of the paid media, always :tg-const:`telegram.PaidMedia.PHOTO`. + photo (Sequence[:class:`telegram.PhotoSize`]): The photo. + + Attributes: + type (:obj:`str`): Type of the paid media, always :tg-const:`telegram.PaidMedia.PHOTO`. + photo (Tuple[:class:`telegram.PhotoSize`]): The photo. + """ + + __slots__ = ("photo",) + + def __init__( + self, + photo: Sequence["PhotoSize"], + *, + api_kwargs: Optional[JSONDict] = None, + ) -> None: + super().__init__(type=PaidMedia.PHOTO, api_kwargs=api_kwargs) + + with self._unfrozen(): + self.photo: Tuple[PhotoSize, ...] = parse_sequence_arg(photo) + + self._id_attrs = (self.type, self.photo) + + @classmethod + def de_json( + cls, data: Optional[JSONDict], bot: Optional["Bot"] = None + ) -> Optional["PaidMediaPhoto"]: + data = cls._parse_data(data) + + if not data: + return None + + data["photo"] = PhotoSize.de_list(data.get("photo"), bot=bot) + return super().de_json(data=data, bot=bot) # type: ignore[return-value] + + +class PaidMediaVideo(PaidMedia): + """ + The paid media is a video. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`video` are equal. + + .. versionadded:: 21.4 + + Args: + type (:obj:`str`): Type of the paid media, always :tg-const:`telegram.PaidMedia.VIDEO`. + video (:class:`telegram.Video`): The video. + + Attributes: + type (:obj:`str`): Type of the paid media, always :tg-const:`telegram.PaidMedia.VIDEO`. + video (:class:`telegram.Video`): The video. + """ + + __slots__ = ("video",) + + def __init__( + self, + video: Video, + *, + api_kwargs: Optional[JSONDict] = None, + ) -> None: + super().__init__(type=PaidMedia.VIDEO, api_kwargs=api_kwargs) + + with self._unfrozen(): + self.video: Video = video + + self._id_attrs = (self.type, self.video) + + @classmethod + def de_json( + cls, data: Optional[JSONDict], bot: Optional["Bot"] = None + ) -> Optional["PaidMediaVideo"]: + data = cls._parse_data(data) + + if not data: + return None + + data["video"] = Video.de_json(data.get("video"), bot=bot) + return super().de_json(data=data, bot=bot) # type: ignore[return-value] + + +class PaidMediaInfo(TelegramObject): + """ + Describes the paid media added to a message. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`star_count` and :attr:`paid_media` are equal. + + .. versionadded:: 21.4 + + Args: + star_count (:obj:`int`): The number of Telegram Stars that must be paid to buy access to + the media. + paid_media (Sequence[:class:`telegram.PaidMedia`]): Information about the paid media. + + Attributes: + star_count (:obj:`int`): The number of Telegram Stars that must be paid to buy access to + the media. + paid_media (Tuple[:class:`telegram.PaidMedia`]): Information about the paid media. + """ + + __slots__ = ("paid_media", "star_count") + + def __init__( + self, + star_count: int, + paid_media: Sequence[PaidMedia], + *, + api_kwargs: Optional[JSONDict] = None, + ) -> None: + super().__init__(api_kwargs=api_kwargs) + self.star_count: int = star_count + self.paid_media: Tuple[PaidMedia, ...] = parse_sequence_arg(paid_media) + + self._id_attrs = (self.star_count, self.paid_media) + self._freeze() + + @classmethod + def de_json( + cls, data: Optional[JSONDict], bot: Optional["Bot"] = None + ) -> Optional["PaidMediaInfo"]: + data = cls._parse_data(data) + + if not data: + return None + + data["paid_media"] = PaidMedia.de_list(data.get("paid_media"), bot=bot) + return super().de_json(data=data, bot=bot) diff --git a/_poll.py b/_poll.py new file mode 100644 index 0000000000000000000000000000000000000000..8ea387a0950298bb1f8990afd92f9d949a086086 --- /dev/null +++ b/_poll.py @@ -0,0 +1,649 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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 Poll.""" +import datetime +from typing import TYPE_CHECKING, Dict, Final, List, Optional, Sequence, Tuple + +from telegram import constants +from telegram._chat import Chat +from telegram._messageentity import MessageEntity +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.datetime import extract_tzinfo_from_defaults, from_timestamp +from telegram._utils.defaultvalue import DEFAULT_NONE +from telegram._utils.entities import parse_message_entities, parse_message_entity +from telegram._utils.types import JSONDict, ODVInput + +if TYPE_CHECKING: + from telegram import Bot + + +class InputPollOption(TelegramObject): + """ + This object contains information about one answer option in a poll to be sent. + + 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.2 + + Args: + text (:obj:`str`): Option text, + :tg-const:`telegram.PollOption.MIN_LENGTH`-:tg-const:`telegram.PollOption.MAX_LENGTH` + characters. + text_parse_mode (:obj:`str`, optional): |parse_mode| + Currently, only custom emoji entities are allowed. + text_entities (Sequence[:class:`telegram.MessageEntity`], optional): Special entities + that appear in the option :paramref:`text`. It can be specified instead of + :paramref:`text_parse_mode`. + Currently, only custom emoji entities are allowed. + This list is empty if the text does not contain entities. + + Attributes: + text (:obj:`str`): Option text, + :tg-const:`telegram.PollOption.MIN_LENGTH`-:tg-const:`telegram.PollOption.MAX_LENGTH` + characters. + text_parse_mode (:obj:`str`): Optional. |parse_mode| + Currently, only custom emoji entities are allowed. + text_entities (Sequence[:class:`telegram.MessageEntity`]): Special entities + that appear in the option :paramref:`text`. It can be specified instead of + :paramref:`text_parse_mode`. + Currently, only custom emoji entities are allowed. + This list is empty if the text does not contain entities. + """ + + __slots__ = ("text", "text_entities", "text_parse_mode") + + def __init__( + self, + text: str, + text_parse_mode: ODVInput[str] = DEFAULT_NONE, + text_entities: Optional[Sequence[MessageEntity]] = None, + *, + api_kwargs: Optional[JSONDict] = None, + ): + super().__init__(api_kwargs=api_kwargs) + self.text: str = text + self.text_parse_mode: ODVInput[str] = text_parse_mode + self.text_entities: Tuple[MessageEntity, ...] = parse_sequence_arg(text_entities) + + self._id_attrs = (self.text,) + + self._freeze() + + @classmethod + def de_json( + cls, data: Optional[JSONDict], bot: Optional["Bot"] = None + ) -> Optional["InputPollOption"]: + """See :meth:`telegram.TelegramObject.de_json`.""" + data = cls._parse_data(data) + + if not data: + return None + + data["text_entities"] = MessageEntity.de_list(data.get("text_entities"), bot) + + return super().de_json(data=data, bot=bot) + + +class PollOption(TelegramObject): + """ + This object contains information about one answer option in a poll. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`text` and :attr:`voter_count` are equal. + + Args: + text (:obj:`str`): Option text, + :tg-const:`telegram.PollOption.MIN_LENGTH`-:tg-const:`telegram.PollOption.MAX_LENGTH` + characters. + voter_count (:obj:`int`): Number of users that voted for this option. + text_entities (Sequence[:class:`telegram.MessageEntity`], optional): Special entities + that appear in the option text. Currently, only custom emoji entities are allowed in + poll option texts. + + .. versionadded:: 21.2 + + Attributes: + text (:obj:`str`): Option text, + :tg-const:`telegram.PollOption.MIN_LENGTH`-:tg-const:`telegram.PollOption.MAX_LENGTH` + characters. + voter_count (:obj:`int`): Number of users that voted for this option. + text_entities (Tuple[:class:`telegram.MessageEntity`]): Special entities + that appear in the option text. Currently, only custom emoji entities are allowed in + poll option texts. + This list is empty if the question does not contain entities. + + .. versionadded:: 21.2 + + """ + + __slots__ = ("text", "text_entities", "voter_count") + + def __init__( + self, + text: str, + voter_count: int, + text_entities: Optional[Sequence[MessageEntity]] = None, + *, + api_kwargs: Optional[JSONDict] = None, + ): + super().__init__(api_kwargs=api_kwargs) + self.text: str = text + self.voter_count: int = voter_count + self.text_entities: Tuple[MessageEntity, ...] = parse_sequence_arg(text_entities) + + self._id_attrs = (self.text, self.voter_count) + + self._freeze() + + @classmethod + def de_json( + cls, data: Optional[JSONDict], bot: Optional["Bot"] = None + ) -> Optional["PollOption"]: + """See :meth:`telegram.TelegramObject.de_json`.""" + data = cls._parse_data(data) + + if not data: + return None + + data["text_entities"] = MessageEntity.de_list(data.get("text_entities"), bot) + + return super().de_json(data=data, bot=bot) + + def parse_entity(self, entity: MessageEntity) -> str: + """Returns the text in :attr:`text` + from a given :class:`telegram.MessageEntity` of :attr:`text_entities`. + + Note: + This method is present because Telegram calculates the offset and length in + UTF-16 codepoint pairs, which some versions of Python don't handle automatically. + (That is, you can't just slice ``Message.text`` with the offset and length.) + + .. versionadded:: 21.2 + + Args: + entity (:class:`telegram.MessageEntity`): The entity to extract the text from. It must + be an entity that belongs to :attr:`text_entities`. + + Returns: + :obj:`str`: The text of the given entity. + """ + return parse_message_entity(self.text, entity) + + def parse_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 polls question filtered by their ``type`` attribute as + the key, and the text that each entity belongs to as the value of the :obj:`dict`. + + Note: + This method should always be used instead of the :attr:`text_entities` + attribute, since it calculates the correct substring from the message text based on + UTF-16 codepoints. See :attr:`parse_entity` for more info. + + .. versionadded:: 21.2 + + Args: + types (List[:obj:`str`], optional): List of ``MessageEntity`` types as strings. If the + ``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 + the text that belongs to them, calculated based on UTF-16 codepoints. + """ + return parse_message_entities(self.text, self.text_entities, types) + + MIN_LENGTH: Final[int] = constants.PollLimit.MIN_OPTION_LENGTH + """:const:`telegram.constants.PollLimit.MIN_OPTION_LENGTH` + + .. versionadded:: 20.0 + """ + MAX_LENGTH: Final[int] = constants.PollLimit.MAX_OPTION_LENGTH + """:const:`telegram.constants.PollLimit.MAX_OPTION_LENGTH` + + .. versionadded:: 20.0 + """ + + +class PollAnswer(TelegramObject): + """ + This object represents an answer of a user in a non-anonymous poll. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`poll_id`, :attr:`user` and :attr:`option_ids` are equal. + + .. versionchanged:: 20.5 + The order of :paramref:`option_ids` and :paramref:`user` is changed in + 20.5 as the latter one became optional. + + .. versionchanged:: 20.6 + Backward compatiblity for changed order of :paramref:`option_ids` and :paramref:`user` + was removed. + + Args: + poll_id (:obj:`str`): Unique poll identifier. + option_ids (Sequence[:obj:`int`]): Identifiers of answer options, chosen by the user. May + be empty if the user retracted their vote. + + .. versionchanged:: 20.0 + |sequenceclassargs| + user (:class:`telegram.User`, optional): The user that changed the answer to the poll, + if the voter isn't anonymous. If the voter is anonymous, this field will contain the + user :tg-const:`telegram.constants.ChatID.FAKE_CHANNEL` for backwards compatibility. + + .. versionchanged:: 20.5 + :paramref:`user` became optional. + voter_chat (:class:`telegram.Chat`, optional): The chat that changed the answer to the + poll, if the voter is anonymous. + + .. versionadded:: 20.5 + + Attributes: + poll_id (:obj:`str`): Unique poll identifier. + option_ids (Tuple[:obj:`int`]): Identifiers of answer options, chosen by the user. May + be empty if the user retracted their vote. + + .. versionchanged:: 20.0 + |tupleclassattrs| + user (:class:`telegram.User`): Optional. The user, who changed the answer to the + poll, if the voter isn't anonymous. If the voter is anonymous, this field will contain + the user :tg-const:`telegram.constants.ChatID.FAKE_CHANNEL` for backwards compatibility + + .. versionchanged:: 20.5 + :paramref:`user` became optional. + voter_chat (:class:`telegram.Chat`): Optional. The chat that changed the answer to the + poll, if the voter is anonymous. + + .. versionadded:: 20.5 + + """ + + __slots__ = ("option_ids", "poll_id", "user", "voter_chat") + + def __init__( + self, + poll_id: str, + option_ids: Sequence[int], + user: Optional[User] = None, + voter_chat: Optional[Chat] = None, + *, + api_kwargs: Optional[JSONDict] = None, + ): + super().__init__(api_kwargs=api_kwargs) + self.poll_id: str = poll_id + self.voter_chat: Optional[Chat] = voter_chat + self.option_ids: Tuple[int, ...] = parse_sequence_arg(option_ids) + self.user: Optional[User] = user + + self._id_attrs = ( + self.poll_id, + self.option_ids, + self.user, + self.voter_chat, + ) + + self._freeze() + + @classmethod + def de_json( + cls, data: Optional[JSONDict], bot: Optional["Bot"] = None + ) -> Optional["PollAnswer"]: + """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["voter_chat"] = Chat.de_json(data.get("voter_chat"), bot) + + return super().de_json(data=data, bot=bot) + + +class Poll(TelegramObject): + """ + This object contains information about a poll. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`id` is equal. + + Examples: + :any:`Poll Bot ` + + Args: + id (:obj:`str`): Unique poll identifier. + question (:obj:`str`): Poll question, :tg-const:`telegram.Poll.MIN_QUESTION_LENGTH`- + :tg-const:`telegram.Poll.MAX_QUESTION_LENGTH` characters. + options (Sequence[:class:`~telegram.PollOption`]): List of poll options. + + .. versionchanged:: 20.0 + |sequenceclassargs| + is_closed (:obj:`bool`): :obj:`True`, if the poll is closed. + is_anonymous (:obj:`bool`): :obj:`True`, if the poll is anonymous. + type (:obj:`str`): Poll type, currently can be :attr:`REGULAR` or :attr:`QUIZ`. + allows_multiple_answers (:obj:`bool`): :obj:`True`, if the poll allows multiple answers. + correct_option_id (:obj:`int`, optional): A zero based identifier of the correct answer + option. Available only for closed polls in the quiz mode, which were sent + (not forwarded), by the bot or to a private chat with the bot. + explanation (:obj:`str`, optional): Text that is shown when a user chooses an incorrect + answer or taps on the lamp icon in a quiz-style poll, + 0-:tg-const:`telegram.Poll.MAX_EXPLANATION_LENGTH` characters. + explanation_entities (Sequence[:class:`telegram.MessageEntity`], optional): Special + entities like usernames, URLs, bot commands, etc. that appear in the + :attr:`explanation`. This list is empty if the message does not contain explanation + entities. + + .. versionchanged:: 20.0 + + * This attribute is now always a (possibly empty) list and never :obj:`None`. + * |sequenceclassargs| + open_period (:obj:`int`, optional): Amount of time in seconds the poll will be active + after creation. + close_date (:obj:`datetime.datetime`, optional): Point in time (Unix timestamp) when the + poll will be automatically closed. Converted to :obj:`datetime.datetime`. + + .. versionchanged:: 20.3 + |datetime_localization| + question_entities (Sequence[:class:`telegram.MessageEntity`], optional): Special entities + that appear in the :attr:`question`. Currently, only custom emoji entities are allowed + in poll questions. + + .. versionadded:: 21.2 + + Attributes: + id (:obj:`str`): Unique poll identifier. + question (:obj:`str`): Poll question, :tg-const:`telegram.Poll.MIN_QUESTION_LENGTH`- + :tg-const:`telegram.Poll.MAX_QUESTION_LENGTH` characters. + options (Tuple[:class:`~telegram.PollOption`]): List of poll options. + + .. versionchanged:: 20.0 + |tupleclassattrs| + total_voter_count (:obj:`int`): Total number of users that voted in the poll. + is_closed (:obj:`bool`): :obj:`True`, if the poll is closed. + is_anonymous (:obj:`bool`): :obj:`True`, if the poll is anonymous. + type (:obj:`str`): Poll type, currently can be :attr:`REGULAR` or :attr:`QUIZ`. + allows_multiple_answers (:obj:`bool`): :obj:`True`, if the poll allows multiple answers. + correct_option_id (:obj:`int`): Optional. A zero based identifier of the correct answer + option. Available only for closed polls in the quiz mode, which were sent + (not forwarded), by the bot or to a private chat with the bot. + explanation (:obj:`str`): Optional. Text that is shown when a user chooses an incorrect + answer or taps on the lamp icon in a quiz-style poll, + 0-:tg-const:`telegram.Poll.MAX_EXPLANATION_LENGTH` characters. + explanation_entities (Tuple[:class:`telegram.MessageEntity`]): Special entities + like usernames, URLs, bot commands, etc. that appear in the :attr:`explanation`. + This list is empty if the message does not contain explanation entities. + + .. versionchanged:: 20.0 + |tupleclassattrs| + + .. versionchanged:: 20.0 + This attribute is now always a (possibly empty) list and never :obj:`None`. + open_period (:obj:`int`): Optional. Amount of time in seconds the poll will be active + after creation. + close_date (:obj:`datetime.datetime`): Optional. Point in time when the poll will be + automatically closed. + + .. versionchanged:: 20.3 + |datetime_localization| + question_entities (Tuple[:class:`telegram.MessageEntity`]): Special entities + that appear in the :attr:`question`. Currently, only custom emoji entities are allowed + in poll questions. + This list is empty if the question does not contain entities. + + .. versionadded:: 21.2 + + """ + + __slots__ = ( + "allows_multiple_answers", + "close_date", + "correct_option_id", + "explanation", + "explanation_entities", + "id", + "is_anonymous", + "is_closed", + "open_period", + "options", + "question", + "question_entities", + "total_voter_count", + "type", + ) + + def __init__( + self, + id: str, # pylint: disable=redefined-builtin + question: str, + options: Sequence[PollOption], + total_voter_count: int, + is_closed: bool, + is_anonymous: bool, + type: str, # pylint: disable=redefined-builtin + allows_multiple_answers: bool, + correct_option_id: Optional[int] = None, + explanation: Optional[str] = None, + explanation_entities: Optional[Sequence[MessageEntity]] = None, + open_period: Optional[int] = None, + close_date: Optional[datetime.datetime] = None, + question_entities: Optional[Sequence[MessageEntity]] = None, + *, + api_kwargs: Optional[JSONDict] = None, + ): + super().__init__(api_kwargs=api_kwargs) + self.id: str = id + self.question: str = question + self.options: Tuple[PollOption, ...] = parse_sequence_arg(options) + self.total_voter_count: int = total_voter_count + self.is_closed: bool = is_closed + self.is_anonymous: bool = is_anonymous + self.type: str = enum.get_member(constants.PollType, type, type) + self.allows_multiple_answers: bool = allows_multiple_answers + self.correct_option_id: Optional[int] = correct_option_id + self.explanation: Optional[str] = explanation + self.explanation_entities: Tuple[MessageEntity, ...] = parse_sequence_arg( + explanation_entities + ) + self.open_period: Optional[int] = open_period + self.close_date: Optional[datetime.datetime] = close_date + self.question_entities: Tuple[MessageEntity, ...] = parse_sequence_arg(question_entities) + + self._id_attrs = (self.id,) + + self._freeze() + + @classmethod + def de_json(cls, data: Optional[JSONDict], bot: Optional["Bot"] = None) -> Optional["Poll"]: + """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["options"] = [PollOption.de_json(option, bot) for option in data["options"]] + data["explanation_entities"] = MessageEntity.de_list(data.get("explanation_entities"), bot) + data["close_date"] = from_timestamp(data.get("close_date"), tzinfo=loc_tzinfo) + data["question_entities"] = MessageEntity.de_list(data.get("question_entities"), bot) + + return super().de_json(data=data, bot=bot) + + def parse_explanation_entity(self, entity: MessageEntity) -> str: + """Returns the text in :attr:`explanation` from a given :class:`telegram.MessageEntity` of + :attr:`explanation_entities`. + + Note: + This method is present because Telegram calculates the offset and length in + UTF-16 codepoint pairs, which some versions of Python don't handle automatically. + (That is, you can't just slice ``Message.text`` with the offset and length.) + + Args: + entity (:class:`telegram.MessageEntity`): The entity to extract the text from. It must + be an entity that belongs to :attr:`explanation_entities`. + + Returns: + :obj:`str`: The text of the given entity. + + Raises: + RuntimeError: If the poll has no explanation. + + """ + if not self.explanation: + raise RuntimeError("This Poll has no 'explanation'.") + + return parse_message_entity(self.explanation, entity) + + def parse_explanation_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 polls explanation filtered by their ``type`` attribute as + the key, and the text that each entity belongs to as the value of the :obj:`dict`. + + Note: + This method should always be used instead of the :attr:`explanation_entities` + attribute, since it calculates the correct substring from the message text based on + UTF-16 codepoints. See :attr:`parse_explanation_entity` for more info. + + Args: + types (List[:obj:`str`], optional): List of ``MessageEntity`` types as strings. If the + ``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 + the text that belongs to them, calculated based on UTF-16 codepoints. + + Raises: + RuntimeError: If the poll has no explanation. + + """ + if not self.explanation: + raise RuntimeError("This Poll has no 'explanation'.") + + return parse_message_entities(self.explanation, self.explanation_entities, types) + + def parse_question_entity(self, entity: MessageEntity) -> str: + """Returns the text in :attr:`question` from a given :class:`telegram.MessageEntity` of + :attr:`question_entities`. + + .. versionadded:: 21.2 + + Note: + This method is present because Telegram calculates the offset and length in + UTF-16 codepoint pairs, which some versions of Python don't handle automatically. + (That is, you can't just slice ``Message.text`` with the offset and length.) + + Args: + entity (:class:`telegram.MessageEntity`): The entity to extract the text from. It must + be an entity that belongs to :attr:`question_entities`. + + Returns: + :obj:`str`: The text of the given entity. + """ + return parse_message_entity(self.question, entity) + + def parse_question_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 polls question filtered by their ``type`` attribute as + the key, and the text that each entity belongs to as the value of the :obj:`dict`. + + .. versionadded:: 21.2 + + Note: + This method should always be used instead of the :attr:`question_entities` + attribute, since it calculates the correct substring from the message text based on + UTF-16 codepoints. See :attr:`parse_question_entity` for more info. + + Args: + types (List[:obj:`str`], optional): List of ``MessageEntity`` types as strings. If the + ``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 + the text that belongs to them, calculated based on UTF-16 codepoints. + + """ + return parse_message_entities(self.question, self.question_entities, types) + + REGULAR: Final[str] = constants.PollType.REGULAR + """:const:`telegram.constants.PollType.REGULAR`""" + QUIZ: Final[str] = constants.PollType.QUIZ + """:const:`telegram.constants.PollType.QUIZ`""" + MAX_EXPLANATION_LENGTH: Final[int] = constants.PollLimit.MAX_EXPLANATION_LENGTH + """:const:`telegram.constants.PollLimit.MAX_EXPLANATION_LENGTH` + + .. versionadded:: 20.0 + """ + MAX_EXPLANATION_LINE_FEEDS: Final[int] = constants.PollLimit.MAX_EXPLANATION_LINE_FEEDS + """:const:`telegram.constants.PollLimit.MAX_EXPLANATION_LINE_FEEDS` + + .. versionadded:: 20.0 + """ + MIN_OPEN_PERIOD: Final[int] = constants.PollLimit.MIN_OPEN_PERIOD + """:const:`telegram.constants.PollLimit.MIN_OPEN_PERIOD` + + .. versionadded:: 20.0 + """ + MAX_OPEN_PERIOD: Final[int] = constants.PollLimit.MAX_OPEN_PERIOD + """:const:`telegram.constants.PollLimit.MAX_OPEN_PERIOD` + + .. versionadded:: 20.0 + """ + MIN_QUESTION_LENGTH: Final[int] = constants.PollLimit.MIN_QUESTION_LENGTH + """:const:`telegram.constants.PollLimit.MIN_QUESTION_LENGTH` + + .. versionadded:: 20.0 + """ + MAX_QUESTION_LENGTH: Final[int] = constants.PollLimit.MAX_QUESTION_LENGTH + """:const:`telegram.constants.PollLimit.MAX_QUESTION_LENGTH` + + .. versionadded:: 20.0 + """ + MIN_OPTION_LENGTH: Final[int] = constants.PollLimit.MIN_OPTION_LENGTH + """:const:`telegram.constants.PollLimit.MIN_OPTION_LENGTH` + + .. versionadded:: 20.0 + """ + MAX_OPTION_LENGTH: Final[int] = constants.PollLimit.MAX_OPTION_LENGTH + """:const:`telegram.constants.PollLimit.MAX_OPTION_LENGTH` + + .. versionadded:: 20.0 + """ + MIN_OPTION_NUMBER: Final[int] = constants.PollLimit.MIN_OPTION_NUMBER + """:const:`telegram.constants.PollLimit.MIN_OPTION_NUMBER` + + .. versionadded:: 20.0 + """ + MAX_OPTION_NUMBER: Final[int] = constants.PollLimit.MAX_OPTION_NUMBER + """:const:`telegram.constants.PollLimit.MAX_OPTION_NUMBER` + + .. versionadded:: 20.0 + """ diff --git a/_proximityalerttriggered.py b/_proximityalerttriggered.py new file mode 100644 index 0000000000000000000000000000000000000000..0880ca9a6f6fc3b79b9dce9db3a7942f9499da81 --- /dev/null +++ b/_proximityalerttriggered.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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 Proximity Alert.""" +from typing import TYPE_CHECKING, Optional + +from telegram._telegramobject import TelegramObject +from telegram._user import User +from telegram._utils.types import JSONDict + +if TYPE_CHECKING: + from telegram import Bot + + +class ProximityAlertTriggered(TelegramObject): + """ + This object represents the content of a service message, sent whenever a user in the chat + triggers a proximity alert set by another user. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`traveler`, :attr:`watcher` and :attr:`distance` are equal. + + Args: + traveler (:class:`telegram.User`): User that triggered the alert + watcher (:class:`telegram.User`): User that set the alert + distance (:obj:`int`): The distance between the users + + Attributes: + traveler (:class:`telegram.User`): User that triggered the alert + watcher (:class:`telegram.User`): User that set the alert + distance (:obj:`int`): The distance between the users + + """ + + __slots__ = ("distance", "traveler", "watcher") + + def __init__( + self, + traveler: User, + watcher: User, + distance: int, + *, + api_kwargs: Optional[JSONDict] = None, + ): + super().__init__(api_kwargs=api_kwargs) + self.traveler: User = traveler + self.watcher: User = watcher + self.distance: int = distance + + self._id_attrs = (self.traveler, self.watcher, self.distance) + + self._freeze() + + @classmethod + def de_json( + cls, data: Optional[JSONDict], bot: Optional["Bot"] = None + ) -> Optional["ProximityAlertTriggered"]: + """See :meth:`telegram.TelegramObject.de_json`.""" + data = cls._parse_data(data) + + if not data: + return None + + data["traveler"] = User.de_json(data.get("traveler"), bot) + data["watcher"] = User.de_json(data.get("watcher"), bot) + + return super().de_json(data=data, bot=bot) diff --git a/_reaction.py b/_reaction.py new file mode 100644 index 0000000000000000000000000000000000000000..90de7823d791acb8b7d78c26b28f2a62298286d6 --- /dev/null +++ b/_reaction.py @@ -0,0 +1,244 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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 objects that represents a Telegram ReactionType.""" + +from typing import TYPE_CHECKING, Dict, Final, Literal, Optional, Type, Union + +from telegram import constants +from telegram._telegramobject import TelegramObject +from telegram._utils import enum +from telegram._utils.types import JSONDict + +if TYPE_CHECKING: + from telegram import Bot + + +class ReactionType(TelegramObject): + """Base class for Telegram ReactionType Objects. + There exist :class:`telegram.ReactionTypeEmoji`, :class:`telegram.ReactionTypeCustomEmoji` + and :class:`telegram.ReactionTypePaid`. + + .. versionadded:: 20.8 + .. versionchanged:: 21.5 + + Added paid reaction. + + Args: + type (:obj:`str`): Type of the reaction. Can be + :attr:`~telegram.ReactionType.EMOJI`, :attr:`~telegram.ReactionType.CUSTOM_EMOJI` or + :attr:`~telegram.ReactionType.PAID`. + Attributes: + type (:obj:`str`): Type of the reaction. Can be + :attr:`~telegram.ReactionType.EMOJI`, :attr:`~telegram.ReactionType.CUSTOM_EMOJI` or + :attr:`~telegram.ReactionType.PAID`. + + """ + + __slots__ = ("type",) + + EMOJI: Final[constants.ReactionType] = constants.ReactionType.EMOJI + """:const:`telegram.constants.ReactionType.EMOJI`""" + CUSTOM_EMOJI: Final[constants.ReactionType] = constants.ReactionType.CUSTOM_EMOJI + """:const:`telegram.constants.ReactionType.CUSTOM_EMOJI`""" + PAID: Final[constants.ReactionType] = constants.ReactionType.PAID + """:const:`telegram.constants.ReactionType.PAID` + + .. versionadded:: 21.5 + """ + + def __init__( + self, + type: Union[ # pylint: disable=redefined-builtin + Literal["emoji", "custom_emoji", "paid"], constants.ReactionType + ], + *, + api_kwargs: Optional[JSONDict] = None, + ): + super().__init__(api_kwargs=api_kwargs) + # Required by all subclasses + self.type: str = enum.get_member(constants.ReactionType, type, type) + + self._freeze() + + @classmethod + def de_json( + cls, data: Optional[JSONDict], bot: Optional["Bot"] = None + ) -> Optional["ReactionType"]: + """See :meth:`telegram.TelegramObject.de_json`.""" + data = cls._parse_data(data) + + if data is None: + return None + + if not data and cls is ReactionType: + return None + + _class_mapping: Dict[str, Type[ReactionType]] = { + cls.EMOJI: ReactionTypeEmoji, + cls.CUSTOM_EMOJI: ReactionTypeCustomEmoji, + cls.PAID: ReactionTypePaid, + } + + if cls is ReactionType and data.get("type") in _class_mapping: + return _class_mapping[data.pop("type")].de_json(data, bot) + + return super().de_json(data=data, bot=bot) + + +class ReactionTypeEmoji(ReactionType): + """ + Represents a reaction with a normal emoji. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if the :attr:`emoji` is equal. + + .. versionadded:: 20.8 + + Args: + emoji (:obj:`str`): Reaction emoji. It can be one of + :const:`telegram.constants.ReactionEmoji`. + + Attributes: + type (:obj:`str`): Type of the reaction, + always :tg-const:`telegram.ReactionType.EMOJI`. + emoji (:obj:`str`): Reaction emoji. It can be one of + :const:`telegram.constants.ReactionEmoji`. + """ + + __slots__ = ("emoji",) + + def __init__( + self, + emoji: str, + *, + api_kwargs: Optional[JSONDict] = None, + ): + super().__init__(type=ReactionType.EMOJI, api_kwargs=api_kwargs) + + with self._unfrozen(): + self.emoji: str = emoji + self._id_attrs = (self.emoji,) + + +class ReactionTypeCustomEmoji(ReactionType): + """ + Represents a reaction with a custom emoji. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if the :attr:`custom_emoji_id` is equal. + + .. versionadded:: 20.8 + + Args: + custom_emoji_id (:obj:`str`): Custom emoji identifier. + + Attributes: + type (:obj:`str`): Type of the reaction, + always :tg-const:`telegram.ReactionType.CUSTOM_EMOJI`. + custom_emoji_id (:obj:`str`): Custom emoji identifier. + + """ + + __slots__ = ("custom_emoji_id",) + + def __init__( + self, + custom_emoji_id: str, + *, + api_kwargs: Optional[JSONDict] = None, + ): + super().__init__(type=ReactionType.CUSTOM_EMOJI, api_kwargs=api_kwargs) + + with self._unfrozen(): + self.custom_emoji_id: str = custom_emoji_id + self._id_attrs = (self.custom_emoji_id,) + + +class ReactionTypePaid(ReactionType): + """ + The reaction is paid. + + .. versionadded:: 21.5 + + Attributes: + type (:obj:`str`): Type of the reaction, + always :tg-const:`telegram.ReactionType.PAID`. + """ + + __slots__ = () + + def __init__(self, *, api_kwargs: Optional[JSONDict] = None): + super().__init__(type=ReactionType.PAID, api_kwargs=api_kwargs) + self._freeze() + + +class ReactionCount(TelegramObject): + """This class represents a reaction added to a message along with the number of times it was + added. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if the :attr:`type` and :attr:`total_count` is equal. + + .. versionadded:: 20.8 + + Args: + type (:class:`telegram.ReactionType`): Type of the reaction. + total_count (:obj:`int`): Number of times the reaction was added. + + Attributes: + type (:class:`telegram.ReactionType`): Type of the reaction. + total_count (:obj:`int`): Number of times the reaction was added. + """ + + __slots__ = ( + "total_count", + "type", + ) + + def __init__( + self, + type: ReactionType, # pylint: disable=redefined-builtin + total_count: int, + *, + api_kwargs: Optional[JSONDict] = None, + ): + super().__init__(api_kwargs=api_kwargs) + # Required + self.type: ReactionType = type + self.total_count: int = total_count + + self._id_attrs = ( + self.type, + self.total_count, + ) + self._freeze() + + @classmethod + def de_json( + cls, data: Optional[JSONDict], bot: Optional["Bot"] = None + ) -> Optional["ReactionCount"]: + """See :meth:`telegram.TelegramObject.de_json`.""" + data = cls._parse_data(data) + + if not data: + return None + + data["type"] = ReactionType.de_json(data.get("type"), bot) + + return super().de_json(data=data, bot=bot) diff --git a/_reply.py b/_reply.py new file mode 100644 index 0000000000000000000000000000000000000000..65e42665718d390ac848ebc113f808d554997da6 --- /dev/null +++ b/_reply.py @@ -0,0 +1,471 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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 modules contains objects that represents Telegram Replies""" +from typing import TYPE_CHECKING, Optional, Sequence, Tuple, Union + +from telegram._chat import Chat +from telegram._dice import Dice +from telegram._files.animation import Animation +from telegram._files.audio import Audio +from telegram._files.contact import Contact +from telegram._files.document import Document +from telegram._files.location import Location +from telegram._files.photosize import PhotoSize +from telegram._files.sticker import Sticker +from telegram._files.venue import Venue +from telegram._files.video import Video +from telegram._files.videonote import VideoNote +from telegram._files.voice import Voice +from telegram._games.game import Game +from telegram._giveaway import Giveaway, GiveawayWinners +from telegram._linkpreviewoptions import LinkPreviewOptions +from telegram._messageentity import MessageEntity +from telegram._messageorigin import MessageOrigin +from telegram._paidmedia import PaidMediaInfo +from telegram._payment.invoice import Invoice +from telegram._poll import Poll +from telegram._story import Story +from telegram._telegramobject import TelegramObject +from telegram._utils.argumentparsing import parse_sequence_arg +from telegram._utils.defaultvalue import DEFAULT_NONE +from telegram._utils.types import JSONDict, ODVInput + +if TYPE_CHECKING: + from telegram import Bot + + +class ExternalReplyInfo(TelegramObject): + """ + This object contains information about a message that is being replied to, which may + come from another chat or forum topic. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`origin` is equal. + + .. versionadded:: 20.8 + + Args: + origin (:class:`telegram.MessageOrigin`): Origin of the message replied to by the given + message. + chat (:class:`telegram.Chat`, optional): Chat the original message belongs to. Available + only if the chat is a supergroup or a channel. + message_id (:obj:`int`, optional): Unique message identifier inside the original chat. + Available only if the original chat is a supergroup or a channel. + link_preview_options (:class:`telegram.LinkPreviewOptions`, optional): Options used for + link preview generation for the original message, if it is a text message + animation (:class:`telegram.Animation`, optional): Message is an animation, information + about the animation. + audio (:class:`telegram.Audio`, optional): Message is an audio file, information about the + file. + document (:class:`telegram.Document`, optional): Message is a general file, information + about the file. + photo (Sequence[:class:`telegram.PhotoSize`], optional): Message is a photo, available + sizes of the photo. + sticker (:class:`telegram.Sticker`, optional): Message is a sticker, information about the + sticker. + story (:class:`telegram.Story`, optional): Message is a forwarded story. + video (:class:`telegram.Video`, optional): Message is a video, information about the video. + video_note (:class:`telegram.VideoNote`, optional): Message is a `video note + `_, information about the video + message. + voice (:class:`telegram.Voice`, optional): Message is a voice message, information about + the file. + has_media_spoiler (:obj:`bool`, optional): :obj:`True`, if the message media is covered by + a spoiler animation. + contact (:class:`telegram.Contact`, optional): Message is a shared contact, information + about the contact. + dice (:class:`telegram.Dice`, optional): Message is a dice with random value. + game (:Class:`telegram.Game`. optional): Message is a game, information about the game. + :ref:`More about games >> `. + giveaway (:class:`telegram.Giveaway`, optional): Message is a scheduled giveaway, + information about the giveaway. + giveaway_winners (:class:`telegram.GiveawayWinners`, optional): A giveaway with public + winners was completed. + invoice (:class:`telegram.Invoice`, optional): Message is an invoice for a payment, + information about the invoice. :ref:`More about payments >> `. + location (:class:`telegram.Location`, optional): Message is a shared location, information + about the location. + poll (:class:`telegram.Poll`, optional): Message is a native poll, information about the + poll. + venue (:class:`telegram.Venue`, optional): Message is a venue, information about the venue. + paid_media (:class:`telegram.PaidMedia`, optional): Message contains paid media; + information about the paid media. + + .. versionadded:: 21.4 + + Attributes: + origin (:class:`telegram.MessageOrigin`): Origin of the message replied to by the given + message. + chat (:class:`telegram.Chat`): Optional. Chat the original message belongs to. Available + only if the chat is a supergroup or a channel. + message_id (:obj:`int`): Optional. Unique message identifier inside the original chat. + Available only if the original chat is a supergroup or a channel. + link_preview_options (:class:`telegram.LinkPreviewOptions`): Optional. Options used for + link preview generation for the original message, if it is a text message. + animation (:class:`telegram.Animation`): Optional. Message is an animation, information + about the animation. + audio (:class:`telegram.Audio`): Optional. Message is an audio file, information about the + file. + document (:class:`telegram.Document`): Optional. Message is a general file, information + about the file. + photo (Tuple[:class:`telegram.PhotoSize`]): Optional. Message is a photo, available sizes + of the photo. + sticker (:class:`telegram.Sticker`): Optional. Message is a sticker, information about the + sticker. + story (:class:`telegram.Story`): Optional. Message is a forwarded story. + video (:class:`telegram.Video`): Optional. Message is a video, information about the video. + video_note (:class:`telegram.VideoNote`): Optional. Message is a `video note + `_, information about the video + message. + voice (:class:`telegram.Voice`): Optional. Message is a voice message, information about + the file. + has_media_spoiler (:obj:`bool`): Optional. :obj:`True`, if the message media is covered by + a spoiler animation. + contact (:class:`telegram.Contact`): Optional. Message is a shared contact, information + about the contact. + dice (:class:`telegram.Dice`): Optional. Message is a dice with random value. + game (:Class:`telegram.Game`): Optional. Message is a game, information about the game. + :ref:`More about games >> `. + giveaway (:class:`telegram.Giveaway`): Optional. Message is a scheduled giveaway, + information about the giveaway. + giveaway_winners (:class:`telegram.GiveawayWinners`): Optional. A giveaway with public + winners was completed. + invoice (:class:`telegram.Invoice`): Optional. Message is an invoice for a payment, + information about the invoice. :ref:`More about payments >> `. + location (:class:`telegram.Location`): Optional. Message is a shared location, information + about the location. + poll (:class:`telegram.Poll`): Optional. Message is a native poll, information about the + poll. + venue (:class:`telegram.Venue`): Optional. Message is a venue, information about the venue. + paid_media (:class:`telegram.PaidMedia`): Optional. Message contains paid media; + information about the paid media. + + .. versionadded:: 21.4 + """ + + __slots__ = ( + "animation", + "audio", + "chat", + "contact", + "dice", + "document", + "game", + "giveaway", + "giveaway_winners", + "has_media_spoiler", + "invoice", + "link_preview_options", + "location", + "message_id", + "origin", + "paid_media", + "photo", + "poll", + "sticker", + "story", + "venue", + "video", + "video_note", + "voice", + ) + + def __init__( + self, + origin: MessageOrigin, + chat: Optional[Chat] = None, + message_id: Optional[int] = None, + link_preview_options: Optional[LinkPreviewOptions] = None, + animation: Optional[Animation] = None, + audio: Optional[Audio] = None, + document: Optional[Document] = None, + photo: Optional[Sequence[PhotoSize]] = None, + sticker: Optional[Sticker] = None, + story: Optional[Story] = None, + video: Optional[Video] = None, + video_note: Optional[VideoNote] = None, + voice: Optional[Voice] = None, + has_media_spoiler: Optional[bool] = None, + contact: Optional[Contact] = None, + dice: Optional[Dice] = None, + game: Optional[Game] = None, + giveaway: Optional[Giveaway] = None, + giveaway_winners: Optional[GiveawayWinners] = None, + invoice: Optional[Invoice] = None, + location: Optional[Location] = None, + poll: Optional[Poll] = None, + venue: Optional[Venue] = None, + paid_media: Optional[PaidMediaInfo] = None, + *, + api_kwargs: Optional[JSONDict] = None, + ): + super().__init__(api_kwargs=api_kwargs) + + self.origin: MessageOrigin = origin + self.chat: Optional[Chat] = chat + self.message_id: Optional[int] = message_id + self.link_preview_options: Optional[LinkPreviewOptions] = link_preview_options + self.animation: Optional[Animation] = animation + self.audio: Optional[Audio] = audio + self.document: Optional[Document] = document + self.photo: Optional[Tuple[PhotoSize, ...]] = parse_sequence_arg(photo) + self.sticker: Optional[Sticker] = sticker + self.story: Optional[Story] = story + self.video: Optional[Video] = video + self.video_note: Optional[VideoNote] = video_note + self.voice: Optional[Voice] = voice + self.has_media_spoiler: Optional[bool] = has_media_spoiler + self.contact: Optional[Contact] = contact + self.dice: Optional[Dice] = dice + self.game: Optional[Game] = game + self.giveaway: Optional[Giveaway] = giveaway + self.giveaway_winners: Optional[GiveawayWinners] = giveaway_winners + self.invoice: Optional[Invoice] = invoice + self.location: Optional[Location] = location + self.poll: Optional[Poll] = poll + self.venue: Optional[Venue] = venue + self.paid_media: Optional[PaidMediaInfo] = paid_media + + self._id_attrs = (self.origin,) + + self._freeze() + + @classmethod + def de_json( + cls, data: Optional[JSONDict], bot: Optional["Bot"] = None + ) -> Optional["ExternalReplyInfo"]: + """See :meth:`telegram.TelegramObject.de_json`.""" + data = cls._parse_data(data) + + if data is None: + return None + + data["origin"] = MessageOrigin.de_json(data.get("origin"), bot) + data["chat"] = Chat.de_json(data.get("chat"), bot) + data["link_preview_options"] = LinkPreviewOptions.de_json( + data.get("link_preview_options"), bot + ) + data["animation"] = Animation.de_json(data.get("animation"), bot) + data["audio"] = Audio.de_json(data.get("audio"), bot) + data["document"] = Document.de_json(data.get("document"), bot) + data["photo"] = tuple(PhotoSize.de_list(data.get("photo"), bot)) + data["sticker"] = Sticker.de_json(data.get("sticker"), bot) + data["story"] = Story.de_json(data.get("story"), bot) + data["video"] = Video.de_json(data.get("video"), bot) + data["video_note"] = VideoNote.de_json(data.get("video_note"), bot) + data["voice"] = Voice.de_json(data.get("voice"), bot) + data["contact"] = Contact.de_json(data.get("contact"), bot) + data["dice"] = Dice.de_json(data.get("dice"), bot) + data["game"] = Game.de_json(data.get("game"), bot) + data["giveaway"] = Giveaway.de_json(data.get("giveaway"), bot) + data["giveaway_winners"] = GiveawayWinners.de_json(data.get("giveaway_winners"), bot) + data["invoice"] = Invoice.de_json(data.get("invoice"), bot) + data["location"] = Location.de_json(data.get("location"), bot) + data["poll"] = Poll.de_json(data.get("poll"), bot) + data["venue"] = Venue.de_json(data.get("venue"), bot) + data["paid_media"] = PaidMediaInfo.de_json(data.get("paid_media"), bot) + + return super().de_json(data=data, bot=bot) + + +class TextQuote(TelegramObject): + """ + This object contains information about the quoted part of a message that is replied to + by the given message. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`text` and :attr:`position` are equal. + + .. versionadded:: 20.8 + + Args: + text (:obj:`str`): Text of the quoted part of a message that is replied to by the given + message. + position (:obj:`int`): Approximate quote position in the original message in UTF-16 code + units as specified by the sender. + entities (Sequence[:class:`telegram.MessageEntity`], optional): Special entities that + appear + in the quote. Currently, only bold, italic, underline, strikethrough, spoiler, and + custom_emoji entities are kept in quotes. + is_manual (:obj:`bool`, optional): :obj:`True`, if the quote was chosen manually by the + message sender. Otherwise, the quote was added automatically by the server. + + Attributes: + text (:obj:`str`): Text of the quoted part of a message that is replied to by the given + message. + position (:obj:`int`): Approximate quote position in the original message in UTF-16 code + units as specified by the sender. + entities (Tuple[:class:`telegram.MessageEntity`]): Optional. Special entities that appear + in the quote. Currently, only bold, italic, underline, strikethrough, spoiler, and + custom_emoji entities are kept in quotes. + is_manual (:obj:`bool`): Optional. :obj:`True`, if the quote was chosen manually by the + message sender. Otherwise, the quote was added automatically by the server. + """ + + __slots__ = ( + "entities", + "is_manual", + "position", + "text", + ) + + def __init__( + self, + text: str, + position: int, + entities: Optional[Sequence[MessageEntity]] = None, + is_manual: Optional[bool] = None, + *, + api_kwargs: Optional[JSONDict] = None, + ): + super().__init__(api_kwargs=api_kwargs) + + self.text: str = text + self.position: int = position + self.entities: Optional[Tuple[MessageEntity, ...]] = parse_sequence_arg(entities) + self.is_manual: Optional[bool] = is_manual + + self._id_attrs = ( + self.text, + self.position, + ) + + self._freeze() + + @classmethod + def de_json( + cls, data: Optional[JSONDict], bot: Optional["Bot"] = None + ) -> Optional["TextQuote"]: + """See :meth:`telegram.TelegramObject.de_json`.""" + data = cls._parse_data(data) + + if data is None: + return None + + data["entities"] = tuple(MessageEntity.de_list(data.get("entities"), bot)) + + return super().de_json(data=data, bot=bot) + + +class ReplyParameters(TelegramObject): + """ + Describes reply parameters for the message that is being sent. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`message_id` is equal. + + .. versionadded:: 20.8 + + Args: + message_id (:obj:`int`): Identifier of the message that will be replied to in the current + chat, or in the chat :paramref:`chat_id` if it is specified. + chat_id (:obj:`int` | :obj:`str`, optional): If the message to be replied to is from a + different chat, |chat_id_channel| + Not supported for messages sent on behalf of a business account. + allow_sending_without_reply (:obj:`bool`, optional): |allow_sending_without_reply| Can be + used only for replies in the same chat and forum topic. + quote (:obj:`str`, optional): Quoted part of the message to be replied to; 0-1024 + characters after entities parsing. The quote must be an exact substring of the message + to be replied to, including bold, italic, underline, strikethrough, spoiler, and + custom_emoji entities. The message will fail to send if the quote isn't found in the + original message. + quote_parse_mode (:obj:`str`, optional): Mode for parsing entities in the quote. See + :wiki:`formatting options ` for + more details. + quote_entities (Sequence[:class:`telegram.MessageEntity`], optional): A JSON-serialized + list + of special entities that appear in the quote. It can be specified instead of + :paramref:`quote_parse_mode`. + quote_position (:obj:`int`, optional): Position of the quote in the original message in + UTF-16 code units. + + Attributes: + message_id (:obj:`int`): Identifier of the message that will be replied to in the current + chat, or in the chat :paramref:`chat_id` if it is specified. + chat_id (:obj:`int` | :obj:`str`): Optional. If the message to be replied to is from a + different chat, |chat_id_channel| + Not supported for messages sent on behalf of a business account. + allow_sending_without_reply (:obj:`bool`): Optional. |allow_sending_without_reply| Can be + used only for replies in the same chat and forum topic. + quote (:obj:`str`): Optional. Quoted part of the message to be replied to; 0-1024 + characters after entities parsing. The quote must be an exact substring of the message + to be replied to, including bold, italic, underline, strikethrough, spoiler, and + custom_emoji entities. The message will fail to send if the quote isn't found in the + original message. + quote_parse_mode (:obj:`str`): Optional. Mode for parsing entities in the quote. See + :wiki:`formatting options ` for + more details. + quote_entities (Tuple[:class:`telegram.MessageEntity`]): Optional. A JSON-serialized list + of special entities that appear in the quote. It can be specified instead of + :paramref:`quote_parse_mode`. + quote_position (:obj:`int`): Optional. Position of the quote in the original message in + UTF-16 code units. + """ + + __slots__ = ( + "allow_sending_without_reply", + "chat_id", + "message_id", + "quote", + "quote_entities", + "quote_parse_mode", + "quote_position", + ) + + def __init__( + self, + message_id: int, + chat_id: Optional[Union[int, str]] = None, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + quote: Optional[str] = None, + quote_parse_mode: ODVInput[str] = DEFAULT_NONE, + quote_entities: Optional[Sequence[MessageEntity]] = None, + quote_position: Optional[int] = None, + *, + api_kwargs: Optional[JSONDict] = None, + ): + super().__init__(api_kwargs=api_kwargs) + + self.message_id: int = message_id + self.chat_id: Optional[Union[int, str]] = chat_id + self.allow_sending_without_reply: ODVInput[bool] = allow_sending_without_reply + self.quote: Optional[str] = quote + self.quote_parse_mode: ODVInput[str] = quote_parse_mode + self.quote_entities: Optional[Tuple[MessageEntity, ...]] = parse_sequence_arg( + quote_entities + ) + self.quote_position: Optional[int] = quote_position + + self._id_attrs = (self.message_id,) + + self._freeze() + + @classmethod + def de_json( + cls, data: Optional[JSONDict], bot: Optional["Bot"] = None + ) -> Optional["ReplyParameters"]: + """See :meth:`telegram.TelegramObject.de_json`.""" + data = cls._parse_data(data) + + if data is None: + return None + + data["quote_entities"] = tuple(MessageEntity.de_list(data.get("quote_entities"), bot)) + + return super().de_json(data=data, bot=bot) diff --git a/_replykeyboardmarkup.py b/_replykeyboardmarkup.py new file mode 100644 index 0000000000000000000000000000000000000000..1b410ebc709b736b049fc8aa342740301aa8b919 --- /dev/null +++ b/_replykeyboardmarkup.py @@ -0,0 +1,359 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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 ReplyKeyboardMarkup.""" + +from typing import Final, Optional, Sequence, Tuple, Union + +from telegram import constants +from telegram._keyboardbutton import KeyboardButton +from telegram._telegramobject import TelegramObject +from telegram._utils.markup import check_keyboard_type +from telegram._utils.types import JSONDict + + +class ReplyKeyboardMarkup(TelegramObject): + """This object represents a custom keyboard with reply options. Not supported in channels and + for messages sent on behalf of a Telegram Business account. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their size of :attr:`keyboard` and all the buttons are equal. + + .. figure:: https://core.telegram.org/file/464001950/1191a/2RwpmgU-swU.123554/b5\ + 0478c124d5914c23 + :align: center + + A reply keyboard with reply options. + + .. seealso:: + Another kind of keyboard would be the :class:`telegram.InlineKeyboardMarkup`. + + Examples: + * Example usage: A user requests to change the bot's language, bot replies to the request + with a keyboard to select the new language. Other users in the group don't see + the keyboard. + * :any:`Conversation Bot ` + * :any:`Conversation Bot 2 ` + + Args: + keyboard (Sequence[Sequence[:obj:`str` | :class:`telegram.KeyboardButton`]]): Array of + button rows, each represented by an Array of :class:`telegram.KeyboardButton` objects. + resize_keyboard (:obj:`bool`, optional): Requests clients to resize the keyboard vertically + for optimal fit (e.g., make the keyboard smaller if there are just two rows of + buttons). Defaults to :obj:`False`, in which case the custom keyboard is always of the + same height as the app's standard keyboard. + one_time_keyboard (:obj:`bool`, optional): Requests clients to hide the keyboard as soon as + it's been used. The keyboard will still be available, but clients will automatically + display the usual letter-keyboard in the chat - the user can press a special button in + the input field to see the custom keyboard again. Defaults to :obj:`False`. + selective (:obj:`bool`, optional): Use this parameter if you want to show the keyboard to + specific users only. Targets: + + 1) Users that are @mentioned in the :attr:`~telegram.Message.text` of the + :class:`telegram.Message` object. + 2) If the bot's message is a reply to a message in the same chat and forum topic, + sender of the original message. + + Defaults to :obj:`False`. + + input_field_placeholder (:obj:`str`, optional): The placeholder to be shown in the input + field when the keyboard is active; + :tg-const:`telegram.ReplyKeyboardMarkup.MIN_INPUT_FIELD_PLACEHOLDER`- + :tg-const:`telegram.ReplyKeyboardMarkup.MAX_INPUT_FIELD_PLACEHOLDER` + characters. + + .. versionadded:: 13.7 + is_persistent (:obj:`bool`, optional): Requests clients to always show the keyboard when + the regular keyboard is hidden. Defaults to :obj:`False`, in which case the custom + keyboard can be hidden and opened with a keyboard icon. + + .. versionadded:: 20.0 + + Attributes: + keyboard (Tuple[Tuple[:class:`telegram.KeyboardButton`]]): Array of button rows, + each represented by an Array of :class:`telegram.KeyboardButton` objects. + resize_keyboard (:obj:`bool`): Optional. Requests clients to resize the keyboard vertically + for optimal fit (e.g., make the keyboard smaller if there are just two rows of + buttons). Defaults to :obj:`False`, in which case the custom keyboard is always of the + same height as the app's standard keyboard. + one_time_keyboard (:obj:`bool`): Optional. Requests clients to hide the keyboard as soon as + it's been used. The keyboard will still be available, but clients will automatically + display the usual letter-keyboard in the chat - the user can press a special button in + the input field to see the custom keyboard again. Defaults to :obj:`False`. + selective (:obj:`bool`): Optional. Show the keyboard to specific users only. + Targets: + + 1) Users that are @mentioned in the :attr:`~telegram.Message.text` of the + :class:`telegram.Message` object. + 2) If the bot's message is a reply to a message in the same chat and forum topic, + sender of the original message. + + Defaults to :obj:`False`. + + input_field_placeholder (:obj:`str`): Optional. The placeholder to be shown in the input + field when the keyboard is active; + :tg-const:`telegram.ReplyKeyboardMarkup.MIN_INPUT_FIELD_PLACEHOLDER`- + :tg-const:`telegram.ReplyKeyboardMarkup.MAX_INPUT_FIELD_PLACEHOLDER` + characters. + + .. versionadded:: 13.7 + is_persistent (:obj:`bool`): Optional. Requests clients to always show the keyboard when + the regular keyboard is hidden. If :obj:`False`, the custom keyboard can be hidden and + opened with a keyboard icon. + + .. versionadded:: 20.0 + + """ + + __slots__ = ( + "input_field_placeholder", + "is_persistent", + "keyboard", + "one_time_keyboard", + "resize_keyboard", + "selective", + ) + + def __init__( + self, + keyboard: Sequence[Sequence[Union[str, KeyboardButton]]], + resize_keyboard: Optional[bool] = None, + one_time_keyboard: Optional[bool] = None, + selective: Optional[bool] = None, + input_field_placeholder: Optional[str] = None, + is_persistent: Optional[bool] = None, + *, + api_kwargs: Optional[JSONDict] = None, + ): + super().__init__(api_kwargs=api_kwargs) + if not check_keyboard_type(keyboard): + raise ValueError( + "The parameter `keyboard` should be a sequence of sequences of " + "strings or KeyboardButtons" + ) + + # Required + self.keyboard: Tuple[Tuple[KeyboardButton, ...], ...] = tuple( + tuple(KeyboardButton(button) if isinstance(button, str) else button for button in row) + for row in keyboard + ) + + # Optionals + self.resize_keyboard: Optional[bool] = resize_keyboard + self.one_time_keyboard: Optional[bool] = one_time_keyboard + self.selective: Optional[bool] = selective + self.input_field_placeholder: Optional[str] = input_field_placeholder + self.is_persistent: Optional[bool] = is_persistent + + self._id_attrs = (self.keyboard,) + + self._freeze() + + @classmethod + def from_button( + cls, + button: Union[KeyboardButton, str], + resize_keyboard: bool = False, + one_time_keyboard: bool = False, + selective: bool = False, + input_field_placeholder: Optional[str] = None, + is_persistent: Optional[bool] = None, + **kwargs: object, + ) -> "ReplyKeyboardMarkup": + """Shortcut for:: + + ReplyKeyboardMarkup([[button]], **kwargs) + + Return a ReplyKeyboardMarkup from a single KeyboardButton. + + Args: + button (:class:`telegram.KeyboardButton` | :obj:`str`): The button to use in + the markup. + resize_keyboard (:obj:`bool`, optional): Requests clients to resize the keyboard + vertically for optimal fit (e.g., make the keyboard smaller if there are just two + rows of buttons). Defaults to :obj:`False`, in which case the custom keyboard is + always of the same height as the app's standard keyboard. + one_time_keyboard (:obj:`bool`, optional): Requests clients to hide the keyboard as + soon as it's been used. The keyboard will still be available, but clients will + automatically display the usual letter-keyboard in the chat - the user can press + a special button in the input field to see the custom keyboard again. + Defaults to :obj:`False`. + selective (:obj:`bool`, optional): Use this parameter if you want to show the keyboard + to specific users only. Targets: + + 1) Users that are @mentioned in the text of the Message object. + 2) If the bot's message is a reply to a message in the same chat and forum topic, + sender of the original message. + + Defaults to :obj:`False`. + + input_field_placeholder (:obj:`str`): Optional. The placeholder shown in the input + field when the reply is active. + + .. versionadded:: 13.7 + is_persistent (:obj:`bool`): Optional. Requests clients to always show the keyboard + when the regular keyboard is hidden. Defaults to :obj:`False`, in which case the + custom keyboard can be hidden and opened with a keyboard icon. + + .. versionadded:: 20.0 + """ + return cls( + [[button]], + resize_keyboard=resize_keyboard, + one_time_keyboard=one_time_keyboard, + selective=selective, + input_field_placeholder=input_field_placeholder, + is_persistent=is_persistent, + **kwargs, # type: ignore[arg-type] + ) + + @classmethod + def from_row( + cls, + button_row: Sequence[Union[str, KeyboardButton]], + resize_keyboard: bool = False, + one_time_keyboard: bool = False, + selective: bool = False, + input_field_placeholder: Optional[str] = None, + is_persistent: Optional[bool] = None, + **kwargs: object, + ) -> "ReplyKeyboardMarkup": + """Shortcut for:: + + ReplyKeyboardMarkup([button_row], **kwargs) + + Return a ReplyKeyboardMarkup from a single row of KeyboardButtons. + + Args: + button_row (Sequence[:class:`telegram.KeyboardButton` | :obj:`str`]): The button to + use in the markup. + + .. versionchanged:: 20.0 + |sequenceargs| + resize_keyboard (:obj:`bool`, optional): Requests clients to resize the keyboard + vertically for optimal fit (e.g., make the keyboard smaller if there are just two + rows of buttons). Defaults to :obj:`False`, in which case the custom keyboard is + always of the same height as the app's standard keyboard. + one_time_keyboard (:obj:`bool`, optional): Requests clients to hide the keyboard as + soon as it's been used. The keyboard will still be available, but clients will + automatically display the usual letter-keyboard in the chat - the user can press + a special button in the input field to see the custom keyboard again. + Defaults to :obj:`False`. + selective (:obj:`bool`, optional): Use this parameter if you want to show the keyboard + to specific users only. Targets: + + 1) Users that are @mentioned in the text of the Message object. + 2) If the bot's message is a reply to a message in the same chat and forum topic, + sender of the original message. + + Defaults to :obj:`False`. + + input_field_placeholder (:obj:`str`): Optional. The placeholder shown in the input + field when the reply is active. + + .. versionadded:: 13.7 + is_persistent (:obj:`bool`): Optional. Requests clients to always show the keyboard + when the regular keyboard is hidden. Defaults to :obj:`False`, in which case the + custom keyboard can be hidden and opened with a keyboard icon. + + .. versionadded:: 20.0 + + """ + return cls( + [button_row], + resize_keyboard=resize_keyboard, + one_time_keyboard=one_time_keyboard, + selective=selective, + input_field_placeholder=input_field_placeholder, + is_persistent=is_persistent, + **kwargs, # type: ignore[arg-type] + ) + + @classmethod + def from_column( + cls, + button_column: Sequence[Union[str, KeyboardButton]], + resize_keyboard: bool = False, + one_time_keyboard: bool = False, + selective: bool = False, + input_field_placeholder: Optional[str] = None, + is_persistent: Optional[bool] = None, + **kwargs: object, + ) -> "ReplyKeyboardMarkup": + """Shortcut for:: + + ReplyKeyboardMarkup([[button] for button in button_column], **kwargs) + + Return a ReplyKeyboardMarkup from a single column of KeyboardButtons. + + Args: + button_column (Sequence[:class:`telegram.KeyboardButton` | :obj:`str`]): The button + to use in the markup. + + .. versionchanged:: 20.0 + |sequenceargs| + resize_keyboard (:obj:`bool`, optional): Requests clients to resize the keyboard + vertically for optimal fit (e.g., make the keyboard smaller if there are just two + rows of buttons). Defaults to :obj:`False`, in which case the custom keyboard is + always of the same height as the app's standard keyboard. + one_time_keyboard (:obj:`bool`, optional): Requests clients to hide the keyboard as + soon as it's been used. The keyboard will still be available, but clients will + automatically display the usual letter-keyboard in the chat - the user can press + a special button in the input field to see the custom keyboard again. + Defaults to :obj:`False`. + selective (:obj:`bool`, optional): Use this parameter if you want to show the keyboard + to specific users only. Targets: + + 1) Users that are @mentioned in the text of the Message object. + 2) If the bot's message is a reply to a message in the same chat and forum topic, + sender of the original message. + + Defaults to :obj:`False`. + + input_field_placeholder (:obj:`str`): Optional. The placeholder shown in the input + field when the reply is active. + + .. versionadded:: 13.7 + is_persistent (:obj:`bool`): Optional. Requests clients to always show the keyboard + when the regular keyboard is hidden. Defaults to :obj:`False`, in which case the + custom keyboard can be hidden and opened with a keyboard icon. + + .. versionadded:: 20.0 + + """ + button_grid = [[button] for button in button_column] + return cls( + button_grid, + resize_keyboard=resize_keyboard, + one_time_keyboard=one_time_keyboard, + selective=selective, + input_field_placeholder=input_field_placeholder, + is_persistent=is_persistent, + **kwargs, # type: ignore[arg-type] + ) + + MIN_INPUT_FIELD_PLACEHOLDER: Final[int] = constants.ReplyLimit.MIN_INPUT_FIELD_PLACEHOLDER + """:const:`telegram.constants.ReplyLimit.MIN_INPUT_FIELD_PLACEHOLDER` + + .. versionadded:: 20.0 + """ + MAX_INPUT_FIELD_PLACEHOLDER: Final[int] = constants.ReplyLimit.MAX_INPUT_FIELD_PLACEHOLDER + """:const:`telegram.constants.ReplyLimit.MAX_INPUT_FIELD_PLACEHOLDER` + + .. versionadded:: 20.0 + """ diff --git a/_replykeyboardremove.py b/_replykeyboardremove.py new file mode 100644 index 0000000000000000000000000000000000000000..6cd1a649f4ee02b9dfc2a0923e02b0c7043a3892 --- /dev/null +++ b/_replykeyboardremove.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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 ReplyKeyboardRemove.""" +from typing import Optional + +from telegram._telegramobject import TelegramObject +from telegram._utils.types import JSONDict + + +class ReplyKeyboardRemove(TelegramObject): + """ + Upon receiving a message with this object, Telegram clients will remove the current custom + keyboard and display the default letter-keyboard. By default, custom keyboards are displayed + until a new keyboard is sent by a bot. An exception is made for one-time keyboards that are + hidden immediately after the user presses a button (see :class:`telegram.ReplyKeyboardMarkup`). + Not supported in channels and for messages sent on behalf of a Telegram Business account. + + Note: + User will not be able to summon this keyboard; if you want to hide the keyboard from + sight but keep it accessible, use :attr:`telegram.ReplyKeyboardMarkup.one_time_keyboard`. + + Examples: + * Example usage: A user votes in a poll, bot returns confirmation message in reply to + the vote and removes the keyboard for that user, while still showing the keyboard with + poll options to users who haven't voted yet. + * :any:`Conversation Bot ` + * :any:`Conversation Bot 2 ` + + Args: + selective (:obj:`bool`, optional): Use this parameter if you want to remove the keyboard + for specific users only. Targets: + + 1) Users that are @mentioned in the text of the :class:`telegram.Message` object. + 2) If the bot's message is a reply to a message in the same chat and forum topic, + sender of the original message. + + Attributes: + remove_keyboard (:obj:`True`): Requests clients to remove the custom keyboard. + selective (:obj:`bool`): Optional. Remove the keyboard for specific users only. + Targets: + + 1) Users that are @mentioned in the text of the :class:`telegram.Message` object. + 2) If the bot's message is a reply to a message in the same chat and forum topic, + sender of the original message. + + """ + + __slots__ = ("remove_keyboard", "selective") + + def __init__(self, selective: Optional[bool] = None, *, api_kwargs: Optional[JSONDict] = None): + super().__init__(api_kwargs=api_kwargs) + # Required + self.remove_keyboard: bool = True + # Optionals + self.selective: Optional[bool] = selective + + self._freeze() diff --git a/_sentwebappmessage.py b/_sentwebappmessage.py new file mode 100644 index 0000000000000000000000000000000000000000..28ae55f7516cf917b129ef54c49855022454f101 --- /dev/null +++ b/_sentwebappmessage.py @@ -0,0 +1,56 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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 Sent Web App Message.""" +from typing import Optional + +from telegram._telegramobject import TelegramObject +from telegram._utils.types import JSONDict + + +class SentWebAppMessage(TelegramObject): + """Contains information about an inline message sent by a Web App on behalf of a user. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`inline_message_id` are equal. + + .. versionadded:: 20.0 + + Args: + inline_message_id (:obj:`str`, optional): Identifier of the sent inline message. Available + only if there is an :attr:`inline keyboard ` attached to + the message. + + Attributes: + inline_message_id (:obj:`str`): Optional. Identifier of the sent inline message. Available + only if there is an :attr:`inline keyboard ` attached to + the message. + """ + + __slots__ = ("inline_message_id",) + + def __init__( + self, inline_message_id: Optional[str] = None, *, api_kwargs: Optional[JSONDict] = None + ): + super().__init__(api_kwargs=api_kwargs) + # Optionals + self.inline_message_id: Optional[str] = inline_message_id + + self._id_attrs = (self.inline_message_id,) + + self._freeze() diff --git a/_shared.py b/_shared.py new file mode 100644 index 0000000000000000000000000000000000000000..b4ce2c4d5a0df2c86db12d259f7d7869f656daa5 --- /dev/null +++ b/_shared.py @@ -0,0 +1,267 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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 two objects used for request chats/users service messages.""" +from typing import TYPE_CHECKING, Optional, Sequence, Tuple + +from telegram._files.photosize import PhotoSize +from telegram._telegramobject import TelegramObject +from telegram._utils.argumentparsing import parse_sequence_arg +from telegram._utils.types import JSONDict + +if TYPE_CHECKING: + from telegram._bot import Bot + + +class UsersShared(TelegramObject): + """ + This object contains information about the user whose identifier was shared with the bot + using a :class:`telegram.KeyboardButtonRequestUsers` button. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`request_id` and :attr:`users` are equal. + + .. versionadded:: 20.8 + Bot API 7.0 replaces ``UserShared`` with this class. The only difference is that now + the ``user_ids`` is a sequence instead of a single integer. + + .. versionchanged:: 21.1 + The argument :attr:`users` is now considered for the equality comparison instead of + ``user_ids``. + + .. versionremoved:: 21.2 + Removed the deprecated argument and attribute ``user_ids``. + + Args: + request_id (:obj:`int`): Identifier of the request. + users (Sequence[:class:`telegram.SharedUser`]): Information about users shared with the + bot. + + .. versionadded:: 21.1 + + .. versionchanged:: 21.2 + This argument is now required. + + Attributes: + request_id (:obj:`int`): Identifier of the request. + users (Tuple[:class:`telegram.SharedUser`]): Information about users shared with the + bot. + + .. versionadded:: 21.1 + """ + + __slots__ = ("request_id", "users") + + def __init__( + self, + request_id: int, + users: Sequence["SharedUser"], + *, + api_kwargs: Optional[JSONDict] = None, + ): + super().__init__(api_kwargs=api_kwargs) + self.request_id: int = request_id + self.users: Tuple[SharedUser, ...] = parse_sequence_arg(users) + + self._id_attrs = (self.request_id, self.users) + + self._freeze() + + @classmethod + def de_json( + cls, data: Optional[JSONDict], bot: Optional["Bot"] = None + ) -> Optional["UsersShared"]: + """See :meth:`telegram.TelegramObject.de_json`.""" + data = cls._parse_data(data) + + if not data: + return None + + data["users"] = SharedUser.de_list(data.get("users"), bot) + + 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 + if user_ids := data.get("user_ids"): + api_kwargs = {"user_ids": user_ids} + + return super()._de_json(data=data, bot=bot, api_kwargs=api_kwargs) + + +class ChatShared(TelegramObject): + """ + This object contains information about the chat whose identifier was shared with the bot + using a :class:`telegram.KeyboardButtonRequestChat` button. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`request_id` and :attr:`chat_id` are equal. + + .. versionadded:: 20.1 + + Args: + request_id (:obj:`int`): Identifier of the request. + chat_id (:obj:`int`): Identifier of the shared user. This number may be greater than 32 + bits and some programming languages may have difficulty/silent defects in interpreting + it. But it is smaller than 52 bits, so a signed 64-bit integer or double-precision + float type are safe for storing this identifier. + title (:obj:`str`, optional): Title of the chat, if the title was requested by the bot. + + .. versionadded:: 21.1 + username (:obj:`str`, optional): Username of the chat, if the username was requested by + the bot and available. + + .. versionadded:: 21.1 + photo (Sequence[:class:`telegram.PhotoSize`], optional): Available sizes of the chat photo, + if the photo was requested by the bot + + .. versionadded:: 21.1 + + Attributes: + request_id (:obj:`int`): Identifier of the request. + chat_id (:obj:`int`): Identifier of the shared user. This number may be greater than 32 + bits and some programming languages may have difficulty/silent defects in interpreting + it. But it is smaller than 52 bits, so a signed 64-bit integer or double-precision + float type are safe for storing this identifier. + title (:obj:`str`): Optional. Title of the chat, if the title was requested by the bot. + + .. versionadded:: 21.1 + username (:obj:`str`): Optional. Username of the chat, if the username was requested by + the bot and available. + + .. versionadded:: 21.1 + photo (Tuple[:class:`telegram.PhotoSize`]): Optional. Available sizes of the chat photo, + if the photo was requested by the bot + + .. versionadded:: 21.1 + """ + + __slots__ = ("chat_id", "photo", "request_id", "title", "username") + + def __init__( + self, + request_id: int, + chat_id: int, + title: Optional[str] = None, + username: Optional[str] = None, + photo: Optional[Sequence[PhotoSize]] = None, + *, + api_kwargs: Optional[JSONDict] = None, + ): + super().__init__(api_kwargs=api_kwargs) + self.request_id: int = request_id + self.chat_id: int = chat_id + self.title: Optional[str] = title + self.username: Optional[str] = username + self.photo: Optional[Tuple[PhotoSize, ...]] = parse_sequence_arg(photo) + + self._id_attrs = (self.request_id, self.chat_id) + + self._freeze() + + @classmethod + def de_json( + cls, data: Optional[JSONDict], bot: Optional["Bot"] = None + ) -> Optional["ChatShared"]: + """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) + return super().de_json(data=data, bot=bot) + + +class SharedUser(TelegramObject): + """ + This object contains information about a user that was shared with the bot using a + :class:`telegram.KeyboardButtonRequestUsers` button. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`user_id` is equal. + + .. versionadded:: 21.1 + + Args: + user_id (:obj:`int`): Identifier of the shared user. This number may have 32 significant + bits and some programming languages may have difficulty/silent defects in interpreting + it. But it has atmost 52 significant bits, so 64-bit integers or double-precision + float types are safe for storing these identifiers. The bot may not have access to the + user and could be unable to use this identifier, unless the user is already known to + the bot by some other means. + first_name (:obj:`str`, optional): First name of the user, if the name was requested by the + bot. + last_name (:obj:`str`, optional): Last name of the user, if the name was requested by the + bot. + username (:obj:`str`, optional): Username of the user, if the username was requested by the + bot. + photo (Sequence[:class:`telegram.PhotoSize`], optional): Available sizes of the chat photo, + if the photo was requested by the bot. + + Attributes: + user_id (:obj:`int`): Identifier of the shared user. This number may have 32 significant + bits and some programming languages may have difficulty/silent defects in interpreting + it. But it has atmost 52 significant bits, so 64-bit integers or double-precision + float types are safe for storing these identifiers. The bot may not have access to the + user and could be unable to use this identifier, unless the user is already known to + the bot by some other means. + first_name (:obj:`str`): Optional. First name of the user, if the name was requested by the + bot. + last_name (:obj:`str`): Optional. Last name of the user, if the name was requested by the + bot. + username (:obj:`str`): Optional. Username of the user, if the username was requested by the + bot. + photo (Tuple[:class:`telegram.PhotoSize`]): Available sizes of the chat photo, if + the photo was requested by the bot. This list is empty if the photo was not requsted. + """ + + __slots__ = ("first_name", "last_name", "photo", "user_id", "username") + + def __init__( + self, + user_id: int, + first_name: Optional[str] = None, + last_name: Optional[str] = None, + username: Optional[str] = None, + photo: Optional[Sequence[PhotoSize]] = None, + *, + api_kwargs: Optional[JSONDict] = None, + ): + super().__init__(api_kwargs=api_kwargs) + self.user_id: int = user_id + self.first_name: Optional[str] = first_name + self.last_name: Optional[str] = last_name + self.username: Optional[str] = username + self.photo: Optional[Tuple[PhotoSize, ...]] = parse_sequence_arg(photo) + + self._id_attrs = (self.user_id,) + + self._freeze() + + @classmethod + def de_json( + cls, data: Optional[JSONDict], bot: Optional["Bot"] = None + ) -> Optional["SharedUser"]: + """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) + return super().de_json(data=data, bot=bot) diff --git a/_story.py b/_story.py new file mode 100644 index 0000000000000000000000000000000000000000..40d17cdb16d67395da5f631398a0ac0deea1c3c1 --- /dev/null +++ b/_story.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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 related to a Telegram Story.""" + +from typing import TYPE_CHECKING, Optional + +from telegram._chat import Chat +from telegram._telegramobject import TelegramObject +from telegram._utils.types import JSONDict + +if TYPE_CHECKING: + from telegram import Bot + + +class Story(TelegramObject): + """ + This object represents a story. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`chat` and :attr:`id` are equal. + + .. versionadded:: 20.5 + + .. versionchanged:: 21.0 + Added attributes :attr:`chat` and :attr:`id` and equality based on them. + + Args: + chat (:class:`telegram.Chat`): Chat that posted the story. + id (:obj:`int`): Unique identifier for the story in the chat. + + Attributes: + chat (:class:`telegram.Chat`): Chat that posted the story. + id (:obj:`int`): Unique identifier for the story in the chat. + + """ + + __slots__ = ( + "chat", + "id", + ) + + def __init__( + self, + chat: Chat, + id: int, # pylint: disable=redefined-builtin + *, + api_kwargs: Optional[JSONDict] = None, + ) -> None: + super().__init__(api_kwargs=api_kwargs) + self.chat: Chat = chat + self.id: int = id + + self._id_attrs = (self.chat, self.id) + + self._freeze() + + @classmethod + def de_json(cls, data: Optional[JSONDict], bot: Optional["Bot"] = None) -> Optional["Story"]: + """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) + return super().de_json(data=data, bot=bot) diff --git a/_switchinlinequerychosenchat.py b/_switchinlinequerychosenchat.py new file mode 100644 index 0000000000000000000000000000000000000000..631dbd6798a5ce382da82d051aaefdcff1d362a8 --- /dev/null +++ b/_switchinlinequerychosenchat.py @@ -0,0 +1,100 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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 +"""This module contains a class that represents a Telegram SwitchInlineQueryChosenChat.""" +from typing import Optional + +from telegram._telegramobject import TelegramObject +from telegram._utils.types import JSONDict + + +class SwitchInlineQueryChosenChat(TelegramObject): + """ + This object represents an inline button that switches the current user to inline mode in a + chosen chat, with an optional default inline query. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`query`, :attr:`allow_user_chats`, :attr:`allow_bot_chats`, + :attr:`allow_group_chats`, and :attr:`allow_channel_chats` are equal. + + .. versionadded:: 20.3 + + Caution: + The PTB team has discovered that you must pass at least one of + :paramref:`allow_user_chats`, :paramref:`allow_bot_chats`, :paramref:`allow_group_chats`, + or :paramref:`allow_channel_chats` to Telegram. Otherwise, an error will be raised. + + Args: + query (:obj:`str`, optional): The default inline query to be inserted in the input field. + If left empty, only the bot's username will be inserted. + allow_user_chats (:obj:`bool`, optional): Pass :obj:`True`, if private chats with users + can be chosen. + allow_bot_chats (:obj:`bool`, optional): Pass :obj:`True`, if private chats with bots can + be chosen. + allow_group_chats (:obj:`bool`, optional): Pass :obj:`True`, if group and supergroup chats + can be chosen. + allow_channel_chats (:obj:`bool`, optional): Pass :obj:`True`, if channel chats can be + chosen. + + Attributes: + query (:obj:`str`): Optional. The default inline query to be inserted in the input field. + If left empty, only the bot's username will be inserted. + allow_user_chats (:obj:`bool`): Optional. :obj:`True`, if private chats with users can be + chosen. + allow_bot_chats (:obj:`bool`): Optional. :obj:`True`, if private chats with bots can be + chosen. + allow_group_chats (:obj:`bool`): Optional. :obj:`True`, if group and supergroup chats can + be chosen. + allow_channel_chats (:obj:`bool`): Optional. :obj:`True`, if channel chats can be chosen. + + """ + + __slots__ = ( + "allow_bot_chats", + "allow_channel_chats", + "allow_group_chats", + "allow_user_chats", + "query", + ) + + def __init__( + self, + query: Optional[str] = None, + allow_user_chats: Optional[bool] = None, + allow_bot_chats: Optional[bool] = None, + allow_group_chats: Optional[bool] = None, + allow_channel_chats: Optional[bool] = None, + *, + api_kwargs: Optional[JSONDict] = None, + ): + super().__init__(api_kwargs=api_kwargs) + # Optional + self.query: Optional[str] = query + self.allow_user_chats: Optional[bool] = allow_user_chats + self.allow_bot_chats: Optional[bool] = allow_bot_chats + self.allow_group_chats: Optional[bool] = allow_group_chats + self.allow_channel_chats: Optional[bool] = allow_channel_chats + + self._id_attrs = ( + self.query, + self.allow_user_chats, + self.allow_bot_chats, + self.allow_group_chats, + self.allow_channel_chats, + ) + + self._freeze() diff --git a/_telegramobject.py b/_telegramobject.py new file mode 100644 index 0000000000000000000000000000000000000000..5040755322628ed77aaba84641cc490c7ce598a6 --- /dev/null +++ b/_telegramobject.py @@ -0,0 +1,688 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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/]. +"""Base class for Telegram Objects.""" +import contextlib +import datetime +import inspect +import json +from collections.abc import Sized +from contextlib import contextmanager +from copy import deepcopy +from itertools import chain +from types import MappingProxyType +from typing import ( + TYPE_CHECKING, + Any, + ClassVar, + Dict, + Iterator, + List, + Mapping, + Optional, + Set, + Tuple, + Type, + TypeVar, + Union, + cast, +) + +from telegram._utils.datetime import to_timestamp +from telegram._utils.defaultvalue import DefaultValue +from telegram._utils.types import JSONDict +from telegram._utils.warnings import warn + +if TYPE_CHECKING: + from telegram import Bot + +Tele_co = TypeVar("Tele_co", bound="TelegramObject", covariant=True) + + +class TelegramObject: + """Base class for most Telegram objects. + + Objects of this type are subscriptable with strings. See :meth:`__getitem__` for more details. + The :mod:`pickle` and :func:`~copy.deepcopy` behavior of objects of this type are defined by + :meth:`__getstate__`, :meth:`__setstate__` and :meth:`__deepcopy__`. + + Tip: + Objects of this type can be serialized via Python's :mod:`pickle` module and pickled + objects from one version of PTB are usually loadable in future versions. However, we can + not guarantee that this compatibility will always be provided. At least a manual one-time + conversion of the data may be needed on major updates of the library. + + .. versionchanged:: 20.0 + + * Removed argument and attribute ``bot`` for several subclasses. Use + :meth:`set_bot` and :meth:`get_bot` instead. + * Removed the possibility to pass arbitrary keyword arguments for several subclasses. + * String representations objects of this type was overhauled. See :meth:`__repr__` for + details. As this class doesn't implement :meth:`object.__str__`, the default + implementation will be used, which is equivalent to :meth:`__repr__`. + * Objects of this class (or subclasses) are now immutable. This means that you can't set + or delete attributes anymore. Moreover, attributes that were formerly of type + :obj:`list` are now of type :obj:`tuple`. + + Arguments: + api_kwargs (Dict[:obj:`str`, any], optional): |toapikwargsarg| + + .. versionadded:: 20.0 + + Attributes: + api_kwargs (:obj:`types.MappingProxyType` [:obj:`str`, any]): |toapikwargsattr| + + .. versionadded:: 20.0 + + """ + + __slots__ = ("_bot", "_frozen", "_id_attrs", "api_kwargs") + + # Used to cache the names of the parameters of the __init__ method of the class + # Must be a private attribute to avoid name clashes between subclasses + __INIT_PARAMS: ClassVar[Set[str]] = set() + # Used to check if __INIT_PARAMS has been set for the current class. Unfortunately, we can't + # just check if `__INIT_PARAMS is None`, since subclasses use the parent class' __INIT_PARAMS + # unless it's overridden + __INIT_PARAMS_CHECK: Optional[Type["TelegramObject"]] = None + + def __init__(self, *, api_kwargs: Optional[JSONDict] = None) -> None: + # Setting _frozen to `False` here means that classes without arguments still need to + # implement __init__. However, with `True` would mean increased usage of + # `with self._unfrozen()` in the `__init__` of subclasses and we have fewer empty + # classes than classes with arguments. + self._frozen: bool = False + self._id_attrs: Tuple[object, ...] = () + self._bot: Optional[Bot] = None + # We don't do anything with api_kwargs here - see docstring of _apply_api_kwargs + self.api_kwargs: Mapping[str, Any] = MappingProxyType(api_kwargs or {}) + + def __eq__(self, other: object) -> bool: + """Compares this object with :paramref:`other` in terms of equality. + If this object and :paramref:`other` are `not` objects of the same class, + this comparison will fall back to Python's default implementation of :meth:`object.__eq__`. + Otherwise, both objects may be compared in terms of equality, if the corresponding + subclass of :class:`TelegramObject` has defined a set of attributes to compare and + the objects are considered to be equal, if all of these attributes are equal. + If the subclass has not defined a set of attributes to compare, a warning will be issued. + + Tip: + If instances of a class in the :mod:`telegram` module are comparable in terms of + equality, the documentation of the class will state the attributes that will be used + for this comparison. + + Args: + other (:obj:`object`): The object to compare with. + + Returns: + :obj:`bool` + + """ + if isinstance(other, self.__class__): + if not self._id_attrs: + warn( + f"Objects of type {self.__class__.__name__} can not be meaningfully tested for" + " equivalence.", + stacklevel=2, + ) + if not other._id_attrs: + warn( + f"Objects of type {other.__class__.__name__} can not be meaningfully tested" + " for equivalence.", + stacklevel=2, + ) + return self._id_attrs == other._id_attrs + return super().__eq__(other) + + def __hash__(self) -> int: + """Builds a hash value for this object such that the hash of two objects is equal if and + only if the objects are equal in terms of :meth:`__eq__`. + + Returns: + :obj:`int` + """ + if self._id_attrs: + return hash((self.__class__, self._id_attrs)) + return super().__hash__() + + def __setattr__(self, key: str, value: object) -> None: + """Overrides :meth:`object.__setattr__` to prevent the overriding of attributes. + + Raises: + :exc:`AttributeError` + """ + # protected attributes can always be set for convenient internal use + if key[0] == "_" or not getattr(self, "_frozen", True): + super().__setattr__(key, value) + return + + raise AttributeError( + f"Attribute `{key}` of class `{self.__class__.__name__}` can't be set!" + ) + + def __delattr__(self, key: str) -> None: + """Overrides :meth:`object.__delattr__` to prevent the deletion of attributes. + + Raises: + :exc:`AttributeError` + """ + # protected attributes can always be set for convenient internal use + if key[0] == "_" or not getattr(self, "_frozen", True): + super().__delattr__(key) + return + + raise AttributeError( + f"Attribute `{key}` of class `{self.__class__.__name__}` can't be deleted!" + ) + + def __repr__(self) -> str: + """Gives a string representation of this object in the form + ``ClassName(attr_1=value_1, attr_2=value_2, ...)``, where attributes are omitted if they + have the value :obj:`None` or are empty instances of :class:`collections.abc.Sized` (e.g. + :class:`list`, :class:`dict`, :class:`set`, :class:`str`, etc.). + + As this class doesn't implement :meth:`object.__str__`, the default implementation + will be used, which is equivalent to :meth:`__repr__`. + + Returns: + :obj:`str` + """ + # * `__repr__` goal is to be unambiguous + # * `__str__` goal is to be readable + # * `str()` calls `__repr__`, if `__str__` is not defined + # In our case "unambiguous" and "readable" largely coincide, so we can use the same logic. + as_dict = self._get_attrs(recursive=False, include_private=False) + + if not self.api_kwargs: + # Drop api_kwargs from the representation, if empty + as_dict.pop("api_kwargs", None) + else: + # Otherwise, we want to skip the "mappingproxy" part of the repr + as_dict["api_kwargs"] = dict(self.api_kwargs) + + contents = ", ".join( + f"{k}={as_dict[k]!r}" + for k in sorted(as_dict.keys()) + if ( + as_dict[k] is not None + and not ( + isinstance(as_dict[k], Sized) + and len(as_dict[k]) == 0 # type: ignore[arg-type] + ) + ) + ) + return f"{self.__class__.__name__}({contents})" + + def __getitem__(self, item: str) -> object: + """ + Objects of this type are subscriptable with strings, where + ``telegram_object["attribute_name"]`` is equivalent to ``telegram_object.attribute_name``. + + Tip: + This is useful for dynamic attribute lookup, i.e. ``telegram_object[arg]`` where the + value of ``arg`` is determined at runtime. + In all other cases, it's recommended to use the dot notation instead, i.e. + ``telegram_object.attribute_name``. + + .. versionchanged:: 20.0 + + ``telegram_object['from']`` will look up the key ``from_user``. This is to account for + special cases like :attr:`Message.from_user` that deviate from the official Bot API. + + Args: + item (:obj:`str`): The name of the attribute to look up. + + Returns: + :obj:`object` + + Raises: + :exc:`KeyError`: If the object does not have an attribute with the appropriate name. + """ + if item == "from": + item = "from_user" + try: + return getattr(self, item) + except AttributeError as exc: + raise KeyError( + f"Objects of type {self.__class__.__name__} don't have an attribute called " + f"`{item}`." + ) from exc + + def __getstate__(self) -> Dict[str, Union[str, object]]: + """ + Overrides :meth:`object.__getstate__` to customize the pickling process of objects of this + type. + The returned state does `not` contain the :class:`telegram.Bot` instance set with + :meth:`set_bot` (if any), as it can't be pickled. + + Returns: + state (Dict[:obj:`str`, :obj:`object`]): The state of the object. + """ + out = self._get_attrs( + include_private=True, recursive=False, remove_bot=True, convert_default_vault=False + ) + # MappingProxyType is not pickable, so we convert it to a dict and revert in + # __setstate__ + out["api_kwargs"] = dict(self.api_kwargs) + return out + + def __setstate__(self, state: Dict[str, object]) -> None: + """ + Overrides :meth:`object.__setstate__` to customize the unpickling process of objects of + this type. Modifies the object in-place. + + If any data was stored in the :attr:`api_kwargs` of the pickled object, this method checks + if the class now has dedicated attributes for those keys and moves the values from + :attr:`api_kwargs` to the dedicated attributes. + This can happen, if serialized data is loaded with a new version of this library, where + the new version was updated to account for updates of the Telegram Bot API. + + If on the contrary an attribute was removed from the class, the value is not discarded but + made available via :attr:`api_kwargs`. + + Args: + state (:obj:`dict`): The data to set as attributes of this object. + """ + self._unfreeze() + + # Make sure that we have a `_bot` attribute. This is necessary, since __getstate__ omits + # this as Bots are not pickable. + self._bot = None + + # get api_kwargs first because we may need to add entries to it (see try-except below) + api_kwargs = cast(Dict[str, object], state.pop("api_kwargs", {})) + # get _frozen before the loop to avoid setting it to True in the loop + frozen = state.pop("_frozen", False) + + for key, val in state.items(): + try: + setattr(self, key, val) + except AttributeError: + # So an attribute was deprecated and removed from the class. Let's handle this: + # 1) Is the attribute now a property with no setter? Let's check that: + if isinstance(getattr(self.__class__, key, None), property): + # It is, so let's try to set the "private attribute" instead + try: + setattr(self, f"_{key}", val) + # If this fails as well, guess we've completely removed it. Let's add it to + # api_kwargs as fallback + except AttributeError: + api_kwargs[key] = val + + # 2) The attribute is a private attribute, i.e. it went through case 1) in the past + elif key.startswith("_"): + continue # skip adding this to api_kwargs, the attribute is lost forever. + api_kwargs[key] = val # add it to api_kwargs as fallback + + # For api_kwargs we first apply any kwargs that are already attributes of the object + # and then set the rest as MappingProxyType attribute. Converting to MappingProxyType + # is necessary, since __getstate__ converts it to a dict as MPT is not pickable. + self._apply_api_kwargs(api_kwargs) + self.api_kwargs = MappingProxyType(api_kwargs) + + # Apply freezing if necessary + # we .get(…) the setting for backwards compatibility with objects that were pickled + # before the freeze feature was introduced + if frozen: + self._freeze() + + def __deepcopy__(self: Tele_co, memodict: Dict[int, object]) -> Tele_co: + """ + Customizes how :func:`copy.deepcopy` processes objects of this type. + The only difference to the default implementation is that the :class:`telegram.Bot` + instance set via :meth:`set_bot` (if any) is not copied, but shared between the original + and the copy, i.e.:: + + assert telegram_object.get_bot() is copy.deepcopy(telegram_object).get_bot() + + Args: + memodict (:obj:`dict`): A dictionary that maps objects to their copies. + + Returns: + :class:`telegram.TelegramObject`: The copied object. + """ + bot = self._bot # Save bot so we can set it after copying + self.set_bot(None) # set to None so it is not deepcopied + cls = self.__class__ + result = cls.__new__(cls) # create a new instance + memodict[id(self)] = result # save the id of the object in the dict + + result._frozen = False # unfreeze the new object for setting the attributes + + # now we set the attributes in the deepcopied object + for k in self._get_attrs_names(include_private=True): + if k == "_frozen": + # Setting the frozen status to True would prevent the attributes from being set + continue + if k == "api_kwargs": + # Need to copy api_kwargs manually, since it's a MappingProxyType is not + # pickable and deepcopy uses the pickle interface + setattr(result, k, MappingProxyType(deepcopy(dict(self.api_kwargs), memodict))) + continue + + try: + setattr(result, k, deepcopy(getattr(self, k), memodict)) + except AttributeError: + # Skip missing attributes. This can happen if the object was loaded from a pickle + # file that was created with an older version of the library, where the class + # did not have the attribute yet. + continue + + # Apply freezing if necessary + if self._frozen: + result._freeze() + + result.set_bot(bot) # Assign the bots back + self.set_bot(bot) + return result + + @staticmethod + def _parse_data(data: Optional[JSONDict]) -> Optional[JSONDict]: + """Should be called by subclasses that override de_json to ensure that the input + is not altered. Whoever calls de_json might still want to use the original input + for something else. + """ + return None if data is None else data.copy() + + @classmethod + def _de_json( + cls: Type[Tele_co], + data: Optional[JSONDict], + bot: Optional["Bot"], + api_kwargs: Optional[JSONDict] = None, + ) -> Optional[Tele_co]: + if data is None: + return None + + # try-except is significantly faster in case we already have a correct argument set + try: + obj = cls(**data, api_kwargs=api_kwargs) + except TypeError as exc: + if "__init__() got an unexpected keyword argument" not in str(exc): + raise + + if cls.__INIT_PARAMS_CHECK is not cls: + signature = inspect.signature(cls) + cls.__INIT_PARAMS = set(signature.parameters.keys()) + cls.__INIT_PARAMS_CHECK = cls + + api_kwargs = api_kwargs or {} + existing_kwargs: JSONDict = {} + for key, value in data.items(): + (existing_kwargs if key in cls.__INIT_PARAMS else api_kwargs)[key] = value + + obj = cls(api_kwargs=api_kwargs, **existing_kwargs) + + obj.set_bot(bot=bot) + return obj + + @classmethod + def de_json( + cls: Type[Tele_co], data: Optional[JSONDict], bot: Optional["Bot"] = None + ) -> Optional[Tele_co]: + """Converts JSON data to a Telegram object. + + Args: + 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. + + .. versionchanged:: 21.4 + :paramref:`bot` is now optional and defaults to :obj:`None` + + Returns: + The Telegram object. + + """ + return cls._de_json(data=data, bot=bot) + + @classmethod + def de_list( + cls: Type[Tele_co], data: Optional[List[JSONDict]], bot: Optional["Bot"] = None + ) -> Tuple[Tele_co, ...]: + """Converts a list of JSON objects to a tuple of Telegram objects. + + .. versionchanged:: 20.0 + + * Returns a tuple instead of a list. + * Filters out any :obj:`None` values. + + Args: + data (List[Dict[:obj:`str`, ...]]): The JSON data. + bot (:class:`telegram.Bot`, optional): The bot associated with these object. Defaults + to :obj:`None`, in which case shortcut methods will not be available. + + .. versionchanged:: 21.4 + :paramref:`bot` is now optional and defaults to :obj:`None` + + Returns: + A tuple of Telegram objects. + + """ + if not data: + return () + + return tuple(obj for obj in (cls.de_json(d, bot) for d in data) if obj is not None) + + @contextmanager + def _unfrozen(self: Tele_co) -> Iterator[Tele_co]: + """Context manager to temporarily unfreeze the object. For internal use only. + + Note: + with to._unfrozen() as other_to: + assert to is other_to + """ + self._unfreeze() + yield self + self._freeze() + + def _freeze(self) -> None: + self._frozen = True + + def _unfreeze(self) -> None: + self._frozen = False + + def _apply_api_kwargs(self, api_kwargs: JSONDict) -> None: + """Loops through the api kwargs and for every key that exists as attribute of the + object (and is None), it moves the value from `api_kwargs` to the attribute. + *Edits `api_kwargs` in place!* + + This method is currently only called in the unpickling process, i.e. not on "normal" init. + This is because + * automating this is tricky to get right: It should be called at the *end* of the __init__, + preferably only once at the end of the __init__ of the last child class. This could be + done via __init_subclass__, but it's hard to not destroy the signature of __init__ in the + process. + * calling it manually in every __init__ is tedious + * There probably is no use case for it anyway. If you manually initialize a TO subclass, + then you can pass everything as proper argument. + """ + # we convert to list to ensure that the list doesn't change length while we loop + for key in list(api_kwargs.keys()): + # property attributes are not settable, so we need to set the private attribute + if isinstance(getattr(self.__class__, key, None), property): + # if setattr fails, we'll just leave the value in api_kwargs: + with contextlib.suppress(AttributeError): + setattr(self, f"_{key}", api_kwargs.pop(key)) + elif getattr(self, key, True) is None: + setattr(self, key, api_kwargs.pop(key)) + + def _get_attrs_names(self, include_private: bool) -> Iterator[str]: + """ + Returns the names of the attributes of this object. This is used to determine which + attributes should be serialized when pickling the object. + + Args: + include_private (:obj:`bool`): Whether to include private attributes. + + Returns: + Iterator[:obj:`str`]: An iterator over the names of the attributes of this object. + """ + # We want to get all attributes for the class, using self.__slots__ only includes the + # attributes used by that class itself, and not its superclass(es). Hence, we get its MRO + # and then get their attributes. The `[:-1]` slice excludes the `object` class + all_slots = (s for c in self.__class__.__mro__[:-1] for s in c.__slots__) # type: ignore + # chain the class's slots with the user defined subclass __dict__ (class has no slots) + all_attrs = ( + chain(all_slots, self.__dict__.keys()) if hasattr(self, "__dict__") else all_slots + ) + + if include_private: + return all_attrs + return (attr for attr in all_attrs if not attr.startswith("_")) + + def _get_attrs( + self, + include_private: bool = False, + recursive: bool = False, + remove_bot: bool = False, + convert_default_vault: bool = True, + ) -> Dict[str, Union[str, object]]: + """This method is used for obtaining the attributes of the object. + + Args: + include_private (:obj:`bool`): Whether the result should include private variables. + recursive (:obj:`bool`): If :obj:`True`, will convert any ``TelegramObjects`` (if + found) in the attributes to a dictionary. Else, preserves it as an object itself. + remove_bot (:obj:`bool`): Whether the bot should be included in the result. + convert_default_vault (:obj:`bool`): Whether :class:`telegram.DefaultValue` should be + converted to its true value. This is necessary when converting to a dictionary for + end users since DefaultValue is used in some classes that work with + `tg.ext.defaults` (like `LinkPreviewOptions`) + + Returns: + :obj:`dict`: A dict where the keys are attribute names and values are their values. + """ + data = {} + + for key in self._get_attrs_names(include_private=include_private): + value = ( + DefaultValue.get_value(getattr(self, key, None)) + if convert_default_vault + else getattr(self, key, None) + ) + + if value is not None: + if recursive and hasattr(value, "to_dict"): + data[key] = value.to_dict(recursive=True) + else: + data[key] = value + elif not recursive: + data[key] = value + + if recursive and data.get("from_user"): + data["from"] = data.pop("from_user", None) + if remove_bot: + data.pop("_bot", None) + return data + + def to_json(self) -> str: + """Gives a JSON representation of object. + + .. versionchanged:: 20.0 + Now includes all entries of :attr:`api_kwargs`. + + Returns: + :obj:`str` + """ + return json.dumps(self.to_dict()) + + def to_dict(self, recursive: bool = True) -> JSONDict: + """Gives representation of object as :obj:`dict`. + + .. versionchanged:: 20.0 + + * Now includes all entries of :attr:`api_kwargs`. + * Attributes whose values are empty sequences are no longer included. + + Args: + recursive (:obj:`bool`, optional): If :obj:`True`, will convert any TelegramObjects + (if found) in the attributes to a dictionary. Else, preserves it as an object + itself. Defaults to :obj:`True`. + + .. versionadded:: 20.0 + + Returns: + :obj:`dict` + """ + out = self._get_attrs(recursive=recursive) + + # Now we should convert TGObjects to dicts inside objects such as sequences, and convert + # datetimes to timestamps. This mostly eliminates the need for subclasses to override + # `to_dict` + pop_keys: Set[str] = set() + for key, value in out.items(): + if isinstance(value, (tuple, list)): + if not value: + # not popping directly to avoid changing the dict size during iteration + pop_keys.add(key) + continue + + val = [] # empty list to append our converted values to + for item in value: + if hasattr(item, "to_dict"): + val.append(item.to_dict(recursive=recursive)) + # This branch is useful for e.g. Tuple[Tuple[PhotoSize|KeyboardButton]] + elif isinstance(item, (tuple, list)): + val.append( + [ + i.to_dict(recursive=recursive) if hasattr(i, "to_dict") else i + for i in item + ] + ) + else: # if it's not a TGObject, just append it. E.g. [TGObject, 2] + val.append(item) + out[key] = val + + elif isinstance(value, datetime.datetime): + out[key] = to_timestamp(value) + + for key in pop_keys: + out.pop(key) + + # Effectively "unpack" api_kwargs into `out`: + out.update(out.pop("api_kwargs", {})) # type: ignore[call-overload] + return out + + def get_bot(self) -> "Bot": + """Returns the :class:`telegram.Bot` instance associated with this object. + + .. seealso:: :meth:`set_bot` + + .. versionadded: 20.0 + + Raises: + RuntimeError: If no :class:`telegram.Bot` instance was set for this object. + """ + if self._bot is None: + raise RuntimeError( + "This object has no bot associated with it. Shortcuts cannot be used." + ) + return self._bot + + def set_bot(self, bot: Optional["Bot"]) -> None: + """Sets the :class:`telegram.Bot` instance associated with this object. + + .. seealso:: :meth:`get_bot` + + .. versionadded: 20.0 + + Arguments: + bot (:class:`telegram.Bot` | :obj:`None`): The bot instance. + """ + self._bot = bot diff --git a/_update.py b/_update.py new file mode 100644 index 0000000000000000000000000000000000000000..579cb0085809f43c7d905f5e9d5e1e5e13b5be25 --- /dev/null +++ b/_update.py @@ -0,0 +1,772 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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 Update.""" + +from typing import TYPE_CHECKING, Final, List, Optional, Union + +from telegram import constants +from telegram._business import BusinessConnection, BusinessMessagesDeleted +from telegram._callbackquery import CallbackQuery +from telegram._chatboost import ChatBoostRemoved, ChatBoostUpdated +from telegram._chatjoinrequest import ChatJoinRequest +from telegram._chatmemberupdated import ChatMemberUpdated +from telegram._choseninlineresult import ChosenInlineResult +from telegram._inline.inlinequery import InlineQuery +from telegram._message import Message +from telegram._messagereactionupdated import MessageReactionCountUpdated, MessageReactionUpdated +from telegram._payment.precheckoutquery import PreCheckoutQuery +from telegram._payment.shippingquery import ShippingQuery +from telegram._poll import Poll, PollAnswer +from telegram._telegramobject import TelegramObject +from telegram._utils.types import JSONDict +from telegram._utils.warnings import warn + +if TYPE_CHECKING: + from telegram import Bot, Chat, User + + +class Update(TelegramObject): + """This object represents an incoming update. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`update_id` is equal. + + Note: + At most one of the optional parameters can be present in any given update. + + .. seealso:: :wiki:`Your First Bot ` + + Args: + update_id (:obj:`int`): The update's unique identifier. Update identifiers start from a + certain positive number and increase sequentially. This ID becomes especially handy if + you're using Webhooks, since it allows you to ignore repeated updates or to restore the + correct update sequence, should they get out of order. If there are no new updates for + at least a week, then identifier of the next update will be chosen randomly instead of + sequentially. + message (:class:`telegram.Message`, optional): New incoming message of any kind - text, + photo, sticker, etc. + edited_message (:class:`telegram.Message`, optional): New version of a message that is + known to the bot and was edited. This update may at times be triggered by changes to + message fields that are either unavailable or not actively used by your bot. + channel_post (:class:`telegram.Message`, optional): New incoming channel post of any kind + - text, photo, sticker, etc. + edited_channel_post (:class:`telegram.Message`, optional): New version of a channel post + that is known to the bot and was edited. This update may at times be triggered by + changes to message fields that are either unavailable or not actively used by your bot. + inline_query (:class:`telegram.InlineQuery`, optional): New incoming inline query. + chosen_inline_result (:class:`telegram.ChosenInlineResult`, optional): The result of an + inline query that was chosen by a user and sent to their chat partner. + callback_query (:class:`telegram.CallbackQuery`, optional): New incoming callback query. + shipping_query (:class:`telegram.ShippingQuery`, optional): New incoming shipping query. + Only for invoices with flexible price. + pre_checkout_query (:class:`telegram.PreCheckoutQuery`, optional): New incoming + pre-checkout query. Contains full information about checkout. + poll (:class:`telegram.Poll`, optional): New poll state. Bots receive only updates about + manually stopped polls and polls, which are sent by the bot. + poll_answer (:class:`telegram.PollAnswer`, optional): A user changed their answer + in a non-anonymous poll. Bots receive new votes only in polls that were sent + by the bot itself. + my_chat_member (:class:`telegram.ChatMemberUpdated`, optional): The bot's chat member + status was updated in a chat. For private chats, this update is received only when the + bot is blocked or unblocked by the user. + + .. versionadded:: 13.4 + chat_member (:class:`telegram.ChatMemberUpdated`, optional): A chat member's status was + updated in a chat. The bot must be an administrator in the chat and must explicitly + specify :attr:`CHAT_MEMBER` in the list of + :paramref:`telegram.ext.Application.run_polling.allowed_updates` to receive these + updates (see :meth:`telegram.Bot.get_updates`, :meth:`telegram.Bot.set_webhook`, + :meth:`telegram.ext.Application.run_polling` and + :meth:`telegram.ext.Application.run_webhook`). + + .. versionadded:: 13.4 + chat_join_request (:class:`telegram.ChatJoinRequest`, optional): A request to join the + chat has been sent. The bot must have the + :attr:`telegram.ChatPermissions.can_invite_users` administrator right in the chat to + receive these updates. + + .. versionadded:: 13.8 + + chat_boost (:class:`telegram.ChatBoostUpdated`, optional): A chat boost was added or + changed. The bot must be an administrator in the chat to receive these updates. + + .. versionadded:: 20.8 + + removed_chat_boost (:class:`telegram.ChatBoostRemoved`, optional): A boost was removed from + a chat. The bot must be an administrator in the chat to receive these updates. + + .. versionadded:: 20.8 + + message_reaction (:class:`telegram.MessageReactionUpdated`, optional): A reaction to a + message was changed by a user. The bot must be an administrator in the chat and must + explicitly specify :attr:`MESSAGE_REACTION` in the list of + :paramref:`telegram.ext.Application.run_polling.allowed_updates` to receive these + updates (see :meth:`telegram.Bot.get_updates`, :meth:`telegram.Bot.set_webhook`, + :meth:`telegram.ext.Application.run_polling` and + :meth:`telegram.ext.Application.run_webhook`). The update isn't received for reactions + set by bots. + + .. versionadded:: 20.8 + + message_reaction_count (:class:`telegram.MessageReactionCountUpdated`, optional): Reactions + to a message with anonymous reactions were changed. The bot must be an administrator in + the chat and must explicitly specify :attr:`MESSAGE_REACTION_COUNT` in the list of + :paramref:`telegram.ext.Application.run_polling.allowed_updates` to receive these + updates (see :meth:`telegram.Bot.get_updates`, :meth:`telegram.Bot.set_webhook`, + :meth:`telegram.ext.Application.run_polling` and + :meth:`telegram.ext.Application.run_webhook`). The updates are grouped and can be sent + with delay up to a few minutes. + + .. versionadded:: 20.8 + + business_connection (:class:`telegram.BusinessConnection`, optional): The bot was connected + to or disconnected from a business account, or a user edited an existing connection + with the bot. + + .. versionadded:: 21.1 + + business_message (:class:`telegram.Message`, optional): New message from a connected + business account. + + .. versionadded:: 21.1 + + edited_business_message (:class:`telegram.Message`, optional): New version of a message + from a connected business account. + + .. versionadded:: 21.1 + + deleted_business_messages (:class:`telegram.BusinessMessagesDeleted`, optional): Messages + were deleted from a connected business account. + + .. versionadded:: 21.1 + + + Attributes: + update_id (:obj:`int`): The update's unique identifier. Update identifiers start from a + certain positive number and increase sequentially. This ID becomes especially handy if + you're using Webhooks, since it allows you to ignore repeated updates or to restore the + correct update sequence, should they get out of order. If there are no new updates for + at least a week, then identifier of the next update will be chosen randomly instead of + sequentially. + message (:class:`telegram.Message`): Optional. New incoming message of any kind - text, + photo, sticker, etc. + edited_message (:class:`telegram.Message`): Optional. New version of a message that is + known to the bot and was edited. This update may at times be triggered by changes to + message fields that are either unavailable or not actively used by your bot. + channel_post (:class:`telegram.Message`): Optional. New incoming channel post of any kind + - text, photo, sticker, etc. + edited_channel_post (:class:`telegram.Message`): Optional. New version of a channel post + that is known to the bot and was edited. This update may at times be triggered by + changes to message fields that are either unavailable or not actively used by your bot. + inline_query (:class:`telegram.InlineQuery`): Optional. New incoming inline query. + chosen_inline_result (:class:`telegram.ChosenInlineResult`): Optional. The result of an + inline query that was chosen by a user and sent to their chat partner. + callback_query (:class:`telegram.CallbackQuery`): Optional. New incoming callback query. + + Examples: + :any:`Arbitrary Callback Data Bot ` + shipping_query (:class:`telegram.ShippingQuery`): Optional. New incoming shipping query. + Only for invoices with flexible price. + pre_checkout_query (:class:`telegram.PreCheckoutQuery`): Optional. New incoming + pre-checkout query. Contains full information about checkout. + poll (:class:`telegram.Poll`): Optional. New poll state. Bots receive only updates about + manually stopped polls and polls, which are sent by the bot. + poll_answer (:class:`telegram.PollAnswer`): Optional. A user changed their answer + in a non-anonymous poll. Bots receive new votes only in polls that were sent + by the bot itself. + my_chat_member (:class:`telegram.ChatMemberUpdated`): Optional. The bot's chat member + status was updated in a chat. For private chats, this update is received only when the + bot is blocked or unblocked by the user. + + .. versionadded:: 13.4 + chat_member (:class:`telegram.ChatMemberUpdated`): Optional. A chat member's status was + updated in a chat. The bot must be an administrator in the chat and must explicitly + specify :attr:`CHAT_MEMBER` in the list of + :paramref:`telegram.ext.Application.run_polling.allowed_updates` to receive these + updates (see :meth:`telegram.Bot.get_updates`, :meth:`telegram.Bot.set_webhook`, + :meth:`telegram.ext.Application.run_polling` and + :meth:`telegram.ext.Application.run_webhook`). + + .. versionadded:: 13.4 + chat_join_request (:class:`telegram.ChatJoinRequest`): Optional. A request to join the + chat has been sent. The bot must have the + :attr:`telegram.ChatPermissions.can_invite_users` administrator right in the chat to + receive these updates. + + .. versionadded:: 13.8 + + chat_boost (:class:`telegram.ChatBoostUpdated`): Optional. A chat boost was added or + changed. The bot must be an administrator in the chat to receive these updates. + + .. versionadded:: 20.8 + + removed_chat_boost (:class:`telegram.ChatBoostRemoved`): Optional. A boost was removed from + a chat. The bot must be an administrator in the chat to receive these updates. + + .. versionadded:: 20.8 + + message_reaction (:class:`telegram.MessageReactionUpdated`): Optional. A reaction to a + message was changed by a user. The bot must be an administrator in the chat and must + explicitly specify :attr:`MESSAGE_REACTION` in the list of + :paramref:`telegram.ext.Application.run_polling.allowed_updates` to receive these + updates (see :meth:`telegram.Bot.get_updates`, :meth:`telegram.Bot.set_webhook`, + :meth:`telegram.ext.Application.run_polling` and + :meth:`telegram.ext.Application.run_webhook`). The update isn't received for reactions + set by bots. + + .. versionadded:: 20.8 + + message_reaction_count (:class:`telegram.MessageReactionCountUpdated`): Optional. Reactions + to a message with anonymous reactions were changed. The bot must be an administrator in + the chat and must explicitly specify :attr:`MESSAGE_REACTION_COUNT` in the list of + :paramref:`telegram.ext.Application.run_polling.allowed_updates` to receive these + updates (see :meth:`telegram.Bot.get_updates`, :meth:`telegram.Bot.set_webhook`, + :meth:`telegram.ext.Application.run_polling` and + :meth:`telegram.ext.Application.run_webhook`). The updates are grouped and can be sent + with delay up to a few minutes. + + .. versionadded:: 20.8 + + business_connection (:class:`telegram.BusinessConnection`): Optional. The bot was connected + to or disconnected from a business account, or a user edited an existing connection + with the bot. + + .. versionadded:: 21.1 + + business_message (:class:`telegram.Message`): Optional. New message from a connected + business account. + + .. versionadded:: 21.1 + + edited_business_message (:class:`telegram.Message`): Optional. New version of a message + from a connected business account. + + .. versionadded:: 21.1 + + deleted_business_messages (:class:`telegram.BusinessMessagesDeleted`): Optional. Messages + were deleted from a connected business account. + + .. versionadded:: 21.1 + """ + + __slots__ = ( + "_effective_chat", + "_effective_message", + "_effective_sender", + "_effective_user", + "business_connection", + "business_message", + "callback_query", + "channel_post", + "chat_boost", + "chat_join_request", + "chat_member", + "chosen_inline_result", + "deleted_business_messages", + "edited_business_message", + "edited_channel_post", + "edited_message", + "inline_query", + "message", + "message_reaction", + "message_reaction_count", + "my_chat_member", + "poll", + "poll_answer", + "pre_checkout_query", + "removed_chat_boost", + "shipping_query", + "update_id", + ) + + MESSAGE: Final[str] = constants.UpdateType.MESSAGE + """:const:`telegram.constants.UpdateType.MESSAGE` + + .. versionadded:: 13.5""" + EDITED_MESSAGE: Final[str] = constants.UpdateType.EDITED_MESSAGE + """:const:`telegram.constants.UpdateType.EDITED_MESSAGE` + + .. versionadded:: 13.5""" + CHANNEL_POST: Final[str] = constants.UpdateType.CHANNEL_POST + """:const:`telegram.constants.UpdateType.CHANNEL_POST` + + .. versionadded:: 13.5""" + EDITED_CHANNEL_POST: Final[str] = constants.UpdateType.EDITED_CHANNEL_POST + """:const:`telegram.constants.UpdateType.EDITED_CHANNEL_POST` + + .. versionadded:: 13.5""" + INLINE_QUERY: Final[str] = constants.UpdateType.INLINE_QUERY + """:const:`telegram.constants.UpdateType.INLINE_QUERY` + + .. versionadded:: 13.5""" + CHOSEN_INLINE_RESULT: Final[str] = constants.UpdateType.CHOSEN_INLINE_RESULT + """:const:`telegram.constants.UpdateType.CHOSEN_INLINE_RESULT` + + .. versionadded:: 13.5""" + CALLBACK_QUERY: Final[str] = constants.UpdateType.CALLBACK_QUERY + """:const:`telegram.constants.UpdateType.CALLBACK_QUERY` + + .. versionadded:: 13.5""" + SHIPPING_QUERY: Final[str] = constants.UpdateType.SHIPPING_QUERY + """:const:`telegram.constants.UpdateType.SHIPPING_QUERY` + + .. versionadded:: 13.5""" + PRE_CHECKOUT_QUERY: Final[str] = constants.UpdateType.PRE_CHECKOUT_QUERY + """:const:`telegram.constants.UpdateType.PRE_CHECKOUT_QUERY` + + .. versionadded:: 13.5""" + POLL: Final[str] = constants.UpdateType.POLL + """:const:`telegram.constants.UpdateType.POLL` + + .. versionadded:: 13.5""" + POLL_ANSWER: Final[str] = constants.UpdateType.POLL_ANSWER + """:const:`telegram.constants.UpdateType.POLL_ANSWER` + + .. versionadded:: 13.5""" + MY_CHAT_MEMBER: Final[str] = constants.UpdateType.MY_CHAT_MEMBER + """:const:`telegram.constants.UpdateType.MY_CHAT_MEMBER` + + .. versionadded:: 13.5""" + CHAT_MEMBER: Final[str] = constants.UpdateType.CHAT_MEMBER + """:const:`telegram.constants.UpdateType.CHAT_MEMBER` + + .. versionadded:: 13.5""" + CHAT_JOIN_REQUEST: Final[str] = constants.UpdateType.CHAT_JOIN_REQUEST + """:const:`telegram.constants.UpdateType.CHAT_JOIN_REQUEST` + + .. versionadded:: 13.8""" + CHAT_BOOST: Final[str] = constants.UpdateType.CHAT_BOOST + """:const:`telegram.constants.UpdateType.CHAT_BOOST` + + .. versionadded:: 20.8""" + REMOVED_CHAT_BOOST: Final[str] = constants.UpdateType.REMOVED_CHAT_BOOST + """:const:`telegram.constants.UpdateType.REMOVED_CHAT_BOOST` + + .. versionadded:: 20.8""" + MESSAGE_REACTION: Final[str] = constants.UpdateType.MESSAGE_REACTION + """:const:`telegram.constants.UpdateType.MESSAGE_REACTION` + + .. versionadded:: 20.8""" + MESSAGE_REACTION_COUNT: Final[str] = constants.UpdateType.MESSAGE_REACTION_COUNT + """:const:`telegram.constants.UpdateType.MESSAGE_REACTION_COUNT` + + .. versionadded:: 20.8""" + BUSINESS_CONNECTION: Final[str] = constants.UpdateType.BUSINESS_CONNECTION + """:const:`telegram.constants.UpdateType.BUSINESS_CONNECTION` + + .. versionadded:: 21.1""" + BUSINESS_MESSAGE: Final[str] = constants.UpdateType.BUSINESS_MESSAGE + """:const:`telegram.constants.UpdateType.BUSINESS_MESSAGE` + + .. versionadded:: 21.1""" + EDITED_BUSINESS_MESSAGE: Final[str] = constants.UpdateType.EDITED_BUSINESS_MESSAGE + """:const:`telegram.constants.UpdateType.EDITED_BUSINESS_MESSAGE` + + .. versionadded:: 21.1""" + DELETED_BUSINESS_MESSAGES: Final[str] = constants.UpdateType.DELETED_BUSINESS_MESSAGES + """:const:`telegram.constants.UpdateType.DELETED_BUSINESS_MESSAGES` + + .. versionadded:: 21.1""" + ALL_TYPES: Final[List[str]] = list(constants.UpdateType) + """List[:obj:`str`]: A list of all available update types. + + .. versionadded:: 13.5""" + + def __init__( + self, + update_id: int, + message: Optional[Message] = None, + edited_message: Optional[Message] = None, + channel_post: Optional[Message] = None, + edited_channel_post: Optional[Message] = None, + inline_query: Optional[InlineQuery] = None, + chosen_inline_result: Optional[ChosenInlineResult] = None, + callback_query: Optional[CallbackQuery] = None, + shipping_query: Optional[ShippingQuery] = None, + pre_checkout_query: Optional[PreCheckoutQuery] = None, + poll: Optional[Poll] = None, + poll_answer: Optional[PollAnswer] = None, + my_chat_member: Optional[ChatMemberUpdated] = None, + chat_member: Optional[ChatMemberUpdated] = None, + chat_join_request: Optional[ChatJoinRequest] = None, + chat_boost: Optional[ChatBoostUpdated] = None, + removed_chat_boost: Optional[ChatBoostRemoved] = None, + message_reaction: Optional[MessageReactionUpdated] = None, + message_reaction_count: Optional[MessageReactionCountUpdated] = None, + business_connection: Optional[BusinessConnection] = None, + business_message: Optional[Message] = None, + edited_business_message: Optional[Message] = None, + deleted_business_messages: Optional[BusinessMessagesDeleted] = None, + *, + api_kwargs: Optional[JSONDict] = None, + ): + super().__init__(api_kwargs=api_kwargs) + # Required + self.update_id: int = update_id + # Optionals + self.message: Optional[Message] = message + self.edited_message: Optional[Message] = edited_message + self.inline_query: Optional[InlineQuery] = inline_query + self.chosen_inline_result: Optional[ChosenInlineResult] = chosen_inline_result + self.callback_query: Optional[CallbackQuery] = callback_query + self.shipping_query: Optional[ShippingQuery] = shipping_query + self.pre_checkout_query: Optional[PreCheckoutQuery] = pre_checkout_query + self.channel_post: Optional[Message] = channel_post + self.edited_channel_post: Optional[Message] = edited_channel_post + self.poll: Optional[Poll] = poll + self.poll_answer: Optional[PollAnswer] = poll_answer + self.my_chat_member: Optional[ChatMemberUpdated] = my_chat_member + self.chat_member: Optional[ChatMemberUpdated] = chat_member + self.chat_join_request: Optional[ChatJoinRequest] = chat_join_request + self.chat_boost: Optional[ChatBoostUpdated] = chat_boost + self.removed_chat_boost: Optional[ChatBoostRemoved] = removed_chat_boost + self.message_reaction: Optional[MessageReactionUpdated] = message_reaction + self.message_reaction_count: Optional[MessageReactionCountUpdated] = message_reaction_count + self.business_connection: Optional[BusinessConnection] = business_connection + self.business_message: Optional[Message] = business_message + self.edited_business_message: Optional[Message] = edited_business_message + self.deleted_business_messages: Optional[BusinessMessagesDeleted] = ( + deleted_business_messages + ) + + self._effective_user: Optional[User] = None + self._effective_sender: Optional[Union[User, Chat]] = None + self._effective_chat: Optional[Chat] = None + self._effective_message: Optional[Message] = None + + self._id_attrs = (self.update_id,) + + self._freeze() + + @property + def effective_user(self) -> Optional["User"]: + """ + :class:`telegram.User`: The user that sent this update, no matter what kind of update this + is. If no user is associated with this update, this gives :obj:`None`. This is the case + if any of + + * :attr:`channel_post` + * :attr:`edited_channel_post` + * :attr:`poll` + * :attr:`chat_boost` + * :attr:`removed_chat_boost` + * :attr:`message_reaction_count` + * :attr:`deleted_business_messages` + + is present. + + .. versionchanged:: 21.1 + This property now also considers :attr:`business_connection`, :attr:`business_message` + and :attr:`edited_business_message`. + + Example: + * If :attr:`message` is present, this will give + :attr:`telegram.Message.from_user`. + * If :attr:`poll_answer` is present, this will give :attr:`telegram.PollAnswer.user`. + + """ + if self._effective_user: + return self._effective_user + + user = None + + if self.message: + user = self.message.from_user + + elif self.edited_message: + user = self.edited_message.from_user + + elif self.inline_query: + user = self.inline_query.from_user + + elif self.chosen_inline_result: + user = self.chosen_inline_result.from_user + + elif self.callback_query: + user = self.callback_query.from_user + + elif self.shipping_query: + user = self.shipping_query.from_user + + elif self.pre_checkout_query: + user = self.pre_checkout_query.from_user + + elif self.poll_answer: + user = self.poll_answer.user + + elif self.my_chat_member: + user = self.my_chat_member.from_user + + elif self.chat_member: + user = self.chat_member.from_user + + elif self.chat_join_request: + user = self.chat_join_request.from_user + + elif self.message_reaction: + user = self.message_reaction.user + + elif self.business_message: + user = self.business_message.from_user + + elif self.edited_business_message: + user = self.edited_business_message.from_user + + elif self.business_connection: + user = self.business_connection.user + + self._effective_user = user + return user + + @property + def effective_sender(self) -> Optional[Union["User", "Chat"]]: + """ + :class:`telegram.User` or :class:`telegram.Chat`: The user or chat that sent this update, + no matter what kind of update this is. + + Note: + * Depending on the type of update and the user's 'Remain anonymous' setting, this + could either be :class:`telegram.User`, :class:`telegram.Chat` or :obj:`None`. + + If no user whatsoever is associated with this update, this gives :obj:`None`. This + is the case if any of + + * :attr:`poll` + * :attr:`chat_boost` + * :attr:`removed_chat_boost` + * :attr:`message_reaction_count` + * :attr:`deleted_business_messages` + + is present. + + Example: + * If :attr:`message` is present, this will give either + :attr:`telegram.Message.from_user` or :attr:`telegram.Message.sender_chat`. + * If :attr:`poll_answer` is present, this will give either + :attr:`telegram.PollAnswer.user` or :attr:`telegram.PollAnswer.voter_chat`. + * If :attr:`channel_post` is present, this will give + :attr:`telegram.Message.sender_chat`. + + .. versionadded:: 21.1 + """ + if self._effective_sender: + return self._effective_sender + + sender: Optional[Union[User, Chat]] = None + + if message := ( + self.message + or self.edited_message + or self.channel_post + or self.edited_channel_post + or self.business_message + or self.edited_business_message + ): + sender = message.sender_chat + + elif self.poll_answer: + sender = self.poll_answer.voter_chat + + elif self.message_reaction: + sender = self.message_reaction.actor_chat + + if sender is None: + sender = self.effective_user + + self._effective_sender = sender + return sender + + @property + def effective_chat(self) -> Optional["Chat"]: + """ + :class:`telegram.Chat`: The chat that this update was sent in, no matter what kind of + update this is. + If no chat is associated with this update, this gives :obj:`None`. + This is the case, if :attr:`inline_query`, + :attr:`chosen_inline_result`, :attr:`callback_query` from inline messages, + :attr:`shipping_query`, :attr:`pre_checkout_query`, :attr:`poll`, + :attr:`poll_answer`, or :attr:`business_connection` is present. + + .. versionchanged:: 21.1 + This property now also considers :attr:`business_message`, + :attr:`edited_business_message`, and :attr:`deleted_business_messages`. + + Example: + If :attr:`message` is present, this will give :attr:`telegram.Message.chat`. + + """ + if self._effective_chat: + return self._effective_chat + + chat = None + + if self.message: + chat = self.message.chat + + elif self.edited_message: + chat = self.edited_message.chat + + elif self.callback_query and self.callback_query.message: + chat = self.callback_query.message.chat + + elif self.channel_post: + chat = self.channel_post.chat + + elif self.edited_channel_post: + chat = self.edited_channel_post.chat + + elif self.my_chat_member: + chat = self.my_chat_member.chat + + elif self.chat_member: + chat = self.chat_member.chat + + elif self.chat_join_request: + chat = self.chat_join_request.chat + + elif self.chat_boost: + chat = self.chat_boost.chat + + elif self.removed_chat_boost: + chat = self.removed_chat_boost.chat + + elif self.message_reaction: + chat = self.message_reaction.chat + + elif self.message_reaction_count: + chat = self.message_reaction_count.chat + + elif self.business_message: + chat = self.business_message.chat + + elif self.edited_business_message: + chat = self.edited_business_message.chat + + elif self.deleted_business_messages: + chat = self.deleted_business_messages.chat + + self._effective_chat = chat + return chat + + @property + def effective_message(self) -> Optional[Message]: + """ + :class:`telegram.Message`: The message included in this update, no matter what kind of + update this is. More precisely, this will be the message contained in :attr:`message`, + :attr:`edited_message`, :attr:`channel_post`, :attr:`edited_channel_post` or + :attr:`callback_query` (i.e. :attr:`telegram.CallbackQuery.message`) or :obj:`None`, if + none of those are present. + + .. versionchanged:: 21.1 + This property now also considers :attr:`business_message`, and + :attr:`edited_business_message`. + + Tip: + This property will only ever return objects of type :class:`telegram.Message` or + :obj:`None`, never :class:`telegram.MaybeInaccessibleMessage` or + :class:`telegram.InaccessibleMessage`. + Currently, this is only relevant for :attr:`callback_query`, as + :attr:`telegram.CallbackQuery.message` is the only attribute considered by this + property that can be an object of these types. + """ + if self._effective_message: + return self._effective_message + + message: Optional[Message] = None + + if self.message: + message = self.message + + elif self.edited_message: + message = self.edited_message + + elif self.callback_query: + if ( + isinstance(cbq_message := self.callback_query.message, Message) + or cbq_message is None + ): + message = cbq_message + else: + warn( + ( + "`update.callback_query` is not `None`, but of type " + f"`{cbq_message.__class__.__name__}`. This is not considered by " + "`Update.effective_message`. Please manually access this attribute " + "if necessary." + ), + stacklevel=2, + ) + + elif self.channel_post: + message = self.channel_post + + elif self.edited_channel_post: + message = self.edited_channel_post + + elif self.business_message: + message = self.business_message + + elif self.edited_business_message: + message = self.edited_business_message + + self._effective_message = message + return message + + @classmethod + def de_json(cls, data: Optional[JSONDict], bot: Optional["Bot"] = None) -> Optional["Update"]: + """See :meth:`telegram.TelegramObject.de_json`.""" + data = cls._parse_data(data) + + if not data: + return None + + data["message"] = Message.de_json(data.get("message"), bot) + data["edited_message"] = Message.de_json(data.get("edited_message"), bot) + data["inline_query"] = InlineQuery.de_json(data.get("inline_query"), bot) + data["chosen_inline_result"] = ChosenInlineResult.de_json( + data.get("chosen_inline_result"), bot + ) + data["callback_query"] = CallbackQuery.de_json(data.get("callback_query"), bot) + data["shipping_query"] = ShippingQuery.de_json(data.get("shipping_query"), bot) + data["pre_checkout_query"] = PreCheckoutQuery.de_json(data.get("pre_checkout_query"), bot) + data["channel_post"] = Message.de_json(data.get("channel_post"), bot) + data["edited_channel_post"] = Message.de_json(data.get("edited_channel_post"), bot) + data["poll"] = Poll.de_json(data.get("poll"), bot) + data["poll_answer"] = PollAnswer.de_json(data.get("poll_answer"), bot) + data["my_chat_member"] = ChatMemberUpdated.de_json(data.get("my_chat_member"), bot) + data["chat_member"] = ChatMemberUpdated.de_json(data.get("chat_member"), bot) + data["chat_join_request"] = ChatJoinRequest.de_json(data.get("chat_join_request"), bot) + data["chat_boost"] = ChatBoostUpdated.de_json(data.get("chat_boost"), bot) + data["removed_chat_boost"] = ChatBoostRemoved.de_json(data.get("removed_chat_boost"), bot) + data["message_reaction"] = MessageReactionUpdated.de_json( + data.get("message_reaction"), bot + ) + data["message_reaction_count"] = MessageReactionCountUpdated.de_json( + data.get("message_reaction_count"), bot + ) + data["business_connection"] = BusinessConnection.de_json( + data.get("business_connection"), bot + ) + data["business_message"] = Message.de_json(data.get("business_message"), bot) + data["edited_business_message"] = Message.de_json(data.get("edited_business_message"), bot) + data["deleted_business_messages"] = BusinessMessagesDeleted.de_json( + data.get("deleted_business_messages"), bot + ) + + return super().de_json(data=data, bot=bot) diff --git a/_user.py b/_user.py new file mode 100644 index 0000000000000000000000000000000000000000..075c4f12861d35aca01b728f481d5bc09afffe67 --- /dev/null +++ b/_user.py @@ -0,0 +1,2195 @@ +#!/usr/bin/env python +# pylint: disable=redefined-builtin +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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 User.""" +from datetime import datetime +from typing import TYPE_CHECKING, Optional, Sequence, Tuple, Union + +from telegram._inline.inlinekeyboardbutton import InlineKeyboardButton +from telegram._menubutton import MenuButton +from telegram._telegramobject import TelegramObject +from telegram._utils.defaultvalue import DEFAULT_NONE +from telegram._utils.types import CorrectOptionID, FileInput, JSONDict, ODVInput, ReplyMarkup +from telegram.helpers import mention_html as helpers_mention_html +from telegram.helpers import mention_markdown as helpers_mention_markdown + +if TYPE_CHECKING: + from telegram import ( + Animation, + Audio, + Contact, + Document, + InlineKeyboardMarkup, + InputMediaAudio, + InputMediaDocument, + InputMediaPhoto, + InputMediaVideo, + InputPollOption, + LabeledPrice, + LinkPreviewOptions, + Location, + Message, + MessageEntity, + MessageId, + PhotoSize, + ReplyParameters, + Sticker, + UserChatBoosts, + UserProfilePhotos, + Venue, + Video, + VideoNote, + Voice, + ) + + +class User(TelegramObject): + """This object represents a Telegram user or 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. + + .. versionchanged:: 20.0 + The following are now keyword-only arguments in Bot methods: + ``location``, ``filename``, ``venue``, ``contact``, + ``{read, write, connect, pool}_timeout`` ``api_kwargs``. Use a named argument for those, + and notice that some positional arguments changed position as a result. + + Args: + id (:obj:`int`): Unique identifier for this user or bot. + is_bot (:obj:`bool`): :obj:`True`, if this user is a bot. + first_name (:obj:`str`): User's or bot's first name. + last_name (:obj:`str`, optional): User's or bot's last name. + username (:obj:`str`, optional): User's or bot's username. + language_code (:obj:`str`, optional): IETF language tag of the user's language. + can_join_groups (:obj:`str`, optional): :obj:`True`, if the bot can be invited to groups. + Returned only in :meth:`telegram.Bot.get_me`. + can_read_all_group_messages (:obj:`str`, optional): :obj:`True`, if privacy mode is + disabled for the bot. Returned only in :meth:`telegram.Bot.get_me`. + supports_inline_queries (:obj:`str`, optional): :obj:`True`, if the bot supports inline + queries. Returned only in :meth:`telegram.Bot.get_me`. + + is_premium (:obj:`bool`, optional): :obj:`True`, if this user is a Telegram Premium user. + + .. versionadded:: 20.0 + added_to_attachment_menu (:obj:`bool`, optional): :obj:`True`, if this user added + the bot to the attachment menu. + + .. versionadded:: 20.0 + can_connect_to_business (:obj:`bool`, optional): :obj:`True`, if the bot can be connected + to a Telegram Business account to receive its messages. Returned only in + :meth:`telegram.Bot.get_me`. + + .. versionadded:: 21.1 + has_main_web_app (:obj:`bool`, optional): :obj:`True`, if the bot has the main Web App. + Returned only in :meth:`telegram.Bot.get_me`. + + .. versionadded:: 21.5 + + Attributes: + id (:obj:`int`): Unique identifier for this user or bot. + is_bot (:obj:`bool`): :obj:`True`, if this user is a bot. + first_name (:obj:`str`): User's or bot's first name. + last_name (:obj:`str`): Optional. User's or bot's last name. + username (:obj:`str`): Optional. User's or bot's username. + language_code (:obj:`str`): Optional. IETF language tag of the user's language. + can_join_groups (:obj:`str`): Optional. :obj:`True`, if the bot can be invited to groups. + Returned only in :attr:`telegram.Bot.get_me` requests. + can_read_all_group_messages (:obj:`str`): Optional. :obj:`True`, if privacy mode is + disabled for the bot. Returned only in :attr:`telegram.Bot.get_me` requests. + supports_inline_queries (:obj:`str`): Optional. :obj:`True`, if the bot supports inline + queries. Returned only in :attr:`telegram.Bot.get_me` requests. + is_premium (:obj:`bool`): Optional. :obj:`True`, if this user is a Telegram + Premium user. + + .. versionadded:: 20.0 + added_to_attachment_menu (:obj:`bool`): Optional. :obj:`True`, if this user added + the bot to the attachment menu. + + .. versionadded:: 20.0 + can_connect_to_business (:obj:`bool`): Optional. :obj:`True`, if the bot can be connected + to a Telegram Business account to receive its messages. Returned only in + :meth:`telegram.Bot.get_me`. + + .. versionadded:: 21.1 + has_main_web_app (:obj:`bool`) Optional. :obj:`True`, if the bot has the main Web App. + Returned only in :meth:`telegram.Bot.get_me`. + + .. versionadded:: 21.5 + + .. |user_chat_id_note| replace:: This shortcuts build on the assumption that :attr:`User.id` + coincides with the :attr:`Chat.id` of the private chat with the user. This has been the + case so far, but Telegram does not guarantee that this stays this way. + """ + + __slots__ = ( + "added_to_attachment_menu", + "can_connect_to_business", + "can_join_groups", + "can_read_all_group_messages", + "first_name", + "has_main_web_app", + "id", + "is_bot", + "is_premium", + "language_code", + "last_name", + "supports_inline_queries", + "username", + ) + + def __init__( + self, + id: int, + first_name: str, + is_bot: bool, + last_name: Optional[str] = None, + username: Optional[str] = None, + language_code: Optional[str] = None, + can_join_groups: Optional[bool] = None, + can_read_all_group_messages: Optional[bool] = None, + supports_inline_queries: Optional[bool] = None, + is_premium: Optional[bool] = None, + added_to_attachment_menu: Optional[bool] = None, + can_connect_to_business: Optional[bool] = None, + has_main_web_app: Optional[bool] = None, + *, + api_kwargs: Optional[JSONDict] = None, + ): + super().__init__(api_kwargs=api_kwargs) + # Required + self.id: int = id + self.first_name: str = first_name + self.is_bot: bool = is_bot + # Optionals + self.last_name: Optional[str] = last_name + self.username: Optional[str] = username + self.language_code: Optional[str] = language_code + self.can_join_groups: Optional[bool] = can_join_groups + self.can_read_all_group_messages: Optional[bool] = can_read_all_group_messages + self.supports_inline_queries: Optional[bool] = supports_inline_queries + self.is_premium: Optional[bool] = is_premium + self.added_to_attachment_menu: Optional[bool] = added_to_attachment_menu + self.can_connect_to_business: Optional[bool] = can_connect_to_business + self.has_main_web_app: Optional[bool] = has_main_web_app + + self._id_attrs = (self.id,) + + self._freeze() + + @property + def name(self) -> str: + """:obj:`str`: Convenience property. If available, returns the user's :attr:`username` + prefixed with "@". If :attr:`username` is not available, returns :attr:`full_name`. + """ + if self.username: + return f"@{self.username}" + return self.full_name + + @property + def full_name(self) -> str: + """:obj:`str`: Convenience property. The user's :attr:`first_name`, followed by (if + available) :attr:`last_name`. + """ + if self.last_name: + return f"{self.first_name} {self.last_name}" + return self.first_name + + @property + def link(self) -> Optional[str]: + """:obj:`str`: Convenience property. If :attr:`username` is available, returns a t.me link + of the user. + """ + if self.username: + return f"https://t.me/{self.username}" + return None + + async def get_profile_photos( + self, + offset: Optional[int] = None, + limit: 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, + ) -> Optional["UserProfilePhotos"]: + """Shortcut for:: + + await bot.get_user_profile_photos(update.effective_user.id, *args, **kwargs) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.get_user_profile_photos`. + + """ + return await self.get_bot().get_user_profile_photos( + user_id=self.id, + offset=offset, + limit=limit, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + def mention_markdown(self, name: Optional[str] = None) -> str: + """ + Note: + :tg-const:`telegram.constants.ParseMode.MARKDOWN` is a legacy mode, retained by + Telegram for backward compatibility. You should use :meth:`mention_markdown_v2` + instead. + + Args: + name (:obj:`str`): The name used as a link for the user. Defaults to :attr:`full_name`. + + Returns: + :obj:`str`: The inline mention for the user as markdown (version 1). + + """ + if name: + return helpers_mention_markdown(self.id, name) + return helpers_mention_markdown(self.id, self.full_name) + + def mention_markdown_v2(self, name: Optional[str] = None) -> str: + """ + Args: + name (:obj:`str`): The name used as a link for the user. Defaults to :attr:`full_name`. + + Returns: + :obj:`str`: The inline mention for the user as markdown (version 2). + + """ + if name: + return helpers_mention_markdown(self.id, name, version=2) + return helpers_mention_markdown(self.id, self.full_name, version=2) + + def mention_html(self, name: Optional[str] = None) -> str: + """ + Args: + name (:obj:`str`): The name used as a link for the user. Defaults to :attr:`full_name`. + + Returns: + :obj:`str`: The inline mention for the user as HTML. + + """ + if name: + return helpers_mention_html(self.id, name) + return helpers_mention_html(self.id, self.full_name) + + def mention_button(self, name: Optional[str] = None) -> InlineKeyboardButton: + """Shortcut for:: + + InlineKeyboardButton(text=name, url=f"tg://user?id={update.effective_user.id}") + + .. versionadded:: 13.9 + + Args: + name (:obj:`str`): The name used as a link for the user. Defaults to :attr:`full_name`. + + Returns: + :class:`telegram.InlineKeyboardButton`: InlineButton with url set to the user mention + """ + return InlineKeyboardButton(text=name or self.full_name, url=f"tg://user?id={self.id}") + + 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, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: Optional[JSONDict] = None, + ) -> bool: + """Shortcut for:: + + await bot.pin_chat_message(chat_id=update.effective_user.id, *args, **kwargs) + + For the documentation of the arguments, please see :meth:`telegram.Bot.pin_chat_message`. + + Note: + |user_chat_id_note| + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + """ + return await self.get_bot().pin_chat_message( + chat_id=self.id, + message_id=message_id, + disable_notification=disable_notification, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + business_connection_id=business_connection_id, + api_kwargs=api_kwargs, + ) + + 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, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: Optional[JSONDict] = None, + ) -> bool: + """Shortcut for:: + + await bot.unpin_chat_message(chat_id=update.effective_user.id, *args, **kwargs) + + For the documentation of the arguments, please see :meth:`telegram.Bot.unpin_chat_message`. + + Note: + |user_chat_id_note| + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + """ + return await self.get_bot().unpin_chat_message( + chat_id=self.id, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + message_id=message_id, + business_connection_id=business_connection_id, + ) + + async def unpin_all_messages( + 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.unpin_all_chat_messages(chat_id=update.effective_user.id, *args, **kwargs) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.unpin_all_chat_messages`. + + Note: + |user_chat_id_note| + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + """ + return await self.get_bot().unpin_all_chat_messages( + chat_id=self.id, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def send_message( + self, + text: str, + parse_mode: ODVInput[str] = DEFAULT_NONE, + disable_notification: ODVInput[bool] = DEFAULT_NONE, + reply_markup: Optional[ReplyMarkup] = None, + entities: Optional[Sequence["MessageEntity"]] = None, + protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: Optional[int] = None, + link_preview_options: ODVInput["LinkPreviewOptions"] = DEFAULT_NONE, + reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, + message_effect_id: Optional[str] = None, + *, + reply_to_message_id: Optional[int] = None, + disable_web_page_preview: Optional[bool] = None, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_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, + ) -> "Message": + """Shortcut for:: + + await bot.send_message(update.effective_user.id, *args, **kwargs) + + For the documentation of the arguments, please see :meth:`telegram.Bot.send_message`. + + Note: + |user_chat_id_note| + + Returns: + :class:`telegram.Message`: On success, instance representing the message posted. + + """ + return await self.get_bot().send_message( + chat_id=self.id, + text=text, + parse_mode=parse_mode, + disable_web_page_preview=disable_web_page_preview, + link_preview_options=link_preview_options, + disable_notification=disable_notification, + reply_to_message_id=reply_to_message_id, + reply_parameters=reply_parameters, + reply_markup=reply_markup, + allow_sending_without_reply=allow_sending_without_reply, + entities=entities, + protect_content=protect_content, + message_thread_id=message_thread_id, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + business_connection_id=business_connection_id, + message_effect_id=message_effect_id, + ) + + async def delete_message( + self, + message_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: + """Shortcut for:: + + await bot.delete_message(update.effective_user.id, *argss, **kwargs) + + For the documentation of the arguments, please see :meth:`telegram.Bot.delete_message`. + + .. versionadded:: 20.8 + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + """ + return await self.get_bot().delete_message( + chat_id=self.id, + message_id=message_id, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def delete_messages( + self, + message_ids: Sequence[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: + """Shortcut for:: + + await bot.delete_messages(update.effective_user.id, *argss, **kwargs) + + For the documentation of the arguments, please see :meth:`telegram.Bot.delete_messages`. + + .. versionadded:: 20.8 + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + """ + return await self.get_bot().delete_messages( + chat_id=self.id, + message_ids=message_ids, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def send_photo( + self, + photo: Union[FileInput, "PhotoSize"], + caption: Optional[str] = None, + disable_notification: ODVInput[bool] = DEFAULT_NONE, + reply_markup: Optional[ReplyMarkup] = None, + parse_mode: ODVInput[str] = DEFAULT_NONE, + caption_entities: Optional[Sequence["MessageEntity"]] = None, + protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: Optional[int] = None, + has_spoiler: Optional[bool] = None, + reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, + message_effect_id: Optional[str] = None, + show_caption_above_media: Optional[bool] = None, + *, + reply_to_message_id: Optional[int] = None, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + filename: 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, + ) -> "Message": + """Shortcut for:: + + await bot.send_photo(update.effective_user.id, *args, **kwargs) + + For the documentation of the arguments, please see :meth:`telegram.Bot.send_photo`. + + Note: + |user_chat_id_note| + + Returns: + :class:`telegram.Message`: On success, instance representing the message posted. + + """ + return await self.get_bot().send_photo( + chat_id=self.id, + photo=photo, + caption=caption, + disable_notification=disable_notification, + reply_to_message_id=reply_to_message_id, + reply_parameters=reply_parameters, + reply_markup=reply_markup, + parse_mode=parse_mode, + allow_sending_without_reply=allow_sending_without_reply, + caption_entities=caption_entities, + filename=filename, + protect_content=protect_content, + message_thread_id=message_thread_id, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + has_spoiler=has_spoiler, + business_connection_id=business_connection_id, + message_effect_id=message_effect_id, + show_caption_above_media=show_caption_above_media, + ) + + async def send_media_group( + self, + media: Sequence[ + Union["InputMediaAudio", "InputMediaDocument", "InputMediaPhoto", "InputMediaVideo"] + ], + disable_notification: ODVInput[bool] = DEFAULT_NONE, + protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: Optional[int] = None, + reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, + message_effect_id: Optional[str] = None, + *, + reply_to_message_id: Optional[int] = None, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_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, + caption: Optional[str] = None, + parse_mode: ODVInput[str] = DEFAULT_NONE, + caption_entities: Optional[Sequence["MessageEntity"]] = None, + ) -> Tuple["Message", ...]: + """Shortcut for:: + + await bot.send_media_group(update.effective_user.id, *args, **kwargs) + + For the documentation of the arguments, please see :meth:`telegram.Bot.send_media_group`. + + Note: + |user_chat_id_note| + + Returns: + Tuple[:class:`telegram.Message`:] On success, a tuple of :class:`~telegram.Message` + instances that were sent is returned. + + """ + return await self.get_bot().send_media_group( + chat_id=self.id, + media=media, + disable_notification=disable_notification, + reply_to_message_id=reply_to_message_id, + reply_parameters=reply_parameters, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + allow_sending_without_reply=allow_sending_without_reply, + protect_content=protect_content, + message_thread_id=message_thread_id, + caption=caption, + parse_mode=parse_mode, + caption_entities=caption_entities, + business_connection_id=business_connection_id, + message_effect_id=message_effect_id, + ) + + async def send_audio( + self, + audio: Union[FileInput, "Audio"], + duration: Optional[int] = None, + performer: Optional[str] = None, + title: Optional[str] = None, + caption: Optional[str] = None, + disable_notification: ODVInput[bool] = DEFAULT_NONE, + reply_markup: Optional[ReplyMarkup] = None, + parse_mode: ODVInput[str] = DEFAULT_NONE, + caption_entities: Optional[Sequence["MessageEntity"]] = None, + protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: Optional[int] = None, + thumbnail: Optional[FileInput] = None, + reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, + message_effect_id: Optional[str] = None, + *, + reply_to_message_id: Optional[int] = None, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + filename: 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, + ) -> "Message": + """Shortcut for:: + + await bot.send_audio(update.effective_user.id, *args, **kwargs) + + For the documentation of the arguments, please see :meth:`telegram.Bot.send_audio`. + + Note: + |user_chat_id_note| + + Returns: + :class:`telegram.Message`: On success, instance representing the message posted. + + """ + return await self.get_bot().send_audio( + chat_id=self.id, + audio=audio, + duration=duration, + performer=performer, + title=title, + caption=caption, + disable_notification=disable_notification, + reply_to_message_id=reply_to_message_id, + reply_parameters=reply_parameters, + reply_markup=reply_markup, + parse_mode=parse_mode, + allow_sending_without_reply=allow_sending_without_reply, + caption_entities=caption_entities, + filename=filename, + protect_content=protect_content, + message_thread_id=message_thread_id, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + thumbnail=thumbnail, + business_connection_id=business_connection_id, + message_effect_id=message_effect_id, + ) + + async def send_chat_action( + self, + action: str, + message_thread_id: Optional[int] = None, + business_connection_id: 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.send_chat_action(update.effective_user.id, *args, **kwargs) + + For the documentation of the arguments, please see :meth:`telegram.Bot.send_chat_action`. + + Note: + |user_chat_id_note| + + Returns: + :obj:`True`: On success. + + """ + return await self.get_bot().send_chat_action( + chat_id=self.id, + action=action, + message_thread_id=message_thread_id, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + business_connection_id=business_connection_id, + ) + + send_action = send_chat_action + """Alias for :attr:`send_chat_action`""" + + async def send_contact( + self, + phone_number: Optional[str] = None, + first_name: Optional[str] = None, + last_name: Optional[str] = None, + disable_notification: ODVInput[bool] = DEFAULT_NONE, + reply_markup: Optional[ReplyMarkup] = None, + vcard: Optional[str] = None, + protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: Optional[int] = None, + reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, + message_effect_id: Optional[str] = None, + *, + reply_to_message_id: Optional[int] = None, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + contact: Optional["Contact"] = 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, + ) -> "Message": + """Shortcut for:: + + await bot.send_contact(update.effective_user.id, *args, **kwargs) + + For the documentation of the arguments, please see :meth:`telegram.Bot.send_contact`. + + Note: + |user_chat_id_note| + + Returns: + :class:`telegram.Message`: On success, instance representing the message posted. + + """ + return await self.get_bot().send_contact( + chat_id=self.id, + phone_number=phone_number, + first_name=first_name, + last_name=last_name, + disable_notification=disable_notification, + reply_to_message_id=reply_to_message_id, + reply_parameters=reply_parameters, + reply_markup=reply_markup, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + contact=contact, + vcard=vcard, + api_kwargs=api_kwargs, + allow_sending_without_reply=allow_sending_without_reply, + protect_content=protect_content, + message_thread_id=message_thread_id, + business_connection_id=business_connection_id, + message_effect_id=message_effect_id, + ) + + async def send_dice( + self, + disable_notification: ODVInput[bool] = DEFAULT_NONE, + reply_markup: Optional[ReplyMarkup] = None, + emoji: Optional[str] = None, + protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: Optional[int] = None, + reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, + message_effect_id: Optional[str] = None, + *, + reply_to_message_id: Optional[int] = None, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_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, + ) -> "Message": + """Shortcut for:: + + await bot.send_dice(update.effective_user.id, *args, **kwargs) + + For the documentation of the arguments, please see :meth:`telegram.Bot.send_dice`. + + Note: + |user_chat_id_note| + + Returns: + :class:`telegram.Message`: On success, instance representing the message posted. + + """ + return await self.get_bot().send_dice( + chat_id=self.id, + disable_notification=disable_notification, + reply_to_message_id=reply_to_message_id, + reply_parameters=reply_parameters, + reply_markup=reply_markup, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + emoji=emoji, + api_kwargs=api_kwargs, + allow_sending_without_reply=allow_sending_without_reply, + protect_content=protect_content, + message_thread_id=message_thread_id, + business_connection_id=business_connection_id, + message_effect_id=message_effect_id, + ) + + async def send_document( + self, + document: Union[FileInput, "Document"], + caption: Optional[str] = None, + disable_notification: ODVInput[bool] = DEFAULT_NONE, + reply_markup: Optional[ReplyMarkup] = None, + parse_mode: ODVInput[str] = DEFAULT_NONE, + disable_content_type_detection: Optional[bool] = None, + caption_entities: Optional[Sequence["MessageEntity"]] = None, + protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: Optional[int] = None, + thumbnail: Optional[FileInput] = None, + reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, + message_effect_id: Optional[str] = None, + *, + reply_to_message_id: Optional[int] = None, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + filename: 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, + ) -> "Message": + """Shortcut for:: + + await bot.send_document(update.effective_user.id, *args, **kwargs) + + For the documentation of the arguments, please see :meth:`telegram.Bot.send_document`. + + Note: + |user_chat_id_note| + + Returns: + :class:`telegram.Message`: On success, instance representing the message posted. + + """ + return await self.get_bot().send_document( + chat_id=self.id, + document=document, + filename=filename, + caption=caption, + disable_notification=disable_notification, + reply_to_message_id=reply_to_message_id, + reply_parameters=reply_parameters, + reply_markup=reply_markup, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + parse_mode=parse_mode, + thumbnail=thumbnail, + api_kwargs=api_kwargs, + disable_content_type_detection=disable_content_type_detection, + allow_sending_without_reply=allow_sending_without_reply, + caption_entities=caption_entities, + protect_content=protect_content, + message_thread_id=message_thread_id, + business_connection_id=business_connection_id, + message_effect_id=message_effect_id, + ) + + async def send_game( + self, + game_short_name: str, + disable_notification: ODVInput[bool] = DEFAULT_NONE, + reply_markup: Optional["InlineKeyboardMarkup"] = None, + protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: Optional[int] = None, + reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, + message_effect_id: Optional[str] = None, + *, + reply_to_message_id: Optional[int] = None, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_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, + ) -> "Message": + """Shortcut for:: + + await bot.send_game(update.effective_user.id, *args, **kwargs) + + For the documentation of the arguments, please see :meth:`telegram.Bot.send_game`. + + Note: + |user_chat_id_note| + + Returns: + :class:`telegram.Message`: On success, instance representing the message posted. + + """ + return await self.get_bot().send_game( + chat_id=self.id, + game_short_name=game_short_name, + disable_notification=disable_notification, + reply_to_message_id=reply_to_message_id, + reply_parameters=reply_parameters, + reply_markup=reply_markup, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + allow_sending_without_reply=allow_sending_without_reply, + protect_content=protect_content, + message_thread_id=message_thread_id, + business_connection_id=business_connection_id, + message_effect_id=message_effect_id, + ) + + async def send_invoice( + self, + title: str, + description: str, + payload: str, + provider_token: Optional[str], + currency: str, + prices: Sequence["LabeledPrice"], + start_parameter: Optional[str] = None, + photo_url: Optional[str] = None, + photo_size: Optional[int] = None, + photo_width: Optional[int] = None, + photo_height: Optional[int] = None, + need_name: Optional[bool] = None, + need_phone_number: Optional[bool] = None, + need_email: Optional[bool] = None, + need_shipping_address: Optional[bool] = None, + is_flexible: Optional[bool] = None, + disable_notification: ODVInput[bool] = DEFAULT_NONE, + reply_markup: Optional["InlineKeyboardMarkup"] = None, + provider_data: Optional[Union[str, object]] = None, + send_phone_number_to_provider: Optional[bool] = None, + send_email_to_provider: Optional[bool] = None, + max_tip_amount: Optional[int] = None, + suggested_tip_amounts: Optional[Sequence[int]] = None, + protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: Optional[int] = None, + reply_parameters: Optional["ReplyParameters"] = None, + message_effect_id: Optional[str] = None, + *, + reply_to_message_id: Optional[int] = None, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_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, + ) -> "Message": + """Shortcut for:: + + await bot.send_invoice(update.effective_user.id, *args, **kwargs) + + For the documentation of the arguments, please see :meth:`telegram.Bot.send_invoice`. + + Warning: + As of API 5.2 :paramref:`start_parameter ` + is an optional argument and therefore the + order of the arguments had to be changed. Use keyword arguments to make sure that the + arguments are passed correctly. + + Note: + |user_chat_id_note| + + .. versionchanged:: 13.5 + As of Bot API 5.2, the parameter + :paramref:`start_parameter ` is optional. + + Returns: + :class:`telegram.Message`: On success, instance representing the message posted. + + """ + return await self.get_bot().send_invoice( + chat_id=self.id, + title=title, + description=description, + payload=payload, + provider_token=provider_token, + currency=currency, + prices=prices, + start_parameter=start_parameter, + photo_url=photo_url, + photo_size=photo_size, + photo_width=photo_width, + photo_height=photo_height, + need_name=need_name, + need_phone_number=need_phone_number, + need_email=need_email, + need_shipping_address=need_shipping_address, + is_flexible=is_flexible, + disable_notification=disable_notification, + reply_to_message_id=reply_to_message_id, + reply_parameters=reply_parameters, + reply_markup=reply_markup, + provider_data=provider_data, + send_phone_number_to_provider=send_phone_number_to_provider, + send_email_to_provider=send_email_to_provider, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + allow_sending_without_reply=allow_sending_without_reply, + max_tip_amount=max_tip_amount, + suggested_tip_amounts=suggested_tip_amounts, + protect_content=protect_content, + message_thread_id=message_thread_id, + message_effect_id=message_effect_id, + ) + + async def send_location( + self, + latitude: Optional[float] = None, + longitude: Optional[float] = None, + disable_notification: ODVInput[bool] = DEFAULT_NONE, + reply_markup: Optional[ReplyMarkup] = None, + live_period: Optional[int] = None, + horizontal_accuracy: Optional[float] = None, + heading: Optional[int] = None, + proximity_alert_radius: Optional[int] = None, + protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: Optional[int] = None, + reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, + message_effect_id: Optional[str] = None, + *, + reply_to_message_id: Optional[int] = None, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + location: Optional["Location"] = 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, + ) -> "Message": + """Shortcut for:: + + await bot.send_location(update.effective_user.id, *args, **kwargs) + + For the documentation of the arguments, please see :meth:`telegram.Bot.send_location`. + + Note: + |user_chat_id_note| + + Returns: + :class:`telegram.Message`: On success, instance representing the message posted. + + """ + return await self.get_bot().send_location( + chat_id=self.id, + latitude=latitude, + longitude=longitude, + disable_notification=disable_notification, + reply_to_message_id=reply_to_message_id, + reply_parameters=reply_parameters, + reply_markup=reply_markup, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + location=location, + live_period=live_period, + api_kwargs=api_kwargs, + horizontal_accuracy=horizontal_accuracy, + heading=heading, + proximity_alert_radius=proximity_alert_radius, + allow_sending_without_reply=allow_sending_without_reply, + protect_content=protect_content, + message_thread_id=message_thread_id, + business_connection_id=business_connection_id, + message_effect_id=message_effect_id, + ) + + async def send_animation( + self, + animation: Union[FileInput, "Animation"], + duration: Optional[int] = None, + width: Optional[int] = None, + height: Optional[int] = None, + caption: Optional[str] = None, + parse_mode: ODVInput[str] = DEFAULT_NONE, + disable_notification: ODVInput[bool] = DEFAULT_NONE, + reply_markup: Optional[ReplyMarkup] = None, + caption_entities: Optional[Sequence["MessageEntity"]] = None, + protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: Optional[int] = None, + has_spoiler: Optional[bool] = None, + thumbnail: Optional[FileInput] = None, + reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, + message_effect_id: Optional[str] = None, + show_caption_above_media: Optional[bool] = None, + *, + reply_to_message_id: Optional[int] = None, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + filename: 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, + ) -> "Message": + """Shortcut for:: + + await bot.send_animation(update.effective_user.id, *args, **kwargs) + + For the documentation of the arguments, please see :meth:`telegram.Bot.send_animation`. + + Note: + |user_chat_id_note| + + Returns: + :class:`telegram.Message`: On success, instance representing the message posted. + + """ + return await self.get_bot().send_animation( + chat_id=self.id, + animation=animation, + duration=duration, + width=width, + height=height, + caption=caption, + parse_mode=parse_mode, + disable_notification=disable_notification, + reply_to_message_id=reply_to_message_id, + reply_parameters=reply_parameters, + reply_markup=reply_markup, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + allow_sending_without_reply=allow_sending_without_reply, + caption_entities=caption_entities, + filename=filename, + protect_content=protect_content, + message_thread_id=message_thread_id, + has_spoiler=has_spoiler, + thumbnail=thumbnail, + business_connection_id=business_connection_id, + message_effect_id=message_effect_id, + show_caption_above_media=show_caption_above_media, + ) + + async def send_sticker( + self, + sticker: Union[FileInput, "Sticker"], + disable_notification: ODVInput[bool] = DEFAULT_NONE, + reply_markup: Optional[ReplyMarkup] = None, + protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: Optional[int] = None, + emoji: Optional[str] = None, + reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, + message_effect_id: Optional[str] = None, + *, + reply_to_message_id: Optional[int] = None, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_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, + ) -> "Message": + """Shortcut for:: + + await bot.send_sticker(update.effective_user.id, *args, **kwargs) + + For the documentation of the arguments, please see :meth:`telegram.Bot.send_sticker`. + + Note: + |user_chat_id_note| + + Returns: + :class:`telegram.Message`: On success, instance representing the message posted. + + """ + return await self.get_bot().send_sticker( + chat_id=self.id, + sticker=sticker, + disable_notification=disable_notification, + reply_to_message_id=reply_to_message_id, + reply_parameters=reply_parameters, + reply_markup=reply_markup, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + allow_sending_without_reply=allow_sending_without_reply, + protect_content=protect_content, + message_thread_id=message_thread_id, + emoji=emoji, + business_connection_id=business_connection_id, + message_effect_id=message_effect_id, + ) + + async def send_video( + self, + video: Union[FileInput, "Video"], + duration: Optional[int] = None, + caption: Optional[str] = None, + disable_notification: ODVInput[bool] = DEFAULT_NONE, + reply_markup: Optional[ReplyMarkup] = None, + width: Optional[int] = None, + height: Optional[int] = None, + parse_mode: ODVInput[str] = DEFAULT_NONE, + supports_streaming: Optional[bool] = None, + caption_entities: Optional[Sequence["MessageEntity"]] = None, + protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: Optional[int] = None, + has_spoiler: Optional[bool] = None, + thumbnail: Optional[FileInput] = None, + reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, + message_effect_id: Optional[str] = None, + show_caption_above_media: Optional[bool] = None, + *, + reply_to_message_id: Optional[int] = None, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + filename: 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, + ) -> "Message": + """Shortcut for:: + + await bot.send_video(update.effective_user.id, *args, **kwargs) + + For the documentation of the arguments, please see :meth:`telegram.Bot.send_video`. + + Note: + |user_chat_id_note| + + Returns: + :class:`telegram.Message`: On success, instance representing the message posted. + + """ + return await self.get_bot().send_video( + chat_id=self.id, + video=video, + duration=duration, + caption=caption, + disable_notification=disable_notification, + reply_to_message_id=reply_to_message_id, + reply_parameters=reply_parameters, + reply_markup=reply_markup, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + width=width, + height=height, + parse_mode=parse_mode, + supports_streaming=supports_streaming, + thumbnail=thumbnail, + api_kwargs=api_kwargs, + allow_sending_without_reply=allow_sending_without_reply, + caption_entities=caption_entities, + filename=filename, + protect_content=protect_content, + message_thread_id=message_thread_id, + has_spoiler=has_spoiler, + business_connection_id=business_connection_id, + message_effect_id=message_effect_id, + show_caption_above_media=show_caption_above_media, + ) + + async def send_venue( + self, + latitude: Optional[float] = None, + longitude: Optional[float] = None, + title: Optional[str] = None, + address: Optional[str] = None, + foursquare_id: Optional[str] = None, + disable_notification: ODVInput[bool] = DEFAULT_NONE, + reply_markup: Optional[ReplyMarkup] = None, + foursquare_type: Optional[str] = None, + google_place_id: Optional[str] = None, + google_place_type: Optional[str] = None, + protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: Optional[int] = None, + reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, + message_effect_id: Optional[str] = None, + *, + reply_to_message_id: Optional[int] = None, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + venue: Optional["Venue"] = 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, + ) -> "Message": + """Shortcut for:: + + await bot.send_venue(update.effective_user.id, *args, **kwargs) + + For the documentation of the arguments, please see :meth:`telegram.Bot.send_venue`. + + Note: + |user_chat_id_note| + + Returns: + :class:`telegram.Message`: On success, instance representing the message posted. + + """ + return await self.get_bot().send_venue( + chat_id=self.id, + latitude=latitude, + longitude=longitude, + title=title, + address=address, + foursquare_id=foursquare_id, + disable_notification=disable_notification, + reply_to_message_id=reply_to_message_id, + reply_parameters=reply_parameters, + reply_markup=reply_markup, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + venue=venue, + foursquare_type=foursquare_type, + api_kwargs=api_kwargs, + google_place_id=google_place_id, + google_place_type=google_place_type, + allow_sending_without_reply=allow_sending_without_reply, + protect_content=protect_content, + message_thread_id=message_thread_id, + business_connection_id=business_connection_id, + message_effect_id=message_effect_id, + ) + + async def send_video_note( + self, + video_note: Union[FileInput, "VideoNote"], + duration: Optional[int] = None, + length: Optional[int] = None, + disable_notification: ODVInput[bool] = DEFAULT_NONE, + reply_markup: Optional[ReplyMarkup] = None, + protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: Optional[int] = None, + thumbnail: Optional[FileInput] = None, + reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, + message_effect_id: Optional[str] = None, + *, + reply_to_message_id: Optional[int] = None, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + filename: 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, + ) -> "Message": + """Shortcut for:: + + await bot.send_video_note(update.effective_user.id, *args, **kwargs) + + For the documentation of the arguments, please see :meth:`telegram.Bot.send_video_note`. + + Note: + |user_chat_id_note| + + Returns: + :class:`telegram.Message`: On success, instance representing the message posted. + + """ + return await self.get_bot().send_video_note( + chat_id=self.id, + video_note=video_note, + duration=duration, + length=length, + disable_notification=disable_notification, + reply_to_message_id=reply_to_message_id, + reply_parameters=reply_parameters, + reply_markup=reply_markup, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + allow_sending_without_reply=allow_sending_without_reply, + filename=filename, + protect_content=protect_content, + message_thread_id=message_thread_id, + thumbnail=thumbnail, + business_connection_id=business_connection_id, + message_effect_id=message_effect_id, + ) + + async def send_voice( + self, + voice: Union[FileInput, "Voice"], + duration: Optional[int] = None, + caption: Optional[str] = None, + disable_notification: ODVInput[bool] = DEFAULT_NONE, + reply_markup: Optional[ReplyMarkup] = None, + parse_mode: ODVInput[str] = DEFAULT_NONE, + caption_entities: Optional[Sequence["MessageEntity"]] = None, + protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: Optional[int] = None, + reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, + message_effect_id: Optional[str] = None, + *, + reply_to_message_id: Optional[int] = None, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, + filename: 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, + ) -> "Message": + """Shortcut for:: + + await bot.send_voice(update.effective_user.id, *args, **kwargs) + + For the documentation of the arguments, please see :meth:`telegram.Bot.send_voice`. + + Note: + |user_chat_id_note| + + Returns: + :class:`telegram.Message`: On success, instance representing the message posted. + + """ + return await self.get_bot().send_voice( + chat_id=self.id, + voice=voice, + duration=duration, + caption=caption, + disable_notification=disable_notification, + reply_to_message_id=reply_to_message_id, + reply_parameters=reply_parameters, + reply_markup=reply_markup, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + parse_mode=parse_mode, + api_kwargs=api_kwargs, + allow_sending_without_reply=allow_sending_without_reply, + caption_entities=caption_entities, + filename=filename, + protect_content=protect_content, + message_thread_id=message_thread_id, + business_connection_id=business_connection_id, + message_effect_id=message_effect_id, + ) + + async def send_poll( + self, + question: str, + options: Sequence[Union[str, "InputPollOption"]], + is_anonymous: Optional[bool] = None, + type: Optional[str] = None, + allows_multiple_answers: Optional[bool] = None, + correct_option_id: Optional[CorrectOptionID] = None, + is_closed: Optional[bool] = None, + disable_notification: ODVInput[bool] = DEFAULT_NONE, + 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, + explanation_entities: Optional[Sequence["MessageEntity"]] = None, + protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: Optional[int] = None, + reply_parameters: Optional["ReplyParameters"] = None, + business_connection_id: Optional[str] = None, + question_parse_mode: ODVInput[str] = DEFAULT_NONE, + question_entities: Optional[Sequence["MessageEntity"]] = None, + message_effect_id: Optional[str] = None, + *, + reply_to_message_id: Optional[int] = None, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_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, + ) -> "Message": + """Shortcut for:: + + await bot.send_poll(update.effective_user.id, *args, **kwargs) + + For the documentation of the arguments, please see :meth:`telegram.Bot.send_poll`. + + Note: + |user_chat_id_note| + + Returns: + :class:`telegram.Message`: On success, instance representing the message posted. + + """ + return await self.get_bot().send_poll( + chat_id=self.id, + question=question, + options=options, + is_anonymous=is_anonymous, + type=type, # pylint=pylint, + allows_multiple_answers=allows_multiple_answers, + correct_option_id=correct_option_id, + is_closed=is_closed, + disable_notification=disable_notification, + reply_to_message_id=reply_to_message_id, + reply_parameters=reply_parameters, + reply_markup=reply_markup, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + explanation=explanation, + explanation_parse_mode=explanation_parse_mode, + open_period=open_period, + close_date=close_date, + api_kwargs=api_kwargs, + allow_sending_without_reply=allow_sending_without_reply, + explanation_entities=explanation_entities, + protect_content=protect_content, + message_thread_id=message_thread_id, + business_connection_id=business_connection_id, + question_parse_mode=question_parse_mode, + question_entities=question_entities, + message_effect_id=message_effect_id, + ) + + async def send_copy( + self, + from_chat_id: Union[str, int], + message_id: int, + caption: Optional[str] = None, + parse_mode: ODVInput[str] = DEFAULT_NONE, + caption_entities: Optional[Sequence["MessageEntity"]] = None, + disable_notification: ODVInput[bool] = DEFAULT_NONE, + reply_markup: Optional[ReplyMarkup] = None, + protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: Optional[int] = None, + reply_parameters: Optional["ReplyParameters"] = None, + show_caption_above_media: Optional[bool] = None, + *, + reply_to_message_id: Optional[int] = None, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_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, + ) -> "MessageId": + """Shortcut for:: + + await bot.copy_message(chat_id=update.effective_user.id, *args, **kwargs) + + For the documentation of the arguments, please see :meth:`telegram.Bot.copy_message`. + + Note: + |user_chat_id_note| + + Returns: + :class:`telegram.Message`: On success, instance representing the message posted. + + """ + return await self.get_bot().copy_message( + chat_id=self.id, + from_chat_id=from_chat_id, + message_id=message_id, + caption=caption, + parse_mode=parse_mode, + caption_entities=caption_entities, + disable_notification=disable_notification, + reply_to_message_id=reply_to_message_id, + reply_parameters=reply_parameters, + allow_sending_without_reply=allow_sending_without_reply, + reply_markup=reply_markup, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + protect_content=protect_content, + message_thread_id=message_thread_id, + show_caption_above_media=show_caption_above_media, + ) + + async def copy_message( + self, + chat_id: Union[int, str], + message_id: int, + caption: Optional[str] = None, + parse_mode: ODVInput[str] = DEFAULT_NONE, + caption_entities: Optional[Sequence["MessageEntity"]] = None, + disable_notification: ODVInput[bool] = DEFAULT_NONE, + reply_markup: Optional[ReplyMarkup] = None, + protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: Optional[int] = None, + reply_parameters: Optional["ReplyParameters"] = None, + show_caption_above_media: Optional[bool] = None, + *, + reply_to_message_id: Optional[int] = None, + allow_sending_without_reply: ODVInput[bool] = DEFAULT_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, + ) -> "MessageId": + """Shortcut for:: + + await bot.copy_message(from_chat_id=update.effective_user.id, *args, **kwargs) + + For the documentation of the arguments, please see :meth:`telegram.Bot.copy_message`. + + Note: + |user_chat_id_note| + + Returns: + :class:`telegram.MessageId`: On success, returns the MessageId of the sent message. + + """ + return await self.get_bot().copy_message( + from_chat_id=self.id, + chat_id=chat_id, + message_id=message_id, + caption=caption, + parse_mode=parse_mode, + caption_entities=caption_entities, + disable_notification=disable_notification, + reply_to_message_id=reply_to_message_id, + reply_parameters=reply_parameters, + allow_sending_without_reply=allow_sending_without_reply, + reply_markup=reply_markup, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + protect_content=protect_content, + message_thread_id=message_thread_id, + show_caption_above_media=show_caption_above_media, + ) + + async def send_copies( + self, + from_chat_id: Union[str, int], + message_ids: Sequence[int], + disable_notification: ODVInput[bool] = DEFAULT_NONE, + protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: Optional[int] = None, + remove_caption: 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, + ) -> Tuple["MessageId", ...]: + """Shortcut for:: + + await bot.copy_messages(chat_id=update.effective_user.id, *argss, **kwargs) + + For the documentation of the arguments, please see :meth:`telegram.Bot.copy_messages`. + + .. seealso:: :meth:`copy_message`, :meth:`send_copy`, :meth:`copy_messages`. + + .. versionadded:: 20.8 + + Returns: + Tuple[:class:`telegram.MessageId`]: On success, a tuple of :class:`~telegram.MessageId` + of the sent messages is returned. + + """ + return await self.get_bot().copy_messages( + chat_id=self.id, + from_chat_id=from_chat_id, + message_ids=message_ids, + disable_notification=disable_notification, + protect_content=protect_content, + message_thread_id=message_thread_id, + remove_caption=remove_caption, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def copy_messages( + self, + chat_id: Union[str, int], + message_ids: Sequence[int], + disable_notification: ODVInput[bool] = DEFAULT_NONE, + protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_id: Optional[int] = None, + remove_caption: 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, + ) -> Tuple["MessageId", ...]: + """Shortcut for:: + + await bot.copy_messages(from_chat_id=update.effective_user.id, *argss, **kwargs) + + For the documentation of the arguments, please see :meth:`telegram.Bot.copy_messages`. + + .. seealso:: :meth:`copy_message`, :meth:`send_copy`, :meth:`send_copies`. + + .. versionadded:: 20.8 + + Returns: + Tuple[:class:`telegram.MessageId`]: On success, a tuple of :class:`~telegram.MessageId` + of the sent messages is returned. + + """ + return await self.get_bot().copy_messages( + from_chat_id=self.id, + chat_id=chat_id, + message_ids=message_ids, + disable_notification=disable_notification, + protect_content=protect_content, + message_thread_id=message_thread_id, + remove_caption=remove_caption, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def forward_from( + self, + from_chat_id: Union[str, int], + message_id: int, + disable_notification: ODVInput[bool] = DEFAULT_NONE, + protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_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, + ) -> "Message": + """Shortcut for:: + + await bot.forward_message(chat_id=update.effective_user.id, *argss, **kwargs) + + For the documentation of the arguments, please see :meth:`telegram.Bot.forward_message`. + + .. seealso:: :meth:`forward_to`, :meth:`forward_messages_from`, :meth:`forward_messages_to` + + .. versionadded:: 20.0 + + Returns: + :class:`telegram.Message`: On success, instance representing the message posted. + + """ + return await self.get_bot().forward_message( + chat_id=self.id, + from_chat_id=from_chat_id, + message_id=message_id, + disable_notification=disable_notification, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + protect_content=protect_content, + message_thread_id=message_thread_id, + ) + + async def forward_to( + self, + chat_id: Union[int, str], + message_id: int, + disable_notification: ODVInput[bool] = DEFAULT_NONE, + protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_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, + ) -> "Message": + """Shortcut for:: + + await bot.forward_message(from_chat_id=update.effective_user.id, *argss, **kwargs) + + For the documentation of the arguments, please see :meth:`telegram.Bot.forward_message`. + + .. seealso:: :meth:`forward_from`, :meth:`forward_messages_from`, + :meth:`forward_messages_to` + + .. versionadded:: 20.0 + + Returns: + :class:`telegram.Message`: On success, instance representing the message posted. + + """ + return await self.get_bot().forward_message( + from_chat_id=self.id, + chat_id=chat_id, + message_id=message_id, + disable_notification=disable_notification, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + protect_content=protect_content, + message_thread_id=message_thread_id, + ) + + async def forward_messages_from( + self, + from_chat_id: Union[str, int], + message_ids: Sequence[int], + disable_notification: ODVInput[bool] = DEFAULT_NONE, + protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_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, + ) -> Tuple["MessageId", ...]: + """Shortcut for:: + + await bot.forward_messages(chat_id=update.effective_user.id, *argss, **kwargs) + + For the documentation of the arguments, please see :meth:`telegram.Bot.forward_messages`. + + .. seealso:: :meth:`forward_to`, :meth:`forward_from`, :meth:`forward_messages_to`. + + .. versionadded:: 20.8 + + Returns: + Tuple[:class:`telegram.MessageId`]: On success, a tuple of :class:`~telegram.MessageId` + of sent messages is returned. + + """ + return await self.get_bot().forward_messages( + chat_id=self.id, + from_chat_id=from_chat_id, + message_ids=message_ids, + disable_notification=disable_notification, + protect_content=protect_content, + message_thread_id=message_thread_id, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def forward_messages_to( + self, + chat_id: Union[int, str], + message_ids: Sequence[int], + disable_notification: ODVInput[bool] = DEFAULT_NONE, + protect_content: ODVInput[bool] = DEFAULT_NONE, + message_thread_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, + ) -> Tuple["MessageId", ...]: + """Shortcut for:: + + await bot.forward_messages(from_chat_id=update.effective_user.id, *argss, **kwargs) + + For the documentation of the arguments, please see :meth:`telegram.Bot.forward_messages`. + + .. seealso:: :meth:`forward_from`, :meth:`forward_to`, :meth:`forward_messages_from`. + + .. versionadded:: 20.8 + + Returns: + Tuple[:class:`telegram.MessageId`]: On success, a tuple of :class:`~telegram.MessageId` + of sent messages is returned. + + """ + return await self.get_bot().forward_messages( + from_chat_id=self.id, + chat_id=chat_id, + message_ids=message_ids, + disable_notification=disable_notification, + protect_content=protect_content, + message_thread_id=message_thread_id, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def approve_join_request( + 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: + """Shortcut for:: + + await bot.approve_chat_join_request(user_id=update.effective_user.id, *args, **kwargs) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.approve_chat_join_request`. + + Note: + |user_chat_id_note| + + .. versionadded:: 13.8 + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + """ + return await self.get_bot().approve_chat_join_request( + user_id=self.id, + chat_id=chat_id, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def decline_join_request( + 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: + """Shortcut for:: + + await bot.decline_chat_join_request(user_id=update.effective_user.id, *args, **kwargs) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.decline_chat_join_request`. + + Note: + |user_chat_id_note| + + .. versionadded:: 13.8 + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + """ + return await self.get_bot().decline_chat_join_request( + user_id=self.id, + chat_id=chat_id, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def set_menu_button( + self, + menu_button: Optional[MenuButton] = 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.set_chat_menu_button(chat_id=update.effective_user.id, *argss, **kwargs) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.set_chat_menu_button`. + + .. seealso:: :meth:`get_menu_button` + + Note: + |user_chat_id_note| + + .. versionadded:: 20.0 + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + """ + return await self.get_bot().set_chat_menu_button( + chat_id=self.id, + menu_button=menu_button, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def get_menu_button( + 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, + ) -> MenuButton: + """Shortcut for:: + + await bot.get_chat_menu_button(chat_id=update.effective_user.id, *args, **kwargs) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.get_chat_menu_button`. + + .. seealso:: :meth:`set_menu_button` + + Note: + |user_chat_id_note| + + .. versionadded:: 20.0 + + Returns: + :class:`telegram.MenuButton`: On success, the current menu button is returned. + """ + return await self.get_bot().get_chat_menu_button( + chat_id=self.id, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def get_chat_boosts( + 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, + ) -> "UserChatBoosts": + """Shortcut for:: + + await bot.get_user_chat_boosts(user_id=update.effective_user.id, *args, **kwargs) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.get_user_chat_boosts`. + + .. versionadded:: 20.8 + + Returns: + :class:`telegram.UserChatBoosts`: On success, returns the boosts applied by the user. + """ + return await self.get_bot().get_user_chat_boosts( + chat_id=chat_id, + user_id=self.id, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def refund_star_payment( + self, + telegram_payment_charge_id: 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: + """Shortcut for:: + + await bot.refund_star_payment(user_id=update.effective_user.id, *args, **kwargs) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.refund_star_payment`. + + .. versionadded:: 21.3 + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + """ + return await self.get_bot().refund_star_payment( + user_id=self.id, + telegram_payment_charge_id=telegram_payment_charge_id, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) diff --git a/_userprofilephotos.py b/_userprofilephotos.py new file mode 100644 index 0000000000000000000000000000000000000000..9a5e4a066efc68190d870b4b23a44cb7a5bb0b0f --- /dev/null +++ b/_userprofilephotos.py @@ -0,0 +1,84 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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 UserProfilePhotos.""" +from typing import TYPE_CHECKING, Optional, Sequence, Tuple + +from telegram._files.photosize import PhotoSize +from telegram._telegramobject import TelegramObject +from telegram._utils.types import JSONDict + +if TYPE_CHECKING: + from telegram import Bot + + +class UserProfilePhotos(TelegramObject): + """This object represents a user's profile pictures. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`total_count` and :attr:`photos` are equal. + + Args: + total_count (:obj:`int`): Total number of profile pictures the target user has. + photos (Sequence[Sequence[:class:`telegram.PhotoSize`]]): Requested profile pictures (in up + to 4 sizes each). + + .. versionchanged:: 20.0 + |sequenceclassargs| + + Attributes: + total_count (:obj:`int`): Total number of profile pictures. + photos (Tuple[Tuple[:class:`telegram.PhotoSize`]]): Requested profile pictures (in up to 4 + sizes each). + + .. versionchanged:: 20.0 + |tupleclassattrs| + + """ + + __slots__ = ("photos", "total_count") + + def __init__( + self, + total_count: int, + photos: Sequence[Sequence[PhotoSize]], + *, + api_kwargs: Optional[JSONDict] = None, + ): + super().__init__(api_kwargs=api_kwargs) + # Required + self.total_count: int = total_count + self.photos: Tuple[Tuple[PhotoSize, ...], ...] = tuple(tuple(sizes) for sizes in photos) + + self._id_attrs = (self.total_count, self.photos) + + self._freeze() + + @classmethod + def de_json( + cls, data: Optional[JSONDict], bot: Optional["Bot"] = None + ) -> Optional["UserProfilePhotos"]: + """See :meth:`telegram.TelegramObject.de_json`.""" + data = cls._parse_data(data) + + if not data: + return None + + data["photos"] = [PhotoSize.de_list(photo, bot) for photo in data["photos"]] + + return super().de_json(data=data, bot=bot) diff --git a/_version.py b/_version.py new file mode 100644 index 0000000000000000000000000000000000000000..20043c8309b55ce3c3648b4faa54cbd62c03beda --- /dev/null +++ b/_version.py @@ -0,0 +1,56 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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/]. +# pylint: disable=missing-module-docstring +from typing import Final, NamedTuple + +__all__ = ("__version__", "__version_info__") + + +class Version(NamedTuple): + """Copies the behavior of sys.version_info. + serial is always 0 for stable releases. + """ + + major: int + minor: int + micro: int + releaselevel: str # Literal['alpha', 'beta', 'candidate', 'final'] + serial: int + + def _rl_shorthand(self) -> str: + return { + "alpha": "a", + "beta": "b", + "candidate": "rc", + }[self.releaselevel] + + def __str__(self) -> str: + version = f"{self.major}.{self.minor}" + if self.micro != 0: + version = f"{version}.{self.micro}" + if self.releaselevel != "final": + version = f"{version}{self._rl_shorthand()}{self.serial}" + + return version + + +__version_info__: Final[Version] = Version( + major=21, minor=5, micro=0, releaselevel="final", serial=0 +) +__version__: Final[str] = str(__version_info__) diff --git a/_videochat.py b/_videochat.py new file mode 100644 index 0000000000000000000000000000000000000000..b392fa6d65b56f3ea37f4c7f8c9e26d9a5a83012 --- /dev/null +++ b/_videochat.py @@ -0,0 +1,194 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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 objects related to Telegram video chats.""" +import datetime as dtm +from typing import TYPE_CHECKING, Optional, Sequence, Tuple + +from telegram._telegramobject import TelegramObject +from telegram._user import User +from telegram._utils.argumentparsing import parse_sequence_arg +from telegram._utils.datetime import extract_tzinfo_from_defaults, from_timestamp +from telegram._utils.types import JSONDict + +if TYPE_CHECKING: + from telegram import Bot + + +class VideoChatStarted(TelegramObject): + """ + This object represents a service message about a video + chat started in the chat. Currently holds no information. + + .. versionadded:: 13.4 + .. versionchanged:: 20.0 + This class was renamed from ``VoiceChatStarted`` in accordance to Bot API 6.0. + """ + + __slots__ = () + + def __init__(self, *, api_kwargs: Optional[JSONDict] = None) -> None: + super().__init__(api_kwargs=api_kwargs) + + self._freeze() + + +class VideoChatEnded(TelegramObject): + """ + This object represents a service message about a + video chat ended in the chat. + + Objects of this class are comparable in terms of equality. + Two objects of this class are considered equal, if their + :attr:`duration` are equal. + + .. versionadded:: 13.4 + .. versionchanged:: 20.0 + This class was renamed from ``VoiceChatEnded`` in accordance to Bot API 6.0. + + Args: + duration (:obj:`int`): Voice chat duration in seconds. + + Attributes: + duration (:obj:`int`): Voice chat duration in seconds. + + """ + + __slots__ = ("duration",) + + def __init__( + self, + duration: int, + *, + api_kwargs: Optional[JSONDict] = None, + ) -> None: + super().__init__(api_kwargs=api_kwargs) + self.duration: int = duration + self._id_attrs = (self.duration,) + + self._freeze() + + +class VideoChatParticipantsInvited(TelegramObject): + """ + This object represents a service message about new members invited to a video chat. + + Objects of this class are comparable in terms of equality. + Two objects of this class are considered equal, if their :attr:`users` are equal. + + .. versionadded:: 13.4 + .. versionchanged:: 20.0 + This class was renamed from ``VoiceChatParticipantsInvited`` in accordance to Bot API 6.0. + + Args: + users (Sequence[:class:`telegram.User`]): New members that were invited to the video chat. + + .. versionchanged:: 20.0 + |sequenceclassargs| + + Attributes: + users (Tuple[:class:`telegram.User`]): New members that were invited to the video chat. + + .. versionchanged:: 20.0 + |tupleclassattrs| + + """ + + __slots__ = ("users",) + + def __init__( + self, + users: Sequence[User], + *, + api_kwargs: Optional[JSONDict] = None, + ) -> None: + super().__init__(api_kwargs=api_kwargs) + self.users: Tuple[User, ...] = parse_sequence_arg(users) + self._id_attrs = (self.users,) + + self._freeze() + + @classmethod + def de_json( + cls, data: Optional[JSONDict], bot: Optional["Bot"] = None + ) -> Optional["VideoChatParticipantsInvited"]: + """See :meth:`telegram.TelegramObject.de_json`.""" + data = cls._parse_data(data) + + if not data: + return None + + data["users"] = User.de_list(data.get("users", []), bot) + return super().de_json(data=data, bot=bot) + + +class VideoChatScheduled(TelegramObject): + """This object represents a service message about a video chat scheduled in the chat. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`start_date` are equal. + + .. versionchanged:: 20.0 + This class was renamed from ``VoiceChatScheduled`` in accordance to Bot API 6.0. + + Args: + start_date (:obj:`datetime.datetime`): Point in time (Unix timestamp) when the video + chat is supposed to be started by a chat administrator + + .. versionchanged:: 20.3 + |datetime_localization| + Attributes: + start_date (:obj:`datetime.datetime`): Point in time (Unix timestamp) when the video + chat is supposed to be started by a chat administrator + + .. versionchanged:: 20.3 + |datetime_localization| + + """ + + __slots__ = ("start_date",) + + def __init__( + self, + start_date: dtm.datetime, + *, + api_kwargs: Optional[JSONDict] = None, + ) -> None: + super().__init__(api_kwargs=api_kwargs) + self.start_date: dtm.datetime = start_date + + self._id_attrs = (self.start_date,) + + self._freeze() + + @classmethod + def de_json( + cls, data: Optional[JSONDict], bot: Optional["Bot"] = None + ) -> Optional["VideoChatScheduled"]: + """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["start_date"] = from_timestamp(data["start_date"], tzinfo=loc_tzinfo) + + return super().de_json(data=data, bot=bot) diff --git a/_webappdata.py b/_webappdata.py new file mode 100644 index 0000000000000000000000000000000000000000..50b68d3460be326a93cda829885f7d168366e5fc --- /dev/null +++ b/_webappdata.py @@ -0,0 +1,64 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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 WebAppData.""" + +from typing import Optional + +from telegram._telegramobject import TelegramObject +from telegram._utils.types import JSONDict + + +class WebAppData(TelegramObject): + """Contains data sent from a `Web App `_ to the bot. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`data` and :attr:`button_text` are equal. + + Examples: + :any:`Webapp Bot ` + + .. versionadded:: 20.0 + + Args: + data (:obj:`str`): The data. Be aware that a bad client can send arbitrary data in this + field. + button_text (:obj:`str`): Text of the :paramref:`~telegram.KeyboardButton.web_app` keyboard + button, from which the Web App was opened. + + Attributes: + data (:obj:`str`): The data. Be aware that a bad client can send arbitrary data in this + field. + button_text (:obj:`str`): Text of the :paramref:`~telegram.KeyboardButton.web_app` keyboard + button, from which the Web App was opened. + + Warning: + Be aware that a bad client can send arbitrary data in this field. + """ + + __slots__ = ("button_text", "data") + + def __init__(self, data: str, button_text: str, *, api_kwargs: Optional[JSONDict] = None): + super().__init__(api_kwargs=api_kwargs) + # Required + self.data: str = data + self.button_text: str = button_text + + self._id_attrs = (self.data, self.button_text) + + self._freeze() diff --git a/_webappinfo.py b/_webappinfo.py new file mode 100644 index 0000000000000000000000000000000000000000..c5452ab0adbea4fd0210c24e007e5f8348493cc9 --- /dev/null +++ b/_webappinfo.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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 Web App Info.""" + +from typing import Optional + +from telegram._telegramobject import TelegramObject +from telegram._utils.types import JSONDict + + +class WebAppInfo(TelegramObject): + """ + This object contains information about a `Web App `_. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`url` are equal. + + Examples: + :any:`Webapp Bot ` + + .. versionadded:: 20.0 + + Args: + url (:obj:`str`): An HTTPS URL of a Web App to be opened with additional data as specified + in `Initializing Web Apps \ + `_. + + Attributes: + url (:obj:`str`): An HTTPS URL of a Web App to be opened with additional data as specified + in `Initializing Web Apps \ + `_. + """ + + __slots__ = ("url",) + + def __init__(self, url: str, *, api_kwargs: Optional[JSONDict] = None): + super().__init__(api_kwargs=api_kwargs) + # Required + self.url: str = url + + self._id_attrs = (self.url,) + + self._freeze() diff --git a/_webhookinfo.py b/_webhookinfo.py new file mode 100644 index 0000000000000000000000000000000000000000..a6f309a930d65100eb01346cac060dc34ff724e6 --- /dev/null +++ b/_webhookinfo.py @@ -0,0 +1,182 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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 WebhookInfo.""" +from datetime import datetime +from typing import TYPE_CHECKING, Optional, Sequence, Tuple + +from telegram._telegramobject import TelegramObject +from telegram._utils.argumentparsing import parse_sequence_arg +from telegram._utils.datetime import extract_tzinfo_from_defaults, from_timestamp +from telegram._utils.types import JSONDict + +if TYPE_CHECKING: + from telegram import Bot + + +class WebhookInfo(TelegramObject): + """This object represents a Telegram WebhookInfo. + + Contains information about the current status of a webhook. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`url`, :attr:`has_custom_certificate`, + :attr:`pending_update_count`, :attr:`ip_address`, :attr:`last_error_date`, + :attr:`last_error_message`, :attr:`max_connections`, :attr:`allowed_updates` and + :attr:`last_synchronization_error_date` are equal. + + .. versionchanged:: 20.0 + :attr:`last_synchronization_error_date` is considered as well when comparing objects of + this type in terms of equality. + + Args: + url (:obj:`str`): Webhook URL, may be empty if webhook is not set up. + has_custom_certificate (:obj:`bool`): :obj:`True`, if a custom certificate was provided for + webhook certificate checks. + pending_update_count (:obj:`int`): Number of updates awaiting delivery. + ip_address (:obj:`str`, optional): Currently used webhook IP address. + last_error_date (:class:`datetime.datetime`): Optional. Datetime for the most recent + error that happened when trying to deliver an update via webhook. + + .. versionchanged:: 20.3 + |datetime_localization| + last_error_message (:obj:`str`, optional): Error message in human-readable format for the + most recent error that happened when trying to deliver an update via webhook. + max_connections (:obj:`int`, optional): Maximum allowed number of simultaneous HTTPS + connections to the webhook for update delivery. + allowed_updates (Sequence[:obj:`str`], optional): A list of update types the bot is + subscribed to. Defaults to all update types, except + :attr:`telegram.Update.chat_member`. + + .. versionchanged:: 20.0 + |sequenceclassargs| + + last_synchronization_error_date (:class:`datetime.datetime`, optional): Datetime of the + most recent error that happened when trying to synchronize available updates with + Telegram datacenters. + + .. versionadded:: 20.0 + + .. versionchanged:: 20.3 + |datetime_localization| + Attributes: + url (:obj:`str`): Webhook URL, may be empty if webhook is not set up. + has_custom_certificate (:obj:`bool`): :obj:`True`, if a custom certificate was provided for + webhook certificate checks. + pending_update_count (:obj:`int`): Number of updates awaiting delivery. + ip_address (:obj:`str`): Optional. Currently used webhook IP address. + last_error_date (:class:`datetime.datetime`): Optional. Datetime for the most recent + error that happened when trying to deliver an update via webhook. + + .. versionchanged:: 20.3 + |datetime_localization| + last_error_message (:obj:`str`): Optional. Error message in human-readable format for the + most recent error that happened when trying to deliver an update via webhook. + max_connections (:obj:`int`): Optional. Maximum allowed number of simultaneous HTTPS + connections to the webhook for update delivery. + allowed_updates (Tuple[:obj:`str`]): Optional. A list of update types the bot is + subscribed to. Defaults to all update types, except + :attr:`telegram.Update.chat_member`. + + .. versionchanged:: 20.0 + + * |tupleclassattrs| + * |alwaystuple| + last_synchronization_error_date (:class:`datetime.datetime`, optional): Datetime of the + most recent error that happened when trying to synchronize available updates with + Telegram datacenters. + + .. versionadded:: 20.0 + + .. versionchanged:: 20.3 + |datetime_localization| + """ + + __slots__ = ( + "allowed_updates", + "has_custom_certificate", + "ip_address", + "last_error_date", + "last_error_message", + "last_synchronization_error_date", + "max_connections", + "pending_update_count", + "url", + ) + + def __init__( + self, + url: str, + has_custom_certificate: bool, + pending_update_count: int, + last_error_date: Optional[datetime] = None, + last_error_message: Optional[str] = None, + max_connections: Optional[int] = None, + allowed_updates: Optional[Sequence[str]] = None, + ip_address: Optional[str] = None, + last_synchronization_error_date: Optional[datetime] = None, + *, + api_kwargs: Optional[JSONDict] = None, + ): + super().__init__(api_kwargs=api_kwargs) + # Required + self.url: str = url + self.has_custom_certificate: bool = has_custom_certificate + self.pending_update_count: int = pending_update_count + + # Optional + self.ip_address: Optional[str] = ip_address + self.last_error_date: Optional[datetime] = last_error_date + self.last_error_message: Optional[str] = last_error_message + self.max_connections: Optional[int] = max_connections + self.allowed_updates: Tuple[str, ...] = parse_sequence_arg(allowed_updates) + self.last_synchronization_error_date: Optional[datetime] = last_synchronization_error_date + + self._id_attrs = ( + self.url, + self.has_custom_certificate, + self.pending_update_count, + self.ip_address, + self.last_error_date, + self.last_error_message, + self.max_connections, + self.allowed_updates, + self.last_synchronization_error_date, + ) + + self._freeze() + + @classmethod + def de_json( + cls, data: Optional[JSONDict], bot: Optional["Bot"] = None + ) -> Optional["WebhookInfo"]: + """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["last_error_date"] = from_timestamp(data.get("last_error_date"), tzinfo=loc_tzinfo) + data["last_synchronization_error_date"] = from_timestamp( + data.get("last_synchronization_error_date"), tzinfo=loc_tzinfo + ) + + return super().de_json(data=data, bot=bot) diff --git a/_writeaccessallowed.py b/_writeaccessallowed.py new file mode 100644 index 0000000000000000000000000000000000000000..0169cb6e7a0b3c9f68032e1a9ade5e094ad455f4 --- /dev/null +++ b/_writeaccessallowed.py @@ -0,0 +1,88 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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 objects related to the write access allowed service message.""" +from typing import Optional + +from telegram._telegramobject import TelegramObject +from telegram._utils.types import JSONDict + + +class WriteAccessAllowed(TelegramObject): + """ + This object represents a service message about a user allowing a bot to write messages after + adding it to the attachment menu, launching a Web App from a link, or accepting an explicit + request from a Web App sent by the method + `requestWriteAccess `_. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`web_app_name` is equal. + + .. versionadded:: 20.0 + .. versionchanged:: 20.6 + Added custom equality comparison for objects of this class. + + Args: + web_app_name (:obj:`str`, optional): Name of the Web App, if the access was granted when + the Web App was launched from a link. + + .. versionadded:: 20.3 + from_request (:obj:`bool`, optional): :obj:`True`, if the access was granted after the user + accepted an explicit request from a Web App sent by the method + `requestWriteAccess `_. + + .. versionadded:: 20.6 + from_attachment_menu (:obj:`bool`, optional): :obj:`True`, if the access was granted when + the bot was added to the attachment or side menu. + + .. versionadded:: 20.6 + + Attributes: + web_app_name (:obj:`str`): Optional. Name of the Web App, if the access was granted when + the Web App was launched from a link. + + .. versionadded:: 20.3 + from_request (:obj:`bool`): Optional. :obj:`True`, if the access was granted after the user + accepted an explicit request from a Web App. + + .. versionadded:: 20.6 + from_attachment_menu (:obj:`bool`): Optional. :obj:`True`, if the access was granted when + the bot was added to the attachment or side menu. + + .. versionadded:: 20.6 + + """ + + __slots__ = ("from_attachment_menu", "from_request", "web_app_name") + + def __init__( + self, + web_app_name: Optional[str] = None, + from_request: Optional[bool] = None, + from_attachment_menu: Optional[bool] = None, + *, + api_kwargs: Optional[JSONDict] = None, + ): + super().__init__(api_kwargs=api_kwargs) + self.web_app_name: Optional[str] = web_app_name + self.from_request: Optional[bool] = from_request + self.from_attachment_menu: Optional[bool] = from_attachment_menu + + self._id_attrs = (self.web_app_name,) + + self._freeze() diff --git a/admonition_inserter.py b/admonition_inserter.py new file mode 100644 index 0000000000000000000000000000000000000000..fa19d9a0f9db7bb35db04f7cec419271aa8acd50 --- /dev/null +++ b/admonition_inserter.py @@ -0,0 +1,232 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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 is intentionally named without "test_" prefix. +# These tests are supposed to be run on GitHub when building docs. +# The tests require Python 3.9+ (just like AdmonitionInserter being tested), +# so they cannot be included in the main suite while older versions of Python are supported. + +import collections.abc + +import pytest + +import telegram.ext +from docs.auxil.admonition_inserter import AdmonitionInserter + + +@pytest.fixture(scope="session") +def admonition_inserter(): + return AdmonitionInserter() + + +class TestAdmonitionInserter: + """This is a minimal-effort test to ensure that the `AdmonitionInserter` + used for automatically inserting references in the docs works as expected. + + It does not aim to cover all links in the documentation, but rather checks that several special + cases (which where discovered during the implementation of `AdmonitionInserter`) are handled + correctly. + """ + + def test_admonitions_dict(self, admonition_inserter): + # there are keys for every type of admonition + assert len(admonition_inserter.admonitions) == len( + admonition_inserter.ALL_ADMONITION_TYPES + ) + + # for each type of admonitions, there is at least one entry + # ({class/method: admonition text}) + for admonition_type in admonition_inserter.ALL_ADMONITION_TYPES: + assert admonition_type in admonition_inserter.admonitions + assert len(admonition_inserter.admonitions[admonition_type].keys()) > 0 + + # checking class admonitions + for admonition_type in admonition_inserter.CLASS_ADMONITION_TYPES: + # keys are telegram classes + for cls in admonition_inserter.admonitions[admonition_type]: + # Test classes crop up in AppBuilder, they can't come from code being tested. + if "tests." in str(cls): + continue + + assert isinstance(cls, type) + assert str(cls).startswith(" + ), + ( + "shortcuts", + telegram.Bot.edit_message_caption, + # this method in CallbackQuery contains two return statements, + # one of which is with Bot + ":meth:`telegram.CallbackQuery.edit_message_caption`", + ), + ( + "use_in", + telegram.InlineQueryResult, + ":meth:`telegram.Bot.answer_web_app_query`", # ForwardRef + ), + ( + "use_in", + telegram.InputMediaPhoto, + ":meth:`telegram.Bot.send_media_group`", # Sequence[Union[...]] + ), + ( + "use_in", + telegram.InlineKeyboardMarkup, + ":meth:`telegram.Bot.send_message`", # optional + ), + ( + "use_in", + telegram.Sticker, + ":meth:`telegram.Bot.get_file`", # .file_id with lots of piped types + ), + ( + "use_in", + telegram.ext.BasePersistence, + ":meth:`telegram.ext.ApplicationBuilder.persistence`", + ), + ("use_in", telegram.ext.Defaults, ":meth:`telegram.ext.ApplicationBuilder.defaults`"), + ( + "use_in", + telegram.ext.JobQueue, + ":meth:`telegram.ext.ApplicationBuilder.job_queue`", # TypeVar + ), + ( + "use_in", + telegram.ext.PicklePersistence, # subclass + ":meth:`telegram.ext.ApplicationBuilder.persistence`", + ), + ], + ) + def test_check_presence(self, admonition_inserter, admonition_type, cls, link): + """Checks if a given link is present in the admonition of a given type for a given + class. + """ + admonitions = admonition_inserter.admonitions + + assert cls in admonitions[admonition_type] + + # exactly one of the lines in the admonition for this class must consist of the link + # (this is a stricter check than just checking if the entire admonition contains the link) + lines_with_link = [ + line + for line in admonitions[admonition_type][cls].splitlines() + # remove whitespaces and occasional bullet list marker + if line.strip().removeprefix("* ") == link + ] + assert lines_with_link, ( + f"Class {cls}, does not have link {link} in a {admonition_type} admonition:\n" + f"{admonitions[admonition_type][cls]}" + ) + assert len(lines_with_link) == 1, ( + f"Class {cls}, must contain only one link {link} in a {admonition_type} admonition:\n" + f"{admonitions[admonition_type][cls]}" + ) + + @pytest.mark.parametrize( + ("admonition_type", "cls", "link"), + [ + ( + "returned_in", + telegram.ext.CallbackContext, + # -> Application[BT, CCT, UD, CD, BD, JQ]. + # In this case classes inside square brackets must not be parsed + ":meth:`telegram.ext.ApplicationBuilder.build`", + ), + ], + ) + def test_check_absence(self, admonition_inserter, admonition_type, cls, link): + """Checks if a given link is **absent** in the admonition of a given type for a given + class. + + If a given class has no admonition of this type at all, the test will also pass. + """ + admonitions = admonition_inserter.admonitions + + assert not ( + cls in admonitions[admonition_type] + and [ + line + for line in admonitions[admonition_type][cls].splitlines() + # remove whitespaces and occasional bullet list marker + if line.strip().removeprefix("* ") == link + ] + ), ( + f"Class {cls} is not supposed to have link {link} in a {admonition_type} admonition:\n" + f"{admonitions[admonition_type][cls]}" + ) diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 0000000000000000000000000000000000000000..ead17a5623a5532f8382f50b520641620bffa6ee --- /dev/null +++ b/codecov.yml @@ -0,0 +1,10 @@ +comment: false + +coverage: + status: + project: + default: + # We allow small coverage decreases in the project because we don't retry + # on hitting flood limits, which adds noise to the coverage + target: auto + threshold: 0.1% diff --git a/conftest.py b/conftest.py new file mode 100644 index 0000000000000000000000000000000000000000..e8ef01cabacad007327a50e4dbafef24f48fc229 --- /dev/null +++ b/conftest.py @@ -0,0 +1,270 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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/]. +import asyncio +import datetime +import logging +import sys +from pathlib import Path +from typing import Dict, List +from uuid import uuid4 + +import pytest + +from telegram import ( + CallbackQuery, + Chat, + ChosenInlineResult, + InlineQuery, + Message, + PreCheckoutQuery, + ShippingQuery, + Update, + User, +) +from telegram.ext import Defaults +from tests.auxil.build_messages import DATE +from tests.auxil.ci_bots import BOT_INFO_PROVIDER +from tests.auxil.constants import PRIVATE_KEY +from tests.auxil.envvars import RUN_TEST_OFFICIAL, TEST_WITH_OPT_DEPS +from tests.auxil.files import data_file +from tests.auxil.networking import NonchalantHttpxRequest +from tests.auxil.pytest_classes import PytestBot, make_bot +from tests.auxil.timezones import BasicTimezone + +if TEST_WITH_OPT_DEPS: + import pytz + + +# Don't collect `test_official.py` on Python 3.10- since it uses newer features like X | Y syntax. +# Docs: https://docs.pytest.org/en/7.1.x/example/pythoncollection.html#customizing-test-collection +collect_ignore = [] +if sys.version_info < (3, 10): + if RUN_TEST_OFFICIAL: + logging.warning("Skipping test_official.py since it requires Python 3.10+") + collect_ignore_glob = ["test_official/*.py"] + + +# This is here instead of in setup.cfg due to https://github.com/pytest-dev/pytest/issues/8343 +def pytest_runtestloop(session: pytest.Session): + session.add_marker( + pytest.mark.filterwarnings("ignore::telegram.warnings.PTBDeprecationWarning") + ) + + +def no_rerun_after_xfail_or_flood(error, name, test: pytest.Function, plugin): + """Don't rerun tests that have xfailed when marked with xfail, or when we hit a flood limit.""" + xfail_present = test.get_closest_marker(name="xfail") + if getattr(error[1], "msg", "") is None: + raise error[1] + did_we_flood = "flood" in getattr(error[1], "msg", "") # _pytest.outcomes.XFailed has 'msg' + return not (xfail_present or did_we_flood) + + +def pytest_collection_modifyitems(items: List[pytest.Item]): + """Here we add a flaky marker to all request making tests and a (no_)req marker to the rest.""" + for item in items: # items are the test methods + parent = item.parent # Get the parent of the item (class, or module if defined outside) + if parent is None: # should never happen, but just in case + return + if ( # Check if the class name ends with 'WithRequest' and if it has no flaky marker + parent.name.endswith("WithRequest") + and not parent.get_closest_marker( # get_closest_marker gets pytest.marks with `name` + name="flaky" + ) # don't add/override any previously set markers + and not parent.get_closest_marker(name="req") + ): # Add the flaky marker with a rerun filter to the class + parent.add_marker(pytest.mark.flaky(3, 1, rerun_filter=no_rerun_after_xfail_or_flood)) + parent.add_marker(pytest.mark.req) + # Add the no_req marker to all classes that end with 'WithoutRequest' and don't have it + elif parent.name.endswith("WithoutRequest") and not parent.get_closest_marker( + name="no_req" + ): + parent.add_marker(pytest.mark.no_req) + + +# Redefine the event_loop fixture to have a session scope. Otherwise `bot` fixture can't be +# session. See https://github.com/pytest-dev/pytest-asyncio/issues/68 for more details. +@pytest.fixture(scope="session") +def event_loop(request): + # ever since ProactorEventLoop became the default in Win 3.8+, the app crashes after the loop + # is closed. Hence, we use SelectorEventLoop on Windows to avoid this. See + # https://github.com/python/cpython/issues/83413, https://github.com/encode/httpx/issues/914 + if sys.platform.startswith("win"): + asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) + return asyncio.get_event_loop_policy().new_event_loop() + # loop.close() # instead of closing here, do that at the every end of the test session + + +@pytest.fixture(scope="session") +def bot_info() -> Dict[str, str]: + return BOT_INFO_PROVIDER.get_info() + + +@pytest.fixture(scope="session") +async def bot(bot_info): + """Makes an ExtBot instance with the given bot_info""" + async with make_bot(bot_info) as _bot: + yield _bot + + +@pytest.fixture(scope="session") +async def offline_bot(bot_info): + """Makes an offline Bot instance with the given bot_info + Note that in tests/ext we also override the `bot` fixture to return the offline bot instead. + """ + async with make_bot(bot_info, offline=True) as _bot: + yield _bot + + +@pytest.fixture +def one_time_bot(bot_info): + """A function scoped bot since the session bot would shutdown when `async with app` finishes""" + return make_bot(bot_info) + + +@pytest.fixture(scope="session") +async def cdc_bot(bot_info): + """Makes an ExtBot instance with the given bot_info that uses arbitrary callback_data""" + async with make_bot(bot_info, arbitrary_callback_data=True) as _bot: + yield _bot + + +@pytest.fixture(scope="session") +async def raw_bot(bot_info): + """Makes an regular Bot instance with the given bot_info""" + async with PytestBot( + bot_info["token"], + private_key=PRIVATE_KEY if TEST_WITH_OPT_DEPS else None, + request=NonchalantHttpxRequest(8), + get_updates_request=NonchalantHttpxRequest(1), + ) as _bot: + yield _bot + + +# Here we store the default bots so that we don't have to create them again and again. +# They are initialized but not shutdown on pytest_sessionfinish because it is causing +# problems with the event loop (Event loop is closed). +_default_bots = {} + + +@pytest.fixture(scope="session") +async def default_bot(request, bot_info): + param = request.param if hasattr(request, "param") else {} + defaults = Defaults(**param) + + # If the bot is already created, return it. Else make a new one. + default_bot = _default_bots.get(defaults) + if default_bot is None: + default_bot = make_bot(bot_info, defaults=defaults) + await default_bot.initialize() + _default_bots[defaults] = default_bot # Defaults object is hashable + return default_bot + + +@pytest.fixture(scope="session") +async def tz_bot(timezone, bot_info): + defaults = Defaults(tzinfo=timezone) + try: # If the bot is already created, return it. Saves time since get_me is not called again. + return _default_bots[defaults] + except KeyError: + default_bot = make_bot(bot_info, defaults=defaults) + await default_bot.initialize() + _default_bots[defaults] = default_bot + return default_bot + + +@pytest.fixture(scope="session") +def chat_id(bot_info): + return bot_info["chat_id"] + + +@pytest.fixture(scope="session") +def super_group_id(bot_info): + return bot_info["super_group_id"] + + +@pytest.fixture(scope="session") +def forum_group_id(bot_info): + return int(bot_info["forum_group_id"]) + + +@pytest.fixture(scope="session") +def channel_id(bot_info): + return bot_info["channel_id"] + + +@pytest.fixture(scope="session") +def provider_token(bot_info): + return bot_info["payment_provider_token"] + + +@pytest.fixture(scope="session") +def subscription_channel_id(bot_info): + return bot_info["subscription_channel_id"] + + +@pytest.fixture +def thumb_file(): + with data_file("thumb.jpg").open("rb") as f: + yield f + + +@pytest.fixture(scope="module") +def class_thumb_file(): + with data_file("thumb.jpg").open("rb") as f: + yield f + + +def _get_false_update_fixture_decorator_params(): + message = Message(1, DATE, Chat(1, ""), from_user=User(1, "", False), text="test") + params = [ + {"callback_query": CallbackQuery(1, User(1, "", False), "chat", message=message)}, + {"channel_post": message}, + {"edited_channel_post": message}, + {"inline_query": InlineQuery(1, User(1, "", False), "", "")}, + {"chosen_inline_result": ChosenInlineResult("id", User(1, "", False), "")}, + {"shipping_query": ShippingQuery("id", User(1, "", False), "", None)}, + {"pre_checkout_query": PreCheckoutQuery("id", User(1, "", False), "", 0, "")}, + {"callback_query": CallbackQuery(1, User(1, "", False), "chat")}, + ] + ids = tuple(key for kwargs in params for key in kwargs) + return {"params": params, "ids": ids} + + +@pytest.fixture(**_get_false_update_fixture_decorator_params()) +def false_update(request): + return Update(update_id=1, **request.param) + + +@pytest.fixture(scope="session", params=["Europe/Berlin", "Asia/Singapore", "UTC"]) +def tzinfo(request): + if TEST_WITH_OPT_DEPS: + return pytz.timezone(request.param) + hours_offset = {"Europe/Berlin": 2, "Asia/Singapore": 8, "UTC": 0}[request.param] + return BasicTimezone(offset=datetime.timedelta(hours=hours_offset), name=request.param) + + +@pytest.fixture(scope="session") +def timezone(tzinfo): + return tzinfo + + +@pytest.fixture +def tmp_file(tmp_path) -> Path: + return tmp_path / uuid4().hex diff --git a/constants.py b/constants.py new file mode 100644 index 0000000000000000000000000000000000000000..52d69aacaef36af1a53a0247d6b2024b9a9e3c6f --- /dev/null +++ b/constants.py @@ -0,0 +1,3123 @@ +# python-telegram-bot - a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# by the python-telegram-bot contributors +# +# 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 several constants that are relevant for working with the Bot API. + +Unless noted otherwise, all constants in this module were extracted from the +`Telegram Bots FAQ `_ and +`Telegram Bots API `_. + +Most of the following constants are related to specific classes or topics and are grouped into +enums. If they are related to a specific class, then they are also available as attributes of +those classes. + +.. versionchanged:: 20.0 + + * Most of the constants in this module are grouped into enums. +""" +# TODO: Remove this when https://github.com/PyCQA/pylint/issues/6887 is resolved. +# pylint: disable=invalid-enum-extension,invalid-slots + +__all__ = [ + "BOT_API_VERSION", + "BOT_API_VERSION_INFO", + "SUPPORTED_WEBHOOK_PORTS", + "ZERO_DATE", + "AccentColor", + "BackgroundFillLimit", + "BackgroundFillType", + "BackgroundTypeLimit", + "BackgroundTypeType", + "BotCommandLimit", + "BotCommandScopeType", + "BotDescriptionLimit", + "BotNameLimit", + "BulkRequestLimit", + "CallbackQueryLimit", + "ChatAction", + "ChatBoostSources", + "ChatID", + "ChatInviteLinkLimit", + "ChatLimit", + "ChatMemberStatus", + "ChatPhotoSize", + "ChatSubscriptionLimit", + "ChatType", + "ContactLimit", + "CustomEmojiStickerLimit", + "DiceEmoji", + "DiceLimit", + "FileSizeLimit", + "FloodLimit", + "ForumIconColor", + "ForumTopicLimit", + "GiveawayLimit", + "InlineKeyboardButtonLimit", + "InlineKeyboardMarkupLimit", + "InlineQueryLimit", + "InlineQueryResultLimit", + "InlineQueryResultType", + "InlineQueryResultsButtonLimit", + "InputMediaType", + "InputPaidMediaType", + "InvoiceLimit", + "KeyboardButtonRequestUsersLimit", + "LocationLimit", + "MaskPosition", + "MediaGroupLimit", + "MenuButtonType", + "MessageAttachmentType", + "MessageEntityType", + "MessageLimit", + "MessageOriginType", + "MessageType", + "PaidMediaType", + "ParseMode", + "PollLimit", + "PollType", + "PollingLimit", + "ProfileAccentColor", + "ReactionEmoji", + "ReactionType", + "ReplyLimit", + "RevenueWithdrawalStateType", + "StarTransactionsLimit", + "StickerFormat", + "StickerLimit", + "StickerSetLimit", + "StickerType", + "TransactionPartnerType", + "UpdateType", + "UserProfilePhotosLimit", + "WebhookLimit", +] + +import datetime +import sys +from enum import Enum +from typing import Final, List, NamedTuple, Optional, Tuple + +from telegram._utils.datetime import UTC +from telegram._utils.enum import IntEnum, StringEnum + + +class _BotAPIVersion(NamedTuple): + """Similar behavior to sys.version_info. + So far TG has only published X.Y releases. We can add X.Y.Z(a(S)) if needed. + """ + + major: int + minor: int + + def __repr__(self) -> str: + """Unfortunately calling super().__repr__ doesn't work with typing.NamedTuple, so we + do this manually. + """ + return f"BotAPIVersion(major={self.major}, minor={self.minor})" + + def __str__(self) -> str: + return f"{self.major}.{self.minor}" + + +class _AccentColor(NamedTuple): + """A helper class for (profile) accent colors. Since TG doesn't define a class for this and + the behavior is quite different for the different accent colors, we don't make this a public + class. This gives us more flexibility to change the implementation if necessary for future + versions. + """ + + identifier: int + name: Optional[str] = None + light_colors: Tuple[int, ...] = () + dark_colors: Tuple[int, ...] = () + + +#: :class:`typing.NamedTuple`: A tuple containing the two components of the version number: +# ``major`` and ``minor``. Both values are integers. +#: The components can also be accessed by name, so ``BOT_API_VERSION_INFO[0]`` is equivalent +#: to ``BOT_API_VERSION_INFO.major`` and so on. Also available as +#: :data:`telegram.__bot_api_version_info__`. +#: +#: .. versionadded:: 20.0 +BOT_API_VERSION_INFO: Final[_BotAPIVersion] = _BotAPIVersion(major=7, minor=9) +#: :obj:`str`: Telegram Bot API +#: version supported by this version of `python-telegram-bot`. Also available as +#: :data:`telegram.__bot_api_version__`. +#: +#: .. versionadded:: 13.4 +BOT_API_VERSION: Final[str] = str(BOT_API_VERSION_INFO) + +# constants above this line are tested + +#: List[:obj:`int`]: Ports supported by +#: :paramref:`telegram.Bot.set_webhook.url`. +SUPPORTED_WEBHOOK_PORTS: Final[List[int]] = [443, 80, 88, 8443] + +#: :obj:`datetime.datetime`, value of unix 0. +#: This date literal is used in :class:`telegram.InaccessibleMessage` +#: +#: .. versionadded:: 20.8 +ZERO_DATE: Final[datetime.datetime] = datetime.datetime(1970, 1, 1, tzinfo=UTC) + + +class AccentColor(Enum): + """This enum contains the available accent colors for + :class:`telegram.ChatFullInfo.accent_color_id`. + The members of this enum are named tuples with the following attributes: + + - ``identifier`` (:obj:`int`): The identifier of the accent color. + - ``name`` (:obj:`str`): Optional. The name of the accent color. + - ``light_colors`` (Tuple[:obj:`str`]): Optional. The light colors of the accent color as HEX + value. + - ``dark_colors`` (Tuple[:obj:`str`]): Optional. The dark colors of the accent color as HEX + value. + + Since Telegram gives no exact specification for the accent colors, future accent colors might + have a different data type. + + .. versionadded:: 20.8 + """ + + __slots__ = () + + COLOR_000 = _AccentColor(identifier=0, name="red") + """Accent color 0. This color can be customized by app themes.""" + COLOR_001 = _AccentColor(identifier=1, name="orange") + """Accent color 1. This color can be customized by app themes.""" + COLOR_002 = _AccentColor(identifier=2, name="purple/violet") + """Accent color 2. This color can be customized by app themes.""" + COLOR_003 = _AccentColor(identifier=3, name="green") + """Accent color 3. This color can be customized by app themes.""" + COLOR_004 = _AccentColor(identifier=4, name="cyan") + """Accent color 4. This color can be customized by app themes.""" + COLOR_005 = _AccentColor(identifier=5, name="blue") + """Accent color 5. This color can be customized by app themes.""" + COLOR_006 = _AccentColor(identifier=6, name="pink") + """Accent color 6. This color can be customized by app themes.""" + COLOR_007 = _AccentColor( + identifier=7, light_colors=(0xE15052, 0xF9AE63), dark_colors=(0xFF9380, 0x992F37) + ) + """Accent color 7. This contains two light colors + + .. raw:: html + +
+
+
+

+ + and two dark colors + + .. raw:: html + +
+
+
+

+ """ + COLOR_008 = _AccentColor( + identifier=8, light_colors=(0xE0802B, 0xFAC534), dark_colors=(0xECB04E, 0xC35714) + ) + """Accent color 8. This contains two light colors + + .. raw:: html + +
+
+
+

+ + and two dark colors + + .. raw:: html + +
+
+
+

+ """ + COLOR_009 = _AccentColor( + identifier=9, light_colors=(0xA05FF3, 0xF48FFF), dark_colors=(0xC697FF, 0x5E31C8) + ) + """Accent color 9. This contains two light colors + + .. raw:: html + +
+
+
+

+ + and two dark colors + + .. raw:: html + +
+
+
+

+ """ + COLOR_010 = _AccentColor( + identifier=10, light_colors=(0x27A910, 0xA7DC57), dark_colors=(0xA7EB6E, 0x167E2D) + ) + """Accent color 10. This contains two light colors + + .. raw:: html + +
+
+
+

+ + and two dark colors + + .. raw:: html + +
+
+
+

+ """ + COLOR_011 = _AccentColor( + identifier=11, light_colors=(0x27ACCE, 0x82E8D6), dark_colors=(0x40D8D0, 0x045C7F) + ) + """Accent color 11. This contains two light colors + + .. raw:: html + +
+
+
+

+ + and two dark colors + + .. raw:: html + +
+
+
+

+ """ + + COLOR_012 = _AccentColor( + identifier=12, light_colors=(0x3391D4, 0x7DD3F0), dark_colors=(0x52BFFF, 0x0B5494) + ) + """Accent color 12. This contains two light colors + + .. raw:: html + +
+
+
+

+ + and two dark colors + + .. raw:: html + +
+
+
+

+ """ + COLOR_013 = _AccentColor( + identifier=13, light_colors=(0xDD4371, 0xFFBE9F), dark_colors=(0xFF86A6, 0x8E366E) + ) + """Accent color 13. This contains two light colors + + .. raw:: html + +
+
+
+

+ + and two dark colors + + .. raw:: html + +
+
+
+

+ """ + COLOR_014 = _AccentColor( + identifier=14, + light_colors=(0x247BED, 0xF04856, 0xFFFFFF), + dark_colors=(0x3FA2FE, 0xE5424F, 0xFFFFFF), + ) + """Accent color 14. This contains three light colors + + .. raw:: html + +
+
+
+
+
+

+ + and three dark colors + + .. raw:: html + +
+
+
+
+
+

+ """ + + COLOR_015 = _AccentColor( + identifier=15, + light_colors=(0xD67722, 0x1EA011, 0xFFFFFF), + dark_colors=(0xFF905E, 0x32A527, 0xFFFFFF), + ) + """Accent color 15. This contains three light colors + + .. raw:: html + +
+
+
+
+
+

+ + and three dark colors + + .. raw:: html + +
+
+
+
+
+

+ """ + + COLOR_016 = _AccentColor( + identifier=16, + light_colors=(0x179E42, 0xE84A3F, 0xFFFFFF), + dark_colors=(0x66D364, 0xD5444F, 0xFFFFFF), + ) + """Accent color 16. This contains three light colors + + .. raw:: html + +
+
+
+
+
+

+ + and three dark colors + + .. raw:: html + +
+
+
+
+
+

+ """ + + COLOR_017 = _AccentColor( + identifier=17, + light_colors=(0x2894AF, 0x6FC456, 0xFFFFFF), + dark_colors=(0x22BCE2, 0x3DA240, 0xFFFFFF), + ) + """Accent color 17. This contains three light colors + + .. raw:: html + +
+
+
+
+
+

+ + and three dark colors + + .. raw:: html + +
+
+
+
+
+

+ """ + + COLOR_018 = _AccentColor( + identifier=18, + light_colors=(0x0C9AB3, 0xFFAD95, 0xFFE6B5), + dark_colors=(0x22BCE2, 0xFF9778, 0xFFDA6B), + ) + """Accent color 18. This contains three light colors + + .. raw:: html + +
+
+
+
+
+

+ + and three dark colors + + .. raw:: html + +
+
+
+
+
+

+ """ + + COLOR_019 = _AccentColor( + identifier=19, + light_colors=(0x7757D6, 0xF79610, 0xFFDE8E), + dark_colors=(0x9791FF, 0xF2731D, 0xFFDB59), + ) + """Accent color 19. This contains three light colors + + .. raw:: html + +
+
+
+
+
+

+ + and three dark colors + + .. raw:: html + +
+
+
+
+
+

+ """ + + COLOR_020 = _AccentColor( + identifier=20, + light_colors=(0x1585CF, 0xF2AB1D, 0xFFFFFF), + dark_colors=(0x3DA6EB, 0xEEA51D, 0xFFFFFF), + ) + """Accent color 20. This contains three light colors + + .. raw:: html + +
+
+
+
+
+

+ + and three dark colors + + .. raw:: html + +
+
+
+
+
+

+ """ + + +class BotCommandLimit(IntEnum): + """This enum contains limitations for :class:`telegram.BotCommand` and + :meth:`telegram.Bot.set_my_commands`. + The enum members of this enumeration are instances of :class:`int` and can be treated as such. + + .. versionadded:: 20.0 + """ + + __slots__ = () + + MIN_COMMAND = 1 + """:obj:`int`: Minimum value allowed for :paramref:`~telegram.BotCommand.command` parameter of + :class:`telegram.BotCommand`. + """ + MAX_COMMAND = 32 + """:obj:`int`: Maximum value allowed for :paramref:`~telegram.BotCommand.command` parameter of + :class:`telegram.BotCommand`. + """ + MIN_DESCRIPTION = 1 + """:obj:`int`: Minimum value allowed for :paramref:`~telegram.BotCommand.description` + parameter of :class:`telegram.BotCommand`. + """ + MAX_DESCRIPTION = 256 + """:obj:`int`: Maximum value allowed for :paramref:`~telegram.BotCommand.description` + parameter of :class:`telegram.BotCommand`. + """ + MAX_COMMAND_NUMBER = 100 + """:obj:`int`: Maximum number of bot commands passed in a :obj:`list` to the + :paramref:`~telegram.Bot.set_my_commands.commands` + parameter of :meth:`telegram.Bot.set_my_commands`. + """ + + +class BotCommandScopeType(StringEnum): + """This enum contains the available types of :class:`telegram.BotCommandScope`. The enum + members of this enumeration are instances of :class:`str` and can be treated as such. + + .. versionadded:: 20.0 + """ + + __slots__ = () + + DEFAULT = "default" + """:obj:`str`: The type of :class:`telegram.BotCommandScopeDefault`.""" + ALL_PRIVATE_CHATS = "all_private_chats" + """:obj:`str`: The type of :class:`telegram.BotCommandScopeAllPrivateChats`.""" + ALL_GROUP_CHATS = "all_group_chats" + """:obj:`str`: The type of :class:`telegram.BotCommandScopeAllGroupChats`.""" + ALL_CHAT_ADMINISTRATORS = "all_chat_administrators" + """:obj:`str`: The type of :class:`telegram.BotCommandScopeAllChatAdministrators`.""" + CHAT = "chat" + """:obj:`str`: The type of :class:`telegram.BotCommandScopeChat`.""" + CHAT_ADMINISTRATORS = "chat_administrators" + """:obj:`str`: The type of :class:`telegram.BotCommandScopeChatAdministrators`.""" + CHAT_MEMBER = "chat_member" + """:obj:`str`: The type of :class:`telegram.BotCommandScopeChatMember`.""" + + +class BotDescriptionLimit(IntEnum): + """This enum contains limitations for the methods :meth:`telegram.Bot.set_my_description` and + :meth:`telegram.Bot.set_my_short_description`. The enum members of this enumeration are + instances of :class:`int` and can be treated as such. + + .. versionadded:: 20.2 + """ + + __slots__ = () + + MAX_DESCRIPTION_LENGTH = 512 + """:obj:`int`: Maximum length for the parameter + :paramref:`~telegram.Bot.set_my_description.description` of + :meth:`telegram.Bot.set_my_description` + """ + MAX_SHORT_DESCRIPTION_LENGTH = 120 + """:obj:`int`: Maximum length for the parameter + :paramref:`~telegram.Bot.set_my_short_description.short_description` of + :meth:`telegram.Bot.set_my_short_description` + """ + + +class BotNameLimit(IntEnum): + """This enum contains limitations for the methods :meth:`telegram.Bot.set_my_name`. + The enum members of this enumeration are instances of :class:`int` and can be treated as such. + + .. versionadded:: 20.3 + """ + + __slots__ = () + + MAX_NAME_LENGTH = 64 + """:obj:`int`: Maximum length for the parameter :paramref:`~telegram.Bot.set_my_name.name` of + :meth:`telegram.Bot.set_my_name` + """ + + +class BulkRequestLimit(IntEnum): + """This enum contains limitations for :meth:`telegram.Bot.delete_messages`, + :meth:`telegram.Bot.forward_messages` and :meth:`telegram.Bot.copy_messages`. The enum members + of this enumeration are instances of :class:`int` and can be treated as such. + + .. versionadded:: 20.8 + """ + + __slots__ = () + + MIN_LIMIT = 1 + """:obj:`int`: Minimum number of messages required for bulk actions.""" + MAX_LIMIT = 100 + """:obj:`int`: Maximum number of messages required for bulk actions.""" + + +class CallbackQueryLimit(IntEnum): + """This enum contains limitations for :class:`telegram.CallbackQuery`/ + :meth:`telegram.Bot.answer_callback_query`. The enum members of this enumeration are instances + of :class:`int` and can be treated as such. + + .. versionadded:: 20.0 + """ + + __slots__ = () + + ANSWER_CALLBACK_QUERY_TEXT_LENGTH = 200 + """:obj:`int`: Maximum number of characters in a :obj:`str` passed as the + :paramref:`~telegram.Bot.answer_callback_query.text` parameter of + :meth:`telegram.Bot.answer_callback_query`.""" + + +class ChatAction(StringEnum): + """This enum contains the available chat actions for :meth:`telegram.Bot.send_chat_action`. + The enum members of this enumeration are instances of :class:`str` and can be treated as such. + + .. versionadded:: 20.0 + """ + + __slots__ = () + + CHOOSE_STICKER = "choose_sticker" + """:obj:`str`: Chat action indicating that the bot is selecting a sticker.""" + FIND_LOCATION = "find_location" + """:obj:`str`: Chat action indicating that the bot is selecting a location.""" + RECORD_VOICE = "record_voice" + """:obj:`str`: Chat action indicating that the bot is recording a voice message.""" + RECORD_VIDEO = "record_video" + """:obj:`str`: Chat action indicating that the bot is recording a video.""" + RECORD_VIDEO_NOTE = "record_video_note" + """:obj:`str`: Chat action indicating that the bot is recording a video note.""" + TYPING = "typing" + """:obj:`str`: A chat indicating the bot is typing.""" + UPLOAD_VOICE = "upload_voice" + """:obj:`str`: Chat action indicating that the bot is uploading a voice message.""" + UPLOAD_DOCUMENT = "upload_document" + """:obj:`str`: Chat action indicating that the bot is uploading a document.""" + UPLOAD_PHOTO = "upload_photo" + """:obj:`str`: Chat action indicating that the bot is uploading a photo.""" + UPLOAD_VIDEO = "upload_video" + """:obj:`str`: Chat action indicating that the bot is uploading a video.""" + UPLOAD_VIDEO_NOTE = "upload_video_note" + """:obj:`str`: Chat action indicating that the bot is uploading a video note.""" + + +class ChatBoostSources(StringEnum): + """This enum contains the available sources for a + :class:`Telegram chat boost `. + The enum members of this enumeration are instances of :class:`str` and can be treated as such. + + .. versionadded:: 20.8 + """ + + __slots__ = () + + GIFT_CODE = "gift_code" + """:obj:`str`: The source of the chat boost was a Telegram Premium gift code.""" + GIVEAWAY = "giveaway" + """:obj:`str`: The source of the chat boost was a Telegram Premium giveaway.""" + PREMIUM = "premium" + """:obj:`str`: The source of the chat boost was a Telegram Premium subscription/gift.""" + + +class ChatID(IntEnum): + """This enum contains some special chat IDs. The enum + members of this enumeration are instances of :class:`int` and can be treated as such. + + .. versionadded:: 20.0 + """ + + __slots__ = () + + ANONYMOUS_ADMIN = 1087968824 + """:obj:`int`: User ID in groups for messages sent by anonymous admins. Telegram chat: + `@GroupAnonymousBot `_. + + Note: + :attr:`telegram.Message.from_user` will contain this ID for backwards compatibility only. + It's recommended to use :attr:`telegram.Message.sender_chat` instead. + """ + SERVICE_CHAT = 777000 + """:obj:`int`: Telegram service chat, that also acts as sender of channel posts forwarded to + discussion groups. Telegram chat: `Telegram `_. + + Note: + :attr:`telegram.Message.from_user` will contain this ID for backwards compatibility only. + It's recommended to use :attr:`telegram.Message.sender_chat` instead. + """ + FAKE_CHANNEL = 136817688 + """:obj:`int`: User ID in groups when message is sent on behalf of a channel, or when a channel + votes on a poll. Telegram chat: `@Channel_Bot `_. + + Note: + * :attr:`telegram.Message.from_user` will contain this ID for backwards compatibility only. + It's recommended to use :attr:`telegram.Message.sender_chat` instead. + * :attr:`telegram.PollAnswer.user` will contain this ID for backwards compatibility only. + It's recommended to use :attr:`telegram.PollAnswer.voter_chat` instead. + """ + + +class ChatInviteLinkLimit(IntEnum): + """This enum contains limitations for :class:`telegram.ChatInviteLink`/ + :meth:`telegram.Bot.create_chat_invite_link`/:meth:`telegram.Bot.edit_chat_invite_link`. The + enum members of this enumeration are instances of :class:`int` and can be treated as such. + + .. versionadded:: 20.0 + """ + + __slots__ = () + + MIN_MEMBER_LIMIT = 1 + """:obj:`int`: Minimum value allowed for the + :paramref:`~telegram.Bot.create_chat_invite_link.member_limit` parameter of + :meth:`telegram.Bot.create_chat_invite_link` and + :paramref:`~telegram.Bot.edit_chat_invite_link.member_limit` of + :meth:`telegram.Bot.edit_chat_invite_link`. + """ + MAX_MEMBER_LIMIT = 99999 + """:obj:`int`: Maximum value allowed for the + :paramref:`~telegram.Bot.create_chat_invite_link.member_limit` parameter of + :meth:`telegram.Bot.create_chat_invite_link` and + :paramref:`~telegram.Bot.edit_chat_invite_link.member_limit` of + :meth:`telegram.Bot.edit_chat_invite_link`. + """ + NAME_LENGTH = 32 + """:obj:`int`: Maximum number of characters in a :obj:`str` passed as the + :paramref:`~telegram.Bot.create_chat_invite_link.name` parameter of + :meth:`telegram.Bot.create_chat_invite_link` and + :paramref:`~telegram.Bot.edit_chat_invite_link.name` of + :meth:`telegram.Bot.edit_chat_invite_link`. + """ + + +class ChatLimit(IntEnum): + """This enum contains limitations for + :meth:`telegram.Bot.set_chat_administrator_custom_title`, + :meth:`telegram.Bot.set_chat_description`, and :meth:`telegram.Bot.set_chat_title`. + The enum members of this enumeration are instances of :class:`int` and can be treated as such. + + .. versionadded:: 20.0 + """ + + __slots__ = () + + CHAT_ADMINISTRATOR_CUSTOM_TITLE_LENGTH = 16 + """:obj:`int`: Maximum length of a :obj:`str` passed as the + :paramref:`~telegram.Bot.set_chat_administrator_custom_title.custom_title` parameter of + :meth:`telegram.Bot.set_chat_administrator_custom_title`. + """ + CHAT_DESCRIPTION_LENGTH = 255 + """:obj:`int`: Maximum number of characters in a :obj:`str` passed as the + :paramref:`~telegram.Bot.set_chat_description.description` parameter of + :meth:`telegram.Bot.set_chat_description`. + """ + MIN_CHAT_TITLE_LENGTH = 1 + """:obj:`int`: Minimum length of a :obj:`str` passed as the + :paramref:`~telegram.Bot.set_chat_title.title` parameter of + :meth:`telegram.Bot.set_chat_title`. + """ + MAX_CHAT_TITLE_LENGTH = 128 + """:obj:`int`: Maximum length of a :obj:`str` passed as the + :paramref:`~telegram.Bot.set_chat_title.title` parameter of + :meth:`telegram.Bot.set_chat_title`. + """ + + +class BackgroundTypeLimit(IntEnum): + """This enum contains limitations for :class:`telegram.BackgroundTypeFill`, + :class:`telegram.BackgroundTypeWallpaper` and :class:`telegram.BackgroundTypePattern`. + The enum members of this enumeration are instances of :class:`int` and can be treated as such. + + .. versionadded:: 21.2 + """ + + __slots__ = () + + MAX_DIMMING = 100 + """:obj:`int`: Maximum value allowed for: + + * :paramref:`~telegram.BackgroundTypeFill.dark_theme_dimming` parameter of + :class:`telegram.BackgroundTypeFill` + * :paramref:`~telegram.BackgroundTypeWallpaper.dark_theme_dimming` parameter of + :class:`telegram.BackgroundTypeWallpaper` + """ + MAX_INTENSITY = 100 + """:obj:`int`: Maximum value allowed for :paramref:`~telegram.BackgroundTypePattern.intensity` + parameter of :class:`telegram.BackgroundTypePattern` + """ + + +class BackgroundFillLimit(IntEnum): + """This enum contains limitations for :class:`telegram.BackgroundFillGradient`. + The enum members of this enumeration are instances of :class:`int` and can be treated as such. + + .. versionadded:: 21.2 + """ + + __slots__ = () + + MAX_ROTATION_ANGLE = 359 + """:obj:`int`: Maximum value allowed for: + :paramref:`~telegram.BackgroundFillGradient.rotation_angle` parameter of + :class:`telegram.BackgroundFillGradient` + """ + + +class ChatMemberStatus(StringEnum): + """This enum contains the available states for :class:`telegram.ChatMember`. The enum + members of this enumeration are instances of :class:`str` and can be treated as such. + + .. versionadded:: 20.0 + """ + + __slots__ = () + + ADMINISTRATOR = "administrator" + """:obj:`str`: A :class:`telegram.ChatMember` who is administrator of the chat.""" + OWNER = "creator" + """:obj:`str`: A :class:`telegram.ChatMember` who is the owner of the chat.""" + BANNED = "kicked" + """:obj:`str`: A :class:`telegram.ChatMember` who was banned in the chat.""" + LEFT = "left" + """:obj:`str`: A :class:`telegram.ChatMember` who has left the chat.""" + MEMBER = "member" + """:obj:`str`: A :class:`telegram.ChatMember` who is a member of the chat.""" + RESTRICTED = "restricted" + """:obj:`str`: A :class:`telegram.ChatMember` who was restricted in this chat.""" + + +class ChatPhotoSize(IntEnum): + """This enum contains limitations for :class:`telegram.ChatPhoto`. + The enum members of this enumeration are instances of :class:`int` and can be treated as such. + + .. versionadded:: 20.0 + """ + + __slots__ = () + + SMALL = 160 + """:obj:`int`: Width and height of a small chat photo, ID of which is passed in + :paramref:`~telegram.ChatPhoto.small_file_id` and + :paramref:`~telegram.ChatPhoto.small_file_unique_id` parameters of + :class:`telegram.ChatPhoto`. + """ + BIG = 640 + """:obj:`int`: Width and height of a big chat photo, ID of which is passed in + :paramref:`~telegram.ChatPhoto.big_file_id` and + :paramref:`~telegram.ChatPhoto.big_file_unique_id` parameters of + :class:`telegram.ChatPhoto`. + """ + + +class ChatType(StringEnum): + """This enum contains the available types of :class:`telegram.Chat`. The enum + members of this enumeration are instances of :class:`str` and can be treated as such. + + .. versionadded:: 20.0 + """ + + __slots__ = () + + SENDER = "sender" + """:obj:`str`: A :class:`telegram.Chat` that represents the chat of a :class:`telegram.User` + sending an :class:`telegram.InlineQuery`. """ + PRIVATE = "private" + """:obj:`str`: A :class:`telegram.Chat` that is private.""" + GROUP = "group" + """:obj:`str`: A :class:`telegram.Chat` that is a group.""" + SUPERGROUP = "supergroup" + """:obj:`str`: A :class:`telegram.Chat` that is a supergroup.""" + CHANNEL = "channel" + """:obj:`str`: A :class:`telegram.Chat` that is a channel.""" + + +class ContactLimit(IntEnum): + """This enum contains limitations for :class:`telegram.InlineQueryResultContact`, + :class:`telegram.InputContactMessageContent`, and :meth:`telegram.Bot.send_contact`. + The enum members of this enumeration are instances of :class:`int` and can be treated as such. + + .. versionadded:: 20.0 + """ + + __slots__ = () + + VCARD = 2048 + """:obj:`int`: Maximum value allowed for: + + * :paramref:`~telegram.Bot.send_contact.vcard` parameter of :meth:`~telegram.Bot.send_contact` + * :paramref:`~telegram.InlineQueryResultContact.vcard` parameter of + :class:`~telegram.InlineQueryResultContact` + * :paramref:`~telegram.InputContactMessageContent.vcard` parameter of + :class:`~telegram.InputContactMessageContent` + """ + + +class CustomEmojiStickerLimit(IntEnum): + """This enum contains limitations for :meth:`telegram.Bot.get_custom_emoji_stickers`. + The enum members of this enumeration are instances of :class:`int` and can be treated as such. + + .. versionadded:: 20.0 + """ + + __slots__ = () + + CUSTOM_EMOJI_IDENTIFIER_LIMIT = 200 + """:obj:`int`: Maximum amount of custom emoji identifiers which can be specified for the + :paramref:`~telegram.Bot.get_custom_emoji_stickers.custom_emoji_ids` parameter of + :meth:`telegram.Bot.get_custom_emoji_stickers`. + """ + + +class DiceEmoji(StringEnum): + """This enum contains the available emoji for :class:`telegram.Dice`/ + :meth:`telegram.Bot.send_dice`. The enum + members of this enumeration are instances of :class:`str` and can be treated as such. + + .. versionadded:: 20.0 + """ + + __slots__ = () + + DICE = "🎲" + """:obj:`str`: A :class:`telegram.Dice` with the emoji ``🎲``.""" + DARTS = "🎯" + """:obj:`str`: A :class:`telegram.Dice` with the emoji ``🎯``.""" + BASKETBALL = "🏀" + """:obj:`str`: A :class:`telegram.Dice` with the emoji ``🏀``.""" + FOOTBALL = "⚽" + """:obj:`str`: A :class:`telegram.Dice` with the emoji ``⚽``.""" + SLOT_MACHINE = "🎰" + """:obj:`str`: A :class:`telegram.Dice` with the emoji ``🎰``.""" + BOWLING = "🎳" + """:obj:`str`: A :class:`telegram.Dice` with the emoji ``🎳``.""" + + +class DiceLimit(IntEnum): + """This enum contains limitations for :class:`telegram.Dice`. + The enum members of this enumeration are instances of :class:`int` and can be treated as such. + + .. versionadded:: 20.0 + """ + + __slots__ = () + + MIN_VALUE = 1 + """:obj:`int`: Minimum value allowed for :paramref:`~telegram.Dice.value` parameter of + :class:`telegram.Dice` (any emoji). + """ + + MAX_VALUE_BASKETBALL = 5 + """:obj:`int`: Maximum value allowed for :paramref:`~telegram.Dice.value` parameter of + :class:`telegram.Dice` if :paramref:`~telegram.Dice.emoji` is + :tg-const:`telegram.constants.DiceEmoji.BASKETBALL`. + """ + MAX_VALUE_BOWLING = 6 + """:obj:`int`: Maximum value allowed for :paramref:`~telegram.Dice.value` parameter of + :class:`telegram.Dice` if :paramref:`~telegram.Dice.emoji` is + :tg-const:`telegram.constants.DiceEmoji.BOWLING`. + """ + MAX_VALUE_DARTS = 6 + """:obj:`int`: Maximum value allowed for :paramref:`~telegram.Dice.value` parameter of + :class:`telegram.Dice` if :paramref:`~telegram.Dice.emoji` is + :tg-const:`telegram.constants.DiceEmoji.DARTS`. + """ + MAX_VALUE_DICE = 6 + """:obj:`int`: Maximum value allowed for :paramref:`~telegram.Dice.value` parameter of + :class:`telegram.Dice` if :paramref:`~telegram.Dice.emoji` is + :tg-const:`telegram.constants.DiceEmoji.DICE`. + """ + MAX_VALUE_FOOTBALL = 5 + """:obj:`int`: Maximum value allowed for :paramref:`~telegram.Dice.value` parameter of + :class:`telegram.Dice` if :paramref:`~telegram.Dice.emoji` is + :tg-const:`telegram.constants.DiceEmoji.FOOTBALL`. + """ + MAX_VALUE_SLOT_MACHINE = 64 + """:obj:`int`: Maximum value allowed for :paramref:`~telegram.Dice.value` parameter of + :class:`telegram.Dice` if :paramref:`~telegram.Dice.emoji` is + :tg-const:`telegram.constants.DiceEmoji.SLOT_MACHINE`. + """ + + +class FileSizeLimit(IntEnum): + """This enum contains limitations regarding the upload and download of files. The enum + members of this enumeration are instances of :class:`int` and can be treated as such. + + .. versionadded:: 20.0 + """ + + __slots__ = () + + FILESIZE_DOWNLOAD = int(20e6) # (20MB) + """:obj:`int`: Bots can download files of up to 20MB in size.""" + FILESIZE_UPLOAD = int(50e6) # (50MB) + """:obj:`int`: Bots can upload non-photo files of up to 50MB in size.""" + FILESIZE_UPLOAD_LOCAL_MODE = int(2e9) # (2000MB) + """:obj:`int`: Bots can upload non-photo files of up to 2000MB in size when using a local bot + API server. + """ + FILESIZE_DOWNLOAD_LOCAL_MODE = sys.maxsize + """:obj:`int`: Bots can download files without a size limit when using a local bot API server. + """ + PHOTOSIZE_UPLOAD = int(10e6) # (10MB) + """:obj:`int`: Bots can upload photo files of up to 10MB in size.""" + VOICE_NOTE_FILE_SIZE = int(1e6) # (1MB) + """:obj:`int`: File size limit for the :meth:`~telegram.Bot.send_voice` method of + :class:`telegram.Bot`. Bots can send :mimetype:`audio/ogg` files of up to 1MB in size as + a voice note. Larger voice notes (up to 20MB) will be sent as files.""" + # It seems OK to link 20MB limit to FILESIZE_DOWNLOAD rather than creating a new constant + + +class FloodLimit(IntEnum): + """This enum contains limitations regarding flood limits. The enum + members of this enumeration are instances of :class:`int` and can be treated as such. + + .. versionadded:: 20.0 + """ + + __slots__ = () + + MESSAGES_PER_SECOND_PER_CHAT = 1 + """:obj:`int`: The number of messages that can be sent per second in a particular chat. + Telegram may allow short bursts that go over this limit, but eventually you'll begin + receiving 429 errors. + """ + MESSAGES_PER_SECOND = 30 + """:obj:`int`: The number of messages that can roughly be sent in an interval of 30 seconds + across all chats. + """ + MESSAGES_PER_MINUTE_PER_GROUP = 20 + """:obj:`int`: The number of messages that can roughly be sent to a particular group within one + minute. + """ + + +class ForumIconColor(IntEnum): + """This enum contains the available colors for use in + :paramref:`telegram.Bot.create_forum_topic.icon_color`. The enum members of this enumeration + are instances of :class:`int` and can be treated as such. + + .. versionadded:: 20.0 + """ + + __slots__ = () + + BLUE = 0x6FB9F0 + """:obj:`int`: An icon with a color which corresponds to blue (``0x6FB9F0``). + + .. raw:: html + +
+ + """ + YELLOW = 0xFFD67E + """:obj:`int`: An icon with a color which corresponds to yellow (``0xFFD67E``). + + .. raw:: html + +
+ + """ + PURPLE = 0xCB86DB + """:obj:`int`: An icon with a color which corresponds to purple (``0xCB86DB``). + + .. raw:: html + +
+ + """ + GREEN = 0x8EEE98 + """:obj:`int`: An icon with a color which corresponds to green (``0x8EEE98``). + + .. raw:: html + +
+ + """ + PINK = 0xFF93B2 + """:obj:`int`: An icon with a color which corresponds to pink (``0xFF93B2``). + + .. raw:: html + +
+ + """ + RED = 0xFB6F5F + """:obj:`int`: An icon with a color which corresponds to red (``0xFB6F5F``). + + .. raw:: html + +
+ + """ + + +class GiveawayLimit(IntEnum): + """This enum contains limitations for :class:`telegram.Giveaway` and related classes. + The enum members of this enumeration are instances of :class:`int` and can be treated as such. + + .. versionadded:: 20.8 + """ + + __slots__ = () + + MAX_WINNERS = 100 + """:obj:`int`: Maximum number of winners allowed for :class:`telegram.GiveawayWinners.winners`. + """ + + +class KeyboardButtonRequestUsersLimit(IntEnum): + """This enum contains limitations for :class:`telegram.KeyboardButtonRequestUsers`. + The enum members of this enumeration are instances of :class:`int` and can be treated as such. + + .. versionadded:: 20.8 + """ + + __slots__ = () + + MIN_QUANTITY = 1 + """:obj:`int`: Minimum value allowed for + :paramref:`~telegram.KeyboardButtonRequestUsers.max_quantity` parameter of + :class:`telegram.KeyboardButtonRequestUsers`. + """ + MAX_QUANTITY = 10 + """:obj:`int`: Maximum value allowed for + :paramref:`~telegram.KeyboardButtonRequestUsers.max_quantity` parameter of + :class:`telegram.KeyboardButtonRequestUsers`. + """ + + +class InlineKeyboardButtonLimit(IntEnum): + """This enum contains limitations for :class:`telegram.InlineKeyboardButton`. + The enum members of this enumeration are instances of :class:`int` and can be treated as such. + + .. versionadded:: 20.0 + """ + + __slots__ = () + + MIN_CALLBACK_DATA = 1 + """:obj:`int`: Minimum value allowed for + :paramref:`~telegram.InlineKeyboardButton.callback_data` parameter of + :class:`telegram.InlineKeyboardButton` + """ + MAX_CALLBACK_DATA = 64 + """:obj:`int`: Maximum value allowed for + :paramref:`~telegram.InlineKeyboardButton.callback_data` parameter of + :class:`telegram.InlineKeyboardButton` + """ + + +class InlineKeyboardMarkupLimit(IntEnum): + """This enum contains limitations for :class:`telegram.InlineKeyboardMarkup`/ + :meth:`telegram.Bot.send_message` & friends. The enum + members of this enumeration are instances of :class:`int` and can be treated as such. + + .. versionadded:: 20.0 + """ + + __slots__ = () + + TOTAL_BUTTON_NUMBER = 100 + """:obj:`int`: Maximum number of buttons that can be attached to a message. + + Note: + This value is undocumented and might be changed by Telegram. + """ + BUTTONS_PER_ROW = 8 + """:obj:`int`: Maximum number of buttons that can be attached to a message per row. + + Note: + This value is undocumented and might be changed by Telegram. + """ + + +class InputMediaType(StringEnum): + """This enum contains the available types of :class:`telegram.InputMedia`. The enum + members of this enumeration are instances of :class:`str` and can be treated as such. + + .. versionadded:: 20.0 + """ + + __slots__ = () + + ANIMATION = "animation" + """:obj:`str`: Type of :class:`telegram.InputMediaAnimation`.""" + DOCUMENT = "document" + """:obj:`str`: Type of :class:`telegram.InputMediaDocument`.""" + AUDIO = "audio" + """:obj:`str`: Type of :class:`telegram.InputMediaAudio`.""" + PHOTO = "photo" + """:obj:`str`: Type of :class:`telegram.InputMediaPhoto`.""" + VIDEO = "video" + """:obj:`str`: Type of :class:`telegram.InputMediaVideo`.""" + + +class InputPaidMediaType(StringEnum): + """This enum contains the available types of :class:`telegram.InputPaidMedia`. The enum + members of this enumeration are instances of :class:`str` and can be treated as such. + + .. versionadded:: 21.4 + """ + + __slots__ = () + + PHOTO = "photo" + """:obj:`str`: Type of :class:`telegram.InputMediaPhoto`.""" + VIDEO = "video" + """:obj:`str`: Type of :class:`telegram.InputMediaVideo`.""" + + +class InlineQueryLimit(IntEnum): + """This enum contains limitations for :class:`telegram.InlineQuery`/ + :meth:`telegram.Bot.answer_inline_query`. The enum members of this enumeration are instances + of :class:`int` and can be treated as such. + + .. versionadded:: 20.0 + """ + + __slots__ = () + + RESULTS = 50 + """:obj:`int`: Maximum number of results that can be passed to + :meth:`telegram.Bot.answer_inline_query`.""" + MAX_OFFSET_LENGTH = 64 + """:obj:`int`: Maximum number of bytes in a :obj:`str` passed as the + :paramref:`~telegram.Bot.answer_inline_query.next_offset` parameter of + :meth:`telegram.Bot.answer_inline_query`.""" + MAX_QUERY_LENGTH = 256 + """:obj:`int`: Maximum number of characters in a :obj:`str` passed as the + :paramref:`~telegram.InlineQuery.query` parameter of :class:`telegram.InlineQuery`.""" + MIN_SWITCH_PM_TEXT_LENGTH = 1 + """:obj:`int`: Minimum number of characters in a :obj:`str` passed as the + :paramref:`~telegram.Bot.answer_inline_query.switch_pm_parameter` parameter of + :meth:`telegram.Bot.answer_inline_query`. + + .. deprecated:: 20.3 + Deprecated in favor of :attr:`InlineQueryResultsButtonLimit.MIN_START_PARAMETER_LENGTH`. + """ + MAX_SWITCH_PM_TEXT_LENGTH = 64 + """:obj:`int`: Maximum number of characters in a :obj:`str` passed as the + :paramref:`~telegram.Bot.answer_inline_query.switch_pm_parameter` parameter of + :meth:`telegram.Bot.answer_inline_query`. + + .. deprecated:: 20.3 + Deprecated in favor of :attr:`InlineQueryResultsButtonLimit.MAX_START_PARAMETER_LENGTH`. + """ + + +class InlineQueryResultLimit(IntEnum): + """This enum contains limitations for :class:`telegram.InlineQueryResult` and its subclasses. + The enum members of this enumeration are instances of :class:`int` and can be treated as such. + + .. versionadded:: 20.0 + """ + + __slots__ = () + + MIN_ID_LENGTH = 1 + """:obj:`int`: Minimum number of bytes in a :obj:`str` passed as the + :paramref:`~telegram.InlineQueryResult.id` parameter of + :class:`telegram.InlineQueryResult` and its subclasses + """ + MAX_ID_LENGTH = 64 + """:obj:`int`: Maximum number of bytes in a :obj:`str` passed as the + :paramref:`~telegram.InlineQueryResult.id` parameter of + :class:`telegram.InlineQueryResult` and its subclasses + """ + + +class InlineQueryResultsButtonLimit(IntEnum): + """This enum contains limitations for :class:`telegram.InlineQueryResultsButton`. + The enum members of this enumeration are instances of :class:`int` and can be treated as such. + + .. versionadded:: 20.3 + """ + + __slots__ = () + + MIN_START_PARAMETER_LENGTH = InlineQueryLimit.MIN_SWITCH_PM_TEXT_LENGTH + """:obj:`int`: Minimum number of characters in a :obj:`str` passed as the + :paramref:`~telegram.InlineQueryResultsButton.start_parameter` parameter of + :meth:`telegram.InlineQueryResultsButton`.""" + + MAX_START_PARAMETER_LENGTH = InlineQueryLimit.MAX_SWITCH_PM_TEXT_LENGTH + """:obj:`int`: Maximum number of characters in a :obj:`str` passed as the + :paramref:`~telegram.InlineQueryResultsButton.start_parameter` parameter of + :meth:`telegram.InlineQueryResultsButton`.""" + + +class InlineQueryResultType(StringEnum): + """This enum contains the available types of :class:`telegram.InlineQueryResult`. The enum + members of this enumeration are instances of :class:`str` and can be treated as such. + + .. versionadded:: 20.0 + """ + + __slots__ = () + + AUDIO = "audio" + """:obj:`str`: Type of :class:`telegram.InlineQueryResultAudio` and + :class:`telegram.InlineQueryResultCachedAudio`. + """ + DOCUMENT = "document" + """:obj:`str`: Type of :class:`telegram.InlineQueryResultDocument` and + :class:`telegram.InlineQueryResultCachedDocument`. + """ + GIF = "gif" + """:obj:`str`: Type of :class:`telegram.InlineQueryResultGif` and + :class:`telegram.InlineQueryResultCachedGif`. + """ + MPEG4GIF = "mpeg4_gif" + """:obj:`str`: Type of :class:`telegram.InlineQueryResultMpeg4Gif` and + :class:`telegram.InlineQueryResultCachedMpeg4Gif`. + """ + PHOTO = "photo" + """:obj:`str`: Type of :class:`telegram.InlineQueryResultPhoto` and + :class:`telegram.InlineQueryResultCachedPhoto`. + """ + STICKER = "sticker" + """:obj:`str`: Type of and :class:`telegram.InlineQueryResultCachedSticker`.""" + VIDEO = "video" + """:obj:`str`: Type of :class:`telegram.InlineQueryResultVideo` and + :class:`telegram.InlineQueryResultCachedVideo`. + """ + VOICE = "voice" + """:obj:`str`: Type of :class:`telegram.InlineQueryResultVoice` and + :class:`telegram.InlineQueryResultCachedVoice`. + """ + ARTICLE = "article" + """:obj:`str`: Type of :class:`telegram.InlineQueryResultArticle`.""" + CONTACT = "contact" + """:obj:`str`: Type of :class:`telegram.InlineQueryResultContact`.""" + GAME = "game" + """:obj:`str`: Type of :class:`telegram.InlineQueryResultGame`.""" + LOCATION = "location" + """:obj:`str`: Type of :class:`telegram.InlineQueryResultLocation`.""" + VENUE = "venue" + """:obj:`str`: Type of :class:`telegram.InlineQueryResultVenue`.""" + + +class LocationLimit(IntEnum): + """This enum contains limitations for + :class:`telegram.Location`/:class:`telegram.ChatLocation`/ + :meth:`telegram.Bot.edit_message_live_location`/:meth:`telegram.Bot.send_location`. + The enum members of this enumeration are instances of :class:`int` and can be treated as such. + + .. versionadded:: 20.0 + """ + + __slots__ = () + + MIN_CHAT_LOCATION_ADDRESS = 1 + """:obj:`int`: Minimum value allowed for :paramref:`~telegram.ChatLocation.address` parameter + of :class:`telegram.ChatLocation` + """ + MAX_CHAT_LOCATION_ADDRESS = 64 + """:obj:`int`: Minimum value allowed for :paramref:`~telegram.ChatLocation.address` parameter + of :class:`telegram.ChatLocation` + """ + + HORIZONTAL_ACCURACY = 1500 + """:obj:`int`: Maximum value allowed for: + + * :paramref:`~telegram.Location.horizontal_accuracy` parameter of :class:`telegram.Location` + * :paramref:`~telegram.InlineQueryResultLocation.horizontal_accuracy` parameter of + :class:`telegram.InlineQueryResultLocation` + * :paramref:`~telegram.InputLocationMessageContent.horizontal_accuracy` parameter of + :class:`telegram.InputLocationMessageContent` + * :paramref:`~telegram.Bot.edit_message_live_location.horizontal_accuracy` parameter of + :meth:`telegram.Bot.edit_message_live_location` + * :paramref:`~telegram.Bot.send_location.horizontal_accuracy` parameter of + :meth:`telegram.Bot.send_location` + """ + + MIN_HEADING = 1 + """:obj:`int`: Minimum value allowed for: + + * :paramref:`~telegram.Location.heading` parameter of :class:`telegram.Location` + * :paramref:`~telegram.InlineQueryResultLocation.heading` parameter of + :class:`telegram.InlineQueryResultLocation` + * :paramref:`~telegram.InputLocationMessageContent.heading` parameter of + :class:`telegram.InputLocationMessageContent` + * :paramref:`~telegram.Bot.edit_message_live_location.heading` parameter of + :meth:`telegram.Bot.edit_message_live_location` + * :paramref:`~telegram.Bot.send_location.heading` parameter of + :meth:`telegram.Bot.send_location` + """ + MAX_HEADING = 360 + """:obj:`int`: Maximum value allowed for: + + * :paramref:`~telegram.Location.heading` parameter of :class:`telegram.Location` + * :paramref:`~telegram.InlineQueryResultLocation.heading` parameter of + :class:`telegram.InlineQueryResultLocation` + * :paramref:`~telegram.InputLocationMessageContent.heading` parameter of + :class:`telegram.InputLocationMessageContent` + * :paramref:`~telegram.Bot.edit_message_live_location.heading` parameter of + :meth:`telegram.Bot.edit_message_live_location` + * :paramref:`~telegram.Bot.send_location.heading` parameter of + :meth:`telegram.Bot.send_location` + """ + + MIN_LIVE_PERIOD = 60 + """:obj:`int`: Minimum value allowed for: + + * :paramref:`~telegram.InlineQueryResultLocation.live_period` parameter of + :class:`telegram.InlineQueryResultLocation` + * :paramref:`~telegram.InputLocationMessageContent.live_period` parameter of + :class:`telegram.InputLocationMessageContent` + * :paramref:`~telegram.Bot.edit_message_live_location.live_period` parameter of + :meth:`telegram.Bot.edit_message_live_location` + * :paramref:`~telegram.Bot.send_location.live_period` parameter of + :meth:`telegram.Bot.send_location` + """ + MAX_LIVE_PERIOD = 86400 + """:obj:`int`: Maximum value allowed for: + + * :paramref:`~telegram.InlineQueryResultLocation.live_period` parameter of + :class:`telegram.InlineQueryResultLocation` + * :paramref:`~telegram.InputLocationMessageContent.live_period` parameter of + :class:`telegram.InputLocationMessageContent` + * :paramref:`~telegram.Bot.edit_message_live_location.live_period` parameter of + :meth:`telegram.Bot.edit_message_live_location` + * :paramref:`~telegram.Bot.send_location.live_period` parameter of + :meth:`telegram.Bot.send_location` + """ + + LIVE_PERIOD_FOREVER = int(hex(0x7FFFFFFF), 16) + """:obj:`int`: Value for live locations that can be edited indefinitely. Passed in: + + * :paramref:`~telegram.InlineQueryResultLocation.live_period` parameter of + :class:`telegram.InlineQueryResultLocation` + * :paramref:`~telegram.InputLocationMessageContent.live_period` parameter of + :class:`telegram.InputLocationMessageContent` + * :paramref:`~telegram.Bot.edit_message_live_location.live_period` parameter of + :meth:`telegram.Bot.edit_message_live_location` + * :paramref:`~telegram.Bot.send_location.live_period` parameter of + :meth:`telegram.Bot.send_location` + + .. versionadded:: 21.2 + """ + + MIN_PROXIMITY_ALERT_RADIUS = 1 + """:obj:`int`: Minimum value allowed for: + + * :paramref:`~telegram.InlineQueryResultLocation.proximity_alert_radius` parameter of + :class:`telegram.InlineQueryResultLocation` + * :paramref:`~telegram.InputLocationMessageContent.proximity_alert_radius` parameter of + :class:`telegram.InputLocationMessageContent` + * :paramref:`~telegram.Bot.edit_message_live_location.proximity_alert_radius` parameter of + :meth:`telegram.Bot.edit_message_live_location` + * :paramref:`~telegram.Bot.send_location.proximity_alert_radius` parameter of + :meth:`telegram.Bot.send_location` + """ + MAX_PROXIMITY_ALERT_RADIUS = 100000 + """:obj:`int`: Maximum value allowed for: + + * :paramref:`~telegram.InlineQueryResultLocation.proximity_alert_radius` parameter of + :class:`telegram.InlineQueryResultLocation` + * :paramref:`~telegram.InputLocationMessageContent.proximity_alert_radius` parameter of + :class:`telegram.InputLocationMessageContent` + * :paramref:`~telegram.Bot.edit_message_live_location.proximity_alert_radius` parameter of + :meth:`telegram.Bot.edit_message_live_location` + * :paramref:`~telegram.Bot.send_location.proximity_alert_radius` parameter of + :meth:`telegram.Bot.send_location` + """ + + +class MaskPosition(StringEnum): + """This enum contains the available positions for :class:`telegram.MaskPosition`. The enum + members of this enumeration are instances of :class:`str` and can be treated as such. + + .. versionadded:: 20.0 + """ + + __slots__ = () + + FOREHEAD = "forehead" + """:obj:`str`: Mask position for a sticker on the forehead.""" + EYES = "eyes" + """:obj:`str`: Mask position for a sticker on the eyes.""" + MOUTH = "mouth" + """:obj:`str`: Mask position for a sticker on the mouth.""" + CHIN = "chin" + """:obj:`str`: Mask position for a sticker on the chin.""" + + +class MediaGroupLimit(IntEnum): + """This enum contains limitations for :meth:`telegram.Bot.send_media_group`. + The enum members of this enumeration are instances of :class:`int` and can be treated as such. + + .. versionadded:: 20.0 + """ + + __slots__ = () + + MIN_MEDIA_LENGTH = 2 + """:obj:`int`: Minimum length of a :obj:`list` passed as the + :paramref:`~telegram.Bot.send_media_group.media` parameter of + :meth:`telegram.Bot.send_media_group`. + """ + MAX_MEDIA_LENGTH = 10 + """:obj:`int`: Maximum length of a :obj:`list` passed as the + :paramref:`~telegram.Bot.send_media_group.media` parameter of + :meth:`telegram.Bot.send_media_group`. + """ + + +class MenuButtonType(StringEnum): + """This enum contains the available types of :class:`telegram.MenuButton`. The enum + members of this enumeration are instances of :class:`str` and can be treated as such. + + .. versionadded:: 20.0 + """ + + __slots__ = () + + COMMANDS = "commands" + """:obj:`str`: The type of :class:`telegram.MenuButtonCommands`.""" + WEB_APP = "web_app" + """:obj:`str`: The type of :class:`telegram.MenuButtonWebApp`.""" + DEFAULT = "default" + """:obj:`str`: The type of :class:`telegram.MenuButtonDefault`.""" + + +class MessageAttachmentType(StringEnum): + """This enum contains the available types of :class:`telegram.Message` that can be seen + as attachment. The enum + members of this enumeration are instances of :class:`str` and can be treated as such. + + .. versionadded:: 20.0 + """ + + __slots__ = () + + # Make sure that all constants here are also listed in the MessageType Enum! + # (Enums are not extendable) + + ANIMATION = "animation" + """:obj:`str`: Messages with :attr:`telegram.Message.animation`.""" + AUDIO = "audio" + """:obj:`str`: Messages with :attr:`telegram.Message.audio`.""" + CONTACT = "contact" + """:obj:`str`: Messages with :attr:`telegram.Message.contact`.""" + DICE = "dice" + """:obj:`str`: Messages with :attr:`telegram.Message.dice`.""" + DOCUMENT = "document" + """:obj:`str`: Messages with :attr:`telegram.Message.document`.""" + GAME = "game" + """:obj:`str`: Messages with :attr:`telegram.Message.game`.""" + INVOICE = "invoice" + """:obj:`str`: Messages with :attr:`telegram.Message.invoice`.""" + LOCATION = "location" + """:obj:`str`: Messages with :attr:`telegram.Message.location`.""" + PAID_MEDIA = "paid_media" + """:obj:`str`: Messages with :attr:`telegram.Message.paid_media`. + + .. versionadded:: 21.4 + """ + PASSPORT_DATA = "passport_data" + """:obj:`str`: Messages with :attr:`telegram.Message.passport_data`.""" + PHOTO = "photo" + """:obj:`str`: Messages with :attr:`telegram.Message.photo`.""" + POLL = "poll" + """:obj:`str`: Messages with :attr:`telegram.Message.poll`.""" + STICKER = "sticker" + """:obj:`str`: Messages with :attr:`telegram.Message.sticker`.""" + STORY = "story" + """:obj:`str`: Messages with :attr:`telegram.Message.story`.""" + SUCCESSFUL_PAYMENT = "successful_payment" + """:obj:`str`: Messages with :attr:`telegram.Message.successful_payment`.""" + VIDEO = "video" + """:obj:`str`: Messages with :attr:`telegram.Message.video`.""" + VIDEO_NOTE = "video_note" + """:obj:`str`: Messages with :attr:`telegram.Message.video_note`.""" + VOICE = "voice" + """:obj:`str`: Messages with :attr:`telegram.Message.voice`.""" + VENUE = "venue" + """:obj:`str`: Messages with :attr:`telegram.Message.venue`.""" + + +class MessageEntityType(StringEnum): + """This enum contains the available types of :class:`telegram.MessageEntity`. The enum + members of this enumeration are instances of :class:`str` and can be treated as such. + + .. versionadded:: 20.0 + """ + + __slots__ = () + + BLOCKQUOTE = "blockquote" + """:obj:`str`: Message entities representing a block quotation. + + .. versionadded:: 20.8 + """ + BOLD = "bold" + """:obj:`str`: Message entities representing bold text.""" + BOT_COMMAND = "bot_command" + """:obj:`str`: Message entities representing a bot command.""" + CASHTAG = "cashtag" + """:obj:`str`: Message entities representing a cashtag.""" + CODE = "code" + """:obj:`str`: Message entities representing monowidth string.""" + CUSTOM_EMOJI = "custom_emoji" + """:obj:`str`: Message entities representing inline custom emoji stickers. + + .. versionadded:: 20.0 + """ + EMAIL = "email" + """:obj:`str`: Message entities representing a email.""" + EXPANDABLE_BLOCKQUOTE = "expandable_blockquote" + """:obj:`str`: Message entities representing collapsed-by-default block quotation. + + .. versionadded:: 21.3 + """ + HASHTAG = "hashtag" + """:obj:`str`: Message entities representing a hashtag.""" + ITALIC = "italic" + """:obj:`str`: Message entities representing italic text.""" + MENTION = "mention" + """:obj:`str`: Message entities representing a mention.""" + PHONE_NUMBER = "phone_number" + """:obj:`str`: Message entities representing a phone number.""" + PRE = "pre" + """:obj:`str`: Message entities representing monowidth block.""" + SPOILER = "spoiler" + """:obj:`str`: Message entities representing spoiler text.""" + STRIKETHROUGH = "strikethrough" + """:obj:`str`: Message entities representing strikethrough text.""" + TEXT_LINK = "text_link" + """:obj:`str`: Message entities representing clickable text URLs.""" + TEXT_MENTION = "text_mention" + """:obj:`str`: Message entities representing text mention for users without usernames.""" + UNDERLINE = "underline" + """:obj:`str`: Message entities representing underline text.""" + URL = "url" + """:obj:`str`: Message entities representing a url.""" + + +class MessageLimit(IntEnum): + """This enum contains limitations for :class:`telegram.Message`/ + :class:`telegram.InputTextMessageContent`/ + :meth:`telegram.Bot.send_message` & friends. The enum + members of this enumeration are instances of :class:`int` and can be treated as such. + + .. versionadded:: 20.0 + """ + + __slots__ = () + + # TODO add links to params? + MAX_TEXT_LENGTH = 4096 + """:obj:`int`: Maximum number of characters in a :obj:`str` passed as: + + * :paramref:`~telegram.Game.text` parameter of :class:`telegram.Game` + * :paramref:`~telegram.Message.text` parameter of :class:`telegram.Message` + * :paramref:`~telegram.InputTextMessageContent.message_text` parameter of + :class:`telegram.InputTextMessageContent` + * :paramref:`~telegram.Bot.send_message.text` parameter of :meth:`telegram.Bot.send_message` + * :paramref:`~telegram.Bot.edit_message_text.text` parameter of + :meth:`telegram.Bot.edit_message_text` + """ + CAPTION_LENGTH = 1024 + """:obj:`int`: Maximum number of characters in a :obj:`str` passed as: + + * :paramref:`~telegram.Message.caption` parameter of :class:`telegram.Message` + * :paramref:`~telegram.InputMedia.caption` parameter of :class:`telegram.InputMedia` + and its subclasses + * ``caption`` parameter of subclasses of :class:`telegram.InlineQueryResult` + * ``caption`` parameter of :meth:`telegram.Bot.send_photo`, :meth:`telegram.Bot.send_audio`, + :meth:`telegram.Bot.send_document`, :meth:`telegram.Bot.send_video`, + :meth:`telegram.Bot.send_animation`, :meth:`telegram.Bot.send_voice`, + :meth:`telegram.Bot.edit_message_caption`, :meth:`telegram.Bot.copy_message` + """ + # constants above this line are tested + MIN_TEXT_LENGTH = 1 + """:obj:`int`: Minimum number of characters in a :obj:`str` passed as the + :paramref:`~telegram.InputTextMessageContent.message_text` parameter of + :class:`telegram.InputTextMessageContent` and the + :paramref:`~telegram.Bot.edit_message_text.text` parameter of + :meth:`telegram.Bot.edit_message_text`. + """ + # TODO this constant is not used. helpers.py contains 64 as a number + DEEP_LINK_LENGTH = 64 + """:obj:`int`: Maximum number of characters for a deep link.""" + # TODO this constant is not used anywhere + MESSAGE_ENTITIES = 100 + """:obj:`int`: Maximum number of entities that can be displayed in a message. Further entities + will simply be ignored by Telegram. + + Note: + This value is undocumented and might be changed by Telegram. + """ + + +class MessageOriginType(StringEnum): + """This enum contains the available types of :class:`telegram.MessageOrigin`. The enum + members of this enumeration are instances of :class:`str` and can be treated as such. + + .. versionadded:: 20.8 + """ + + __slots__ = () + + USER = "user" + """:obj:`str`: A :class:`telegram.MessageOrigin` who is sent by an user.""" + HIDDEN_USER = "hidden_user" + """:obj:`str`: A :class:`telegram.MessageOrigin` who is sent by a hidden user.""" + CHAT = "chat" + """:obj:`str`: A :class:`telegram.MessageOrigin` who is sent by a chat.""" + CHANNEL = "channel" + """:obj:`str`: A :class:`telegram.MessageOrigin` who is sent by a channel.""" + + +class MessageType(StringEnum): + """This enum contains the available types of :class:`telegram.Message`. Here, a "type" means + a kind of message that is visually distinct from other kinds of messages in the Telegram app. + In particular, auxiliary attributes that can be present for multiple types of messages are + not considered in this enumeration. + + The enum members of this enumeration are instances of :class:`str` and can be treated as such. + + .. versionadded:: 20.0 + """ + + __slots__ = () + + # Make sure that all attachment type constants are also listed in the + # MessageAttachmentType Enum! (Enums are not extendable) + + ANIMATION = "animation" + """:obj:`str`: Messages with :attr:`telegram.Message.animation`.""" + AUDIO = "audio" + """:obj:`str`: Messages with :attr:`telegram.Message.audio`.""" + BOOST_ADDED = "boost_added" + """:obj:`str`: Messages with :attr:`telegram.Message.boost_added`. + + .. versionadded:: 21.0 + """ + BUSINESS_CONNECTION_ID = "business_connection_id" + """:obj:`str`: Messages with :attr:`telegram.Message.business_connection_id`. + + .. versionadded:: 21.1 + """ + CHANNEL_CHAT_CREATED = "channel_chat_created" + """:obj:`str`: Messages with :attr:`telegram.Message.channel_chat_created`.""" + CHAT_SHARED = "chat_shared" + """:obj:`str`: Messages with :attr:`telegram.Message.chat_shared`. + + .. versionadded:: 20.8 + """ + CHAT_BACKGROUND_SET = "chat_background_set" + """:obj:`str`: Messages with :attr:`telegram.Message.chat_background_set`. + + .. versionadded:: 21.2 + """ + CONNECTED_WEBSITE = "connected_website" + """:obj:`str`: Messages with :attr:`telegram.Message.connected_website`.""" + CONTACT = "contact" + """:obj:`str`: Messages with :attr:`telegram.Message.contact`.""" + DELETE_CHAT_PHOTO = "delete_chat_photo" + """:obj:`str`: Messages with :attr:`telegram.Message.delete_chat_photo`.""" + DICE = "dice" + """:obj:`str`: Messages with :attr:`telegram.Message.dice`.""" + DOCUMENT = "document" + """:obj:`str`: Messages with :attr:`telegram.Message.document`.""" + EFFECT_ID = "effect_id" + """:obj:`str`: Messages with :attr:`telegram.Message.effect_id`. + + .. versionadded:: 21.3""" + FORUM_TOPIC_CREATED = "forum_topic_created" + """:obj:`str`: Messages with :attr:`telegram.Message.forum_topic_created`. + + .. versionadded:: 20.8 + """ + FORUM_TOPIC_CLOSED = "forum_topic_closed" + """:obj:`str`: Messages with :attr:`telegram.Message.forum_topic_closed`. + + .. versionadded:: 20.8 + """ + FORUM_TOPIC_EDITED = "forum_topic_edited" + """:obj:`str`: Messages with :attr:`telegram.Message.forum_topic_edited`. + + .. versionadded:: 20.8 + """ + FORUM_TOPIC_REOPENED = "forum_topic_reopened" + """:obj:`str`: Messages with :attr:`telegram.Message.forum_topic_reopened`. + + .. versionadded:: 20.8 + """ + GAME = "game" + """:obj:`str`: Messages with :attr:`telegram.Message.game`.""" + GENERAL_FORUM_TOPIC_HIDDEN = "general_forum_topic_hidden" + """:obj:`str`: Messages with :attr:`telegram.Message.general_forum_topic_hidden`. + + .. versionadded:: 20.8 + """ + GENERAL_FORUM_TOPIC_UNHIDDEN = "general_forum_topic_unhidden" + """:obj:`str`: Messages with :attr:`telegram.Message.general_forum_topic_unhidden`. + + .. versionadded:: 20.8 + """ + GIVEAWAY = "giveaway" + """:obj:`str`: Messages with :attr:`telegram.Message.giveaway`. + + .. versionadded:: 20.8 + """ + GIVEAWAY_CREATED = "giveaway_created" + """:obj:`str`: Messages with :attr:`telegram.Message.giveaway_created`. + + .. versionadded:: 20.8 + """ + GIVEAWAY_WINNERS = "giveaway_winners" + """:obj:`str`: Messages with :attr:`telegram.Message.giveaway_winners`. + + .. versionadded:: 20.8 + """ + GIVEAWAY_COMPLETED = "giveaway_completed" + """:obj:`str`: Messages with :attr:`telegram.Message.giveaway_completed`. + + .. versionadded:: 20.8 + """ + GROUP_CHAT_CREATED = "group_chat_created" + """:obj:`str`: Messages with :attr:`telegram.Message.group_chat_created`.""" + INVOICE = "invoice" + """:obj:`str`: Messages with :attr:`telegram.Message.invoice`.""" + LEFT_CHAT_MEMBER = "left_chat_member" + """:obj:`str`: Messages with :attr:`telegram.Message.left_chat_member`.""" + LOCATION = "location" + """:obj:`str`: Messages with :attr:`telegram.Message.location`.""" + MESSAGE_AUTO_DELETE_TIMER_CHANGED = "message_auto_delete_timer_changed" + """:obj:`str`: Messages with :attr:`telegram.Message.message_auto_delete_timer_changed`.""" + MIGRATE_TO_CHAT_ID = "migrate_to_chat_id" + """:obj:`str`: Messages with :attr:`telegram.Message.migrate_to_chat_id`.""" + NEW_CHAT_MEMBERS = "new_chat_members" + """:obj:`str`: Messages with :attr:`telegram.Message.new_chat_members`.""" + NEW_CHAT_TITLE = "new_chat_title" + """:obj:`str`: Messages with :attr:`telegram.Message.new_chat_title`.""" + NEW_CHAT_PHOTO = "new_chat_photo" + """:obj:`str`: Messages with :attr:`telegram.Message.new_chat_photo`.""" + PAID_MEDIA = "paid_media" + """:obj:`str`: Messages with :attr:`telegram.Message.paid_media`. + + .. versionadded:: 21.4 + """ + PASSPORT_DATA = "passport_data" + """:obj:`str`: Messages with :attr:`telegram.Message.passport_data`.""" + PHOTO = "photo" + """:obj:`str`: Messages with :attr:`telegram.Message.photo`.""" + PINNED_MESSAGE = "pinned_message" + """:obj:`str`: Messages with :attr:`telegram.Message.pinned_message`.""" + POLL = "poll" + """:obj:`str`: Messages with :attr:`telegram.Message.poll`.""" + PROXIMITY_ALERT_TRIGGERED = "proximity_alert_triggered" + """:obj:`str`: Messages with :attr:`telegram.Message.proximity_alert_triggered`.""" + REFUNDED_PAYMENT = "refunded_payment" + """:obj:`str`: Messages with :attr:`telegram.Message.refunded_payment`. + + .. versionadded:: 21.4 + """ + REPLY_TO_STORY = "reply_to_story" + """:obj:`str`: Messages with :attr:`telegram.Message.reply_to_story`. + + .. versionadded:: 21.0 + """ + SENDER_BOOST_COUNT = "sender_boost_count" + """:obj:`str`: Messages with :attr:`telegram.Message.sender_boost_count`. + + .. versionadded:: 21.0 + """ + SENDER_BUSINESS_BOT = "sender_business_bot" + """:obj:`str`: Messages with :attr:`telegram.Message.sender_business_bot`. + + .. versionadded:: 21.1 + """ + STICKER = "sticker" + """:obj:`str`: Messages with :attr:`telegram.Message.sticker`.""" + STORY = "story" + """:obj:`str`: Messages with :attr:`telegram.Message.story`.""" + SUPERGROUP_CHAT_CREATED = "supergroup_chat_created" + """:obj:`str`: Messages with :attr:`telegram.Message.supergroup_chat_created`.""" + SUCCESSFUL_PAYMENT = "successful_payment" + """:obj:`str`: Messages with :attr:`telegram.Message.successful_payment`.""" + TEXT = "text" + """:obj:`str`: Messages with :attr:`telegram.Message.text`.""" + USERS_SHARED = "users_shared" + """:obj:`str`: Messages with :attr:`telegram.Message.users_shared`. + + .. versionadded:: 20.8 + """ + VENUE = "venue" + """:obj:`str`: Messages with :attr:`telegram.Message.venue`.""" + VIDEO = "video" + """:obj:`str`: Messages with :attr:`telegram.Message.video`.""" + VIDEO_CHAT_SCHEDULED = "video_chat_scheduled" + """:obj:`str`: Messages with :attr:`telegram.Message.video_chat_scheduled`.""" + VIDEO_CHAT_STARTED = "video_chat_started" + """:obj:`str`: Messages with :attr:`telegram.Message.video_chat_started`.""" + VIDEO_CHAT_ENDED = "video_chat_ended" + """:obj:`str`: Messages with :attr:`telegram.Message.video_chat_ended`.""" + VIDEO_CHAT_PARTICIPANTS_INVITED = "video_chat_participants_invited" + """:obj:`str`: Messages with :attr:`telegram.Message.video_chat_participants_invited`.""" + VIDEO_NOTE = "video_note" + """:obj:`str`: Messages with :attr:`telegram.Message.video_note`.""" + VOICE = "voice" + """:obj:`str`: Messages with :attr:`telegram.Message.voice`.""" + WEB_APP_DATA = "web_app_data" + """:obj:`str`: Messages with :attr:`telegram.Message.web_app_data`. + + .. versionadded:: 20.8 + """ + WRITE_ACCESS_ALLOWED = "write_access_allowed" + """:obj:`str`: Messages with :attr:`telegram.Message.write_access_allowed`. + + .. versionadded:: 20.8 + """ + + +class PaidMediaType(StringEnum): + """ + This enum contains the available types of :class:`telegram.PaidMedia`. The enum + members of this enumeration are instances of :class:`str` and can be treated as such. + + .. versionadded:: 21.4 + """ + + __slots__ = () + + PREVIEW = "preview" + """:obj:`str`: The type of :class:`telegram.PaidMediaPreview`.""" + VIDEO = "video" + """:obj:`str`: The type of :class:`telegram.PaidMediaVideo`.""" + PHOTO = "photo" + """:obj:`str`: The type of :class:`telegram.PaidMediaPhoto`.""" + + +class PollingLimit(IntEnum): + """This enum contains limitations for :paramref:`telegram.Bot.get_updates.limit`. + The enum members of this enumeration are instances of :class:`int` and can be treated as such. + + .. versionadded:: 20.0 + """ + + __slots__ = () + + MIN_LIMIT = 1 + """:obj:`int`: Minimum value allowed for the :paramref:`~telegram.Bot.get_updates.limit` + parameter of :meth:`telegram.Bot.get_updates`. + """ + MAX_LIMIT = 100 + """:obj:`int`: Maximum value allowed for the :paramref:`~telegram.Bot.get_updates.limit` + parameter of :meth:`telegram.Bot.get_updates`. + """ + + +class ProfileAccentColor(Enum): + """This enum contains the available accent colors for + :class:`telegram.ChatFullInfo.profile_accent_color_id`. + The members of this enum are named tuples with the following attributes: + + - ``identifier`` (:obj:`int`): The identifier of the accent color. + - ``name`` (:obj:`str`): Optional. The name of the accent color. + - ``light_colors`` (Tuple[:obj:`str`]): Optional. The light colors of the accent color as HEX + value. + - ``dark_colors`` (Tuple[:obj:`str`]): Optional. The dark colors of the accent color as HEX + value. + + Since Telegram gives no exact specification for the accent colors, future accent colors might + have a different data type. + + .. versionadded:: 20.8 + """ + + __slots__ = () + + COLOR_000 = _AccentColor(identifier=0, light_colors=(0xBA5650,), dark_colors=(0x9C4540,)) + """Accent color 0. This contains one light color + + .. raw:: html + +
+
+ + and one dark color + + .. raw:: html + +
+
+ """ + COLOR_001 = _AccentColor(identifier=1, light_colors=(0xC27C3E,), dark_colors=(0x945E2C,)) + """Accent color 1. This contains one light color + + .. raw:: html + +
+
+ + and one dark color + + .. raw:: html + +
+
+ """ + COLOR_002 = _AccentColor(identifier=2, light_colors=(0x956AC8,), dark_colors=(0x715099,)) + """Accent color 2. This contains one light color + + .. raw:: html + +
+
+ + and one dark color + + .. raw:: html + +
+
+ """ + COLOR_003 = _AccentColor(identifier=3, light_colors=(0x49A355,), dark_colors=(0x33713B,)) + """Accent color 3. This contains one light color + + .. raw:: html + +
+
+ + and one dark color + + .. raw:: html + +
+
+ """ + COLOR_004 = _AccentColor(identifier=4, light_colors=(0x3E97AD,), dark_colors=(0x387E87,)) + """Accent color 4. This contains one light color + + .. raw:: html + +
+
+ + and one dark color + + .. raw:: html + +
+
+ """ + COLOR_005 = _AccentColor(identifier=5, light_colors=(0x5A8FBB,), dark_colors=(0x477194,)) + """Accent color 5. This contains one light color + + .. raw:: html + +
+
+ + and one dark color + + .. raw:: html + +
+
+ """ + COLOR_006 = _AccentColor(identifier=6, light_colors=(0xB85378,), dark_colors=(0x944763,)) + """Accent color 6. This contains one light color + + .. raw:: html + +
+
+ + and one dark color + + .. raw:: html + +
+
+ """ + COLOR_007 = _AccentColor(identifier=7, light_colors=(0x7F8B95,), dark_colors=(0x435261,)) + """Accent color 7. This contains one light color + + .. raw:: html + +
+
+ + and one dark color + + .. raw:: html + +
+
+ """ + COLOR_008 = _AccentColor( + identifier=8, light_colors=(0xC9565D, 0xD97C57), dark_colors=(0x994343, 0xAC583E) + ) + """Accent color 8. This contains two light colors + + .. raw:: html + +
+
+
+

+ + and two dark colors + + .. raw:: html + +
+
+
+

+ """ + COLOR_009 = _AccentColor( + identifier=9, light_colors=(0xCF7244, 0xCC9433), dark_colors=(0x8F552F, 0xA17232) + ) + """Accent color 9. This contains two light colors + + .. raw:: html + +
+
+
+

+ + and two dark colors + + .. raw:: html + +
+
+
+

+ """ + COLOR_010 = _AccentColor( + identifier=10, light_colors=(0x9662D4, 0xB966B6), dark_colors=(0x634691, 0x9250A2) + ) + """Accent color 10. This contains two light colors + + .. raw:: html + +
+
+
+

+ + and two dark colors + + .. raw:: html + +
+
+
+

+ """ + COLOR_011 = _AccentColor( + identifier=11, light_colors=(0x3D9755, 0x89A650), dark_colors=(0x296A43, 0x5F8F44) + ) + """Accent color 11. This contains two light colors + + .. raw:: html + +
+
+
+

+ + and two dark colors + + .. raw:: html + +
+
+
+

+ """ + COLOR_012 = _AccentColor( + identifier=12, light_colors=(0x3D95BA, 0x50AD98), dark_colors=(0x306C7C, 0x3E987E) + ) + """Accent color 12. This contains two light colors + + .. raw:: html + +
+
+
+

+ + and two dark colors + + .. raw:: html + +
+
+
+

+ """ + COLOR_013 = _AccentColor( + identifier=13, light_colors=(0x538BC2, 0x4DA8BD), dark_colors=(0x38618C, 0x458BA1) + ) + """Accent color 13. This contains two light colors + + .. raw:: html + +
+
+
+

+ + and two dark colors + + .. raw:: html + +
+
+
+

+ """ + COLOR_014 = _AccentColor( + identifier=14, light_colors=(0xB04F74, 0xD1666D), dark_colors=(0x884160, 0xA65259) + ) + """Accent color 14. This contains two light colors + + .. raw:: html + +
+
+
+

+ + and two dark colors + + .. raw:: html + +
+
+
+

+ """ + COLOR_015 = _AccentColor( + identifier=15, light_colors=(0x637482, 0x7B8A97), dark_colors=(0x53606E, 0x384654) + ) + """Accent color 15. This contains two light colors + + .. raw:: html + +
+
+
+

+ + and two dark colors + + .. raw:: html + +
+
+
+

+ """ + + +class ReplyLimit(IntEnum): + """This enum contains limitations for :class:`telegram.ForceReply` + and :class:`telegram.ReplyKeyboardMarkup`. + The enum members of this enumeration are instances of :class:`int` and can be treated as such. + + .. versionadded:: 20.0 + """ + + __slots__ = () + + MIN_INPUT_FIELD_PLACEHOLDER = 1 + """:obj:`int`: Minimum value allowed for + :paramref:`~telegram.ForceReply.input_field_placeholder` parameter of + :class:`telegram.ForceReply` and + :paramref:`~telegram.ReplyKeyboardMarkup.input_field_placeholder` parameter of + :class:`telegram.ReplyKeyboardMarkup` + """ + MAX_INPUT_FIELD_PLACEHOLDER = 64 + """:obj:`int`: Maximum value allowed for + :paramref:`~telegram.ForceReply.input_field_placeholder` parameter of + :class:`telegram.ForceReply` and + :paramref:`~telegram.ReplyKeyboardMarkup.input_field_placeholder` parameter of + :class:`telegram.ReplyKeyboardMarkup` + """ + + +class RevenueWithdrawalStateType(StringEnum): + """This enum contains the available types of :class:`telegram.RevenueWithdrawalState`. + The enum members of this enumeration are instances of :class:`str` and can be treated as such. + + .. versionadded:: 21.4 + """ + + __slots__ = () + + PENDING = "pending" + """:obj:`str`: A withdrawal in progress.""" + SUCCEEDED = "succeeded" + """:obj:`str`: A withdrawal succeeded.""" + FAILED = "failed" + """:obj:`str`: A withdrawal failed and the transaction was refunded.""" + + +class StarTransactionsLimit(IntEnum): + """This enum contains limitations for :class:`telegram.Bot.get_star_transactions`. + The enum members of this enumeration are instances of :class:`int` and can be treated as such. + + .. versionadded:: 21.4 + """ + + __slots__ = () + + MIN_LIMIT = 1 + """:obj:`int`: Minimum value allowed for the + :paramref:`~telegram.Bot.get_star_transactions.limit` parameter of + :meth:`telegram.Bot.get_star_transactions`.""" + MAX_LIMIT = 100 + """:obj:`int`: Maximum value allowed for the + :paramref:`~telegram.Bot.get_star_transactions.limit` parameter of + :meth:`telegram.Bot.get_star_transactions`.""" + + +class StickerFormat(StringEnum): + """This enum contains the available formats of :class:`telegram.Sticker` in the set. The enum + members of this enumeration are instances of :class:`str` and can be treated as such. + + .. versionadded:: 20.2 + """ + + __slots__ = () + + STATIC = "static" + """:obj:`str`: Static sticker.""" + ANIMATED = "animated" + """:obj:`str`: Animated sticker.""" + VIDEO = "video" + """:obj:`str`: Video sticker.""" + + +class StickerLimit(IntEnum): + """This enum contains limitations for various sticker methods, such as + :meth:`telegram.Bot.create_new_sticker_set`. + The enum members of this enumeration are instances of :class:`int` and can be treated as such. + + .. versionadded:: 20.0 + """ + + __slots__ = () + + MIN_NAME_AND_TITLE = 1 + """:obj:`int`: Minimum number of characters in a :obj:`str` passed as the + :paramref:`~telegram.Bot.create_new_sticker_set.name` parameter or the + :paramref:`~telegram.Bot.create_new_sticker_set.title` parameter of + :meth:`telegram.Bot.create_new_sticker_set`. + """ + MAX_NAME_AND_TITLE = 64 + """:obj:`int`: Maximum number of characters in a :obj:`str` passed as the + :paramref:`~telegram.Bot.create_new_sticker_set.name` parameter or the + :paramref:`~telegram.Bot.create_new_sticker_set.title` parameter of + :meth:`telegram.Bot.create_new_sticker_set`. + """ + MIN_STICKER_EMOJI = 1 + """:obj:`int`: Minimum number of emojis associated with a sticker, passed as the + :paramref:`~telegram.Bot.setStickerEmojiList.emoji_list` parameter of + :meth:`telegram.Bot.set_sticker_emoji_list`. + + .. versionadded:: 20.2 + """ + MAX_STICKER_EMOJI = 20 + """:obj:`int`: Maximum number of emojis associated with a sticker, passed as the + :paramref:`~telegram.Bot.setStickerEmojiList.emoji_list` parameter of + :meth:`telegram.Bot.set_sticker_emoji_list`. + + .. versionadded:: 20.2 + """ + MAX_SEARCH_KEYWORDS = 20 + """:obj:`int`: Maximum number of search keywords for a sticker, passed as the + :paramref:`~telegram.Bot.set_sticker_keywords.keywords` parameter of + :meth:`telegram.Bot.set_sticker_keywords`. + + .. versionadded:: 20.2 + """ + MAX_KEYWORD_LENGTH = 64 + """:obj:`int`: Maximum number of characters in a search keyword for a sticker, for each item in + :paramref:`~telegram.Bot.set_sticker_keywords.keywords` sequence of + :meth:`telegram.Bot.set_sticker_keywords`. + + .. versionadded:: 20.2 + """ + + +class StickerSetLimit(IntEnum): + """This enum contains limitations for various sticker set methods, such as + :meth:`telegram.Bot.create_new_sticker_set` and :meth:`telegram.Bot.add_sticker_to_set`. + + The enum members of this enumeration are instances of :class:`int` and can be treated as such. + + .. versionadded:: 20.2 + """ + + __slots__ = () + + MIN_INITIAL_STICKERS = 1 + """:obj:`int`: Minimum number of stickers needed to create a sticker set, passed as the + :paramref:`~telegram.Bot.create_new_sticker_set.stickers` parameter of + :meth:`telegram.Bot.create_new_sticker_set`. + """ + MAX_INITIAL_STICKERS = 50 + """:obj:`int`: Maximum number of stickers allowed while creating a sticker set, passed as the + :paramref:`~telegram.Bot.create_new_sticker_set.stickers` parameter of + :meth:`telegram.Bot.create_new_sticker_set`. + """ + MAX_EMOJI_STICKERS = 200 + """:obj:`int`: Maximum number of stickers allowed in an emoji sticker set, as given in + :meth:`telegram.Bot.add_sticker_to_set`. + """ + MAX_ANIMATED_STICKERS = 50 + """:obj:`int`: Maximum number of stickers allowed in an animated or video sticker set, as given + in :meth:`telegram.Bot.add_sticker_to_set`. + + .. deprecated:: 21.1 + The animated sticker limit is now 120, the same as :attr:`MAX_STATIC_STICKERS`. + """ + MAX_STATIC_STICKERS = 120 + """:obj:`int`: Maximum number of stickers allowed in a static sticker set, as given in + :meth:`telegram.Bot.add_sticker_to_set`. + """ + MAX_STATIC_THUMBNAIL_SIZE = 128 + """:obj:`int`: Maximum size of the thumbnail if it is a **.WEBP** or **.PNG** in kilobytes, + as given in :meth:`telegram.Bot.set_sticker_set_thumbnail`.""" + MAX_ANIMATED_THUMBNAIL_SIZE = 32 + """:obj:`int`: Maximum size of the thumbnail if it is a **.TGS** or **.WEBM** in kilobytes, + as given in :meth:`telegram.Bot.set_sticker_set_thumbnail`.""" + STATIC_THUMB_DIMENSIONS = 100 + """:obj:`int`: Exact height and width of the thumbnail if it is a **.WEBP** or **.PNG** in + pixels, as given in :meth:`telegram.Bot.set_sticker_set_thumbnail`.""" + + +class StickerType(StringEnum): + """This enum contains the available types of :class:`telegram.Sticker`. The enum + members of this enumeration are instances of :class:`str` and can be treated as such. + + .. versionadded:: 20.0 + """ + + __slots__ = () + + REGULAR = "regular" + """:obj:`str`: Regular sticker.""" + MASK = "mask" + """:obj:`str`: Mask sticker.""" + CUSTOM_EMOJI = "custom_emoji" + """:obj:`str`: Custom emoji sticker.""" + + +class TransactionPartnerType(StringEnum): + """This enum contains the available types of :class:`telegram.TransactionPartner`. The enum + members of this enumeration are instances of :class:`str` and can be treated as such. + + .. versionadded:: 21.4 + """ + + __slots__ = () + + FRAGMENT = "fragment" + """:obj:`str`: Withdrawal transaction with Fragment.""" + USER = "user" + """:obj:`str`: Transaction with a user.""" + OTHER = "other" + """:obj:`str`: Transaction with unknown source or recipient.""" + TELEGRAM_ADS = "telegram_ads" + """:obj:`str`: Transaction with Telegram Ads.""" + + +class ParseMode(StringEnum): + """This enum contains the available parse modes. The enum + members of this enumeration are instances of :class:`str` and can be treated as such. + + .. versionadded:: 20.0 + """ + + __slots__ = () + + MARKDOWN = "Markdown" + """:obj:`str`: Markdown parse mode. + + Note: + :attr:`MARKDOWN` is a legacy mode, retained by Telegram for backward compatibility. + You should use :attr:`MARKDOWN_V2` instead. + """ + MARKDOWN_V2 = "MarkdownV2" + """:obj:`str`: Markdown parse mode version 2.""" + HTML = "HTML" + """:obj:`str`: HTML parse mode.""" + + +class PollLimit(IntEnum): + """This enum contains limitations for :class:`telegram.Poll`/:class:`telegram.PollOption`/ + :meth:`telegram.Bot.send_poll`. The enum + members of this enumeration are instances of :class:`int` and can be treated as such. + + .. versionadded:: 20.0 + """ + + __slots__ = () + + MIN_QUESTION_LENGTH = 1 + """:obj:`int`: Minimum value allowed for the :paramref:`~telegram.Poll.question` + parameter of :class:`telegram.Poll` and the :paramref:`~telegram.Bot.send_poll.question` + parameter of :meth:`telegram.Bot.send_poll`. + """ + MAX_QUESTION_LENGTH = 300 + """:obj:`int`: Maximum value allowed for the :paramref:`~telegram.Poll.question` + parameter of :class:`telegram.Poll` and the :paramref:`~telegram.Bot.send_poll.question` + parameter of :meth:`telegram.Bot.send_poll`. + """ + MIN_OPTION_LENGTH = 1 + """:obj:`int`: Minimum length of each :obj:`str` passed in a :obj:`list` + to the :paramref:`~telegram.Bot.send_poll.options` parameter of + :meth:`telegram.Bot.send_poll`. + """ + MAX_OPTION_LENGTH = 100 + """:obj:`int`: Maximum length of each :obj:`str` passed in a :obj:`list` + to the :paramref:`~telegram.Bot.send_poll.options` parameter of + :meth:`telegram.Bot.send_poll`. + """ + MIN_OPTION_NUMBER = 2 + """:obj:`int`: Minimum number of strings passed in a :obj:`list` + to the :paramref:`~telegram.Bot.send_poll.options` parameter of + :meth:`telegram.Bot.send_poll`. + """ + MAX_OPTION_NUMBER = 10 + """:obj:`int`: Maximum number of strings passed in a :obj:`list` + to the :paramref:`~telegram.Bot.send_poll.options` parameter of + :meth:`telegram.Bot.send_poll`. + """ + MAX_EXPLANATION_LENGTH = 200 + """:obj:`int`: Maximum number of characters in a :obj:`str` passed as the + :paramref:`~telegram.Poll.explanation` parameter of :class:`telegram.Poll` and the + :paramref:`~telegram.Bot.send_poll.explanation` parameter of :meth:`telegram.Bot.send_poll`. + """ + MAX_EXPLANATION_LINE_FEEDS = 2 + """:obj:`int`: Maximum number of line feeds in a :obj:`str` passed as the + :paramref:`~telegram.Bot.send_poll.explanation` parameter of :meth:`telegram.Bot.send_poll` + after entities parsing. + """ + MIN_OPEN_PERIOD = 5 + """:obj:`int`: Minimum value allowed for the + :paramref:`~telegram.Bot.send_poll.open_period` parameter of :meth:`telegram.Bot.send_poll`. + Also used in the :paramref:`~telegram.Bot.send_poll.close_date` parameter of + :meth:`telegram.Bot.send_poll`. + """ + MAX_OPEN_PERIOD = 600 + """:obj:`int`: Maximum value allowed for the + :paramref:`~telegram.Bot.send_poll.open_period` parameter of :meth:`telegram.Bot.send_poll`. + Also used in the :paramref:`~telegram.Bot.send_poll.close_date` parameter of + :meth:`telegram.Bot.send_poll`. + """ + + +class PollType(StringEnum): + """This enum contains the available types for :class:`telegram.Poll`/ + :meth:`telegram.Bot.send_poll`. The enum + members of this enumeration are instances of :class:`str` and can be treated as such. + + .. versionadded:: 20.0 + """ + + __slots__ = () + + REGULAR = "regular" + """:obj:`str`: regular polls.""" + QUIZ = "quiz" + """:obj:`str`: quiz polls.""" + + +class UpdateType(StringEnum): + """This enum contains the available types of :class:`telegram.Update`. The enum + members of this enumeration are instances of :class:`str` and can be treated as such. + + .. versionadded:: 20.0 + """ + + __slots__ = () + + MESSAGE = "message" + """:obj:`str`: Updates with :attr:`telegram.Update.message`.""" + EDITED_MESSAGE = "edited_message" + """:obj:`str`: Updates with :attr:`telegram.Update.edited_message`.""" + CHANNEL_POST = "channel_post" + """:obj:`str`: Updates with :attr:`telegram.Update.channel_post`.""" + EDITED_CHANNEL_POST = "edited_channel_post" + """:obj:`str`: Updates with :attr:`telegram.Update.edited_channel_post`.""" + INLINE_QUERY = "inline_query" + """:obj:`str`: Updates with :attr:`telegram.Update.inline_query`.""" + CHOSEN_INLINE_RESULT = "chosen_inline_result" + """:obj:`str`: Updates with :attr:`telegram.Update.chosen_inline_result`.""" + CALLBACK_QUERY = "callback_query" + """:obj:`str`: Updates with :attr:`telegram.Update.callback_query`.""" + SHIPPING_QUERY = "shipping_query" + """:obj:`str`: Updates with :attr:`telegram.Update.shipping_query`.""" + PRE_CHECKOUT_QUERY = "pre_checkout_query" + """:obj:`str`: Updates with :attr:`telegram.Update.pre_checkout_query`.""" + POLL = "poll" + """:obj:`str`: Updates with :attr:`telegram.Update.poll`.""" + POLL_ANSWER = "poll_answer" + """:obj:`str`: Updates with :attr:`telegram.Update.poll_answer`.""" + MY_CHAT_MEMBER = "my_chat_member" + """:obj:`str`: Updates with :attr:`telegram.Update.my_chat_member`.""" + CHAT_MEMBER = "chat_member" + """:obj:`str`: Updates with :attr:`telegram.Update.chat_member`.""" + CHAT_JOIN_REQUEST = "chat_join_request" + """:obj:`str`: Updates with :attr:`telegram.Update.chat_join_request`.""" + CHAT_BOOST = "chat_boost" + """:obj:`str`: Updates with :attr:`telegram.Update.chat_boost`. + + .. versionadded:: 20.8 + """ + REMOVED_CHAT_BOOST = "removed_chat_boost" + """:obj:`str`: Updates with :attr:`telegram.Update.removed_chat_boost`. + + .. versionadded:: 20.8 + """ + MESSAGE_REACTION = "message_reaction" + """:obj:`str`: Updates with :attr:`telegram.Update.message_reaction`. + + .. versionadded:: 20.8 + """ + MESSAGE_REACTION_COUNT = "message_reaction_count" + """:obj:`str`: Updates with :attr:`telegram.Update.message_reaction_count`. + + .. versionadded:: 20.8 + """ + BUSINESS_CONNECTION = "business_connection" + """:obj:`str`: Updates with :attr:`telegram.Update.business_connection`. + + .. versionadded:: 21.1 + """ + BUSINESS_MESSAGE = "business_message" + """:obj:`str`: Updates with :attr:`telegram.Update.business_message`. + + .. versionadded:: 21.1 + """ + EDITED_BUSINESS_MESSAGE = "edited_business_message" + """:obj:`str`: Updates with :attr:`telegram.Update.edited_business_message`. + + .. versionadded:: 21.1 + """ + DELETED_BUSINESS_MESSAGES = "deleted_business_messages" + """:obj:`str`: Updates with :attr:`telegram.Update.deleted_business_messages`. + + .. versionadded:: 21.1 + """ + + +class InvoiceLimit(IntEnum): + """This enum contains limitations for :class:`telegram.InputInvoiceMessageContent`, + :meth:`telegram.Bot.send_invoice`, and :meth:`telegram.Bot.create_invoice_link`. + The enum members of this enumeration are instances of :class:`int` and can be treated as such. + + .. versionadded:: 20.0 + """ + + __slots__ = () + + MIN_TITLE_LENGTH = 1 + """:obj:`int`: Minimum number of characters in a :obj:`str` passed as: + + * :paramref:`~telegram.InputInvoiceMessageContent.title` parameter of + :class:`telegram.InputInvoiceMessageContent` + * :paramref:`~telegram.Bot.send_invoice.title` parameter of + :meth:`telegram.Bot.send_invoice`. + * :paramref:`~telegram.Bot.create_invoice_link.title` parameter of + :meth:`telegram.Bot.create_invoice_link`. + """ + MAX_TITLE_LENGTH = 32 + """:obj:`int`: Maximum number of characters in a :obj:`str` passed as: + + * :paramref:`~telegram.InputInvoiceMessageContent.title` parameter of + :class:`telegram.InputInvoiceMessageContent` + * :paramref:`~telegram.Bot.send_invoice.title` parameter of + :meth:`telegram.Bot.send_invoice`. + * :paramref:`~telegram.Bot.create_invoice_link.title` parameter of + :meth:`telegram.Bot.create_invoice_link`. + """ + MIN_DESCRIPTION_LENGTH = 1 + """:obj:`int`: Minimum number of characters in a :obj:`str` passed as: + + * :paramref:`~telegram.InputInvoiceMessageContent.description` parameter of + :class:`telegram.InputInvoiceMessageContent` + * :paramref:`~telegram.Bot.send_invoice.description` parameter of + :meth:`telegram.Bot.send_invoice`. + * :paramref:`~telegram.Bot.create_invoice_link.description` parameter of + :meth:`telegram.Bot.create_invoice_link`. + """ + MAX_DESCRIPTION_LENGTH = 255 + """:obj:`int`: Maximum number of characters in a :obj:`str` passed as: + + * :paramref:`~telegram.InputInvoiceMessageContent.description` parameter of + :class:`telegram.InputInvoiceMessageContent` + * :paramref:`~telegram.Bot.send_invoice.description` parameter of + :meth:`telegram.Bot.send_invoice`. + * :paramref:`~telegram.Bot.create_invoice_link.description` parameter of + :meth:`telegram.Bot.create_invoice_link`. + """ + MIN_PAYLOAD_LENGTH = 1 + """:obj:`int`: Minimum amount of bytes in a :obj:`str` passed as: + + * :paramref:`~telegram.InputInvoiceMessageContent.payload` parameter of + :class:`telegram.InputInvoiceMessageContent` + * :paramref:`~telegram.Bot.send_invoice.payload` parameter of + :meth:`telegram.Bot.send_invoice`. + * :paramref:`~telegram.Bot.create_invoice_link.payload` parameter of + :meth:`telegram.Bot.create_invoice_link`. + """ + MAX_PAYLOAD_LENGTH = 128 + """:obj:`int`: Maximum amount of bytes in a :obj:`str` passed as: + + * :paramref:`~telegram.InputInvoiceMessageContent.payload` parameter of + :class:`telegram.InputInvoiceMessageContent` + * :paramref:`~telegram.Bot.send_invoice.payload` parameter of + :meth:`telegram.Bot.send_invoice`. + * :paramref:`~telegram.Bot.create_invoice_link.payload` parameter of + :meth:`telegram.Bot.create_invoice_link`. + """ + MAX_TIP_AMOUNTS = 4 + """:obj:`int`: Maximum length of a :obj:`Sequence` passed as: + + * :paramref:`~telegram.Bot.send_invoice.suggested_tip_amounts` parameter of + :meth:`telegram.Bot.send_invoice`. + * :paramref:`~telegram.Bot.create_invoice_link.suggested_tip_amounts` parameter of + :meth:`telegram.Bot.create_invoice_link`. + """ + + +class UserProfilePhotosLimit(IntEnum): + """This enum contains limitations for :paramref:`telegram.Bot.get_user_profile_photos.limit`. + The enum members of this enumeration are instances of :class:`int` and can be treated as such. + + .. versionadded:: 20.0 + """ + + __slots__ = () + + MIN_LIMIT = 1 + """:obj:`int`: Minimum value allowed for + :paramref:`~telegram.Bot.get_user_profile_photos.limit` parameter of + :meth:`telegram.Bot.get_user_profile_photos`. + """ + MAX_LIMIT = 100 + """:obj:`int`: Maximum value allowed for + :paramref:`~telegram.Bot.get_user_profile_photos.limit` parameter of + :meth:`telegram.Bot.get_user_profile_photos`. + """ + + +class WebhookLimit(IntEnum): + """This enum contains limitations for :paramref:`telegram.Bot.set_webhook.max_connections` and + :paramref:`telegram.Bot.set_webhook.secret_token`. The enum members of this enumeration are + instances of :class:`int` and can be treated as such. + + .. versionadded:: 20.0 + """ + + __slots__ = () + + MIN_CONNECTIONS_LIMIT = 1 + """:obj:`int`: Minimum value allowed for the + :paramref:`~telegram.Bot.set_webhook.max_connections` parameter of + :meth:`telegram.Bot.set_webhook`. + """ + MAX_CONNECTIONS_LIMIT = 100 + """:obj:`int`: Maximum value allowed for the + :paramref:`~telegram.Bot.set_webhook.max_connections` parameter of + :meth:`telegram.Bot.set_webhook`. + """ + MIN_SECRET_TOKEN_LENGTH = 1 + """:obj:`int`: Minimum length of the secret token for the + :paramref:`~telegram.Bot.set_webhook.secret_token` parameter of + :meth:`telegram.Bot.set_webhook`. + """ + MAX_SECRET_TOKEN_LENGTH = 256 + """:obj:`int`: Maximum length of the secret token for the + :paramref:`~telegram.Bot.set_webhook.secret_token` parameter of + :meth:`telegram.Bot.set_webhook`. + """ + + +class ForumTopicLimit(IntEnum): + """This enum contains limitations for :paramref:`telegram.Bot.create_forum_topic.name` and + :paramref:`telegram.Bot.edit_forum_topic.name`. + The enum members of this enumeration are instances of :class:`int` and can be treated as such. + + .. versionadded:: 20.0 + """ + + __slots__ = () + + MIN_NAME_LENGTH = 1 + """:obj:`int`: Minimum length of a :obj:`str` passed as: + + * :paramref:`~telegram.Bot.create_forum_topic.name` parameter of + :meth:`telegram.Bot.create_forum_topic` + * :paramref:`~telegram.Bot.edit_forum_topic.name` parameter of + :meth:`telegram.Bot.edit_forum_topic` + * :paramref:`~telegram.Bot.edit_general_forum_topic.name` parameter of + :meth:`telegram.Bot.edit_general_forum_topic` + """ + MAX_NAME_LENGTH = 128 + """:obj:`int`: Maximum length of a :obj:`str` passed as: + + * :paramref:`~telegram.Bot.create_forum_topic.name` parameter of + :meth:`telegram.Bot.create_forum_topic` + * :paramref:`~telegram.Bot.edit_forum_topic.name` parameter of + :meth:`telegram.Bot.edit_forum_topic` + * :paramref:`~telegram.Bot.edit_general_forum_topic.name` parameter of + :meth:`telegram.Bot.edit_general_forum_topic` + """ + + +class ReactionType(StringEnum): + """This enum contains the available types of :class:`telegram.ReactionType`. The enum + members of this enumeration are instances of :class:`str` and can be treated as such. + + .. versionadded:: 20.8 + """ + + __slots__ = () + + EMOJI = "emoji" + """:obj:`str`: A :class:`telegram.ReactionType` with a normal emoji.""" + CUSTOM_EMOJI = "custom_emoji" + """:obj:`str`: A :class:`telegram.ReactionType` with a custom emoji.""" + PAID = "paid" + """:obj:`str`: A :class:`telegram.ReactionType` with a paid reaction. + + .. versionadded:: 21.5 + """ + + +class ReactionEmoji(StringEnum): + """This enum contains the available emojis of :class:`telegram.ReactionTypeEmoji`. The enum + members of this enumeration are instances of :class:`str` and can be treated as such. + + .. versionadded:: 20.8 + """ + + __slots__ = () + + THUMBS_UP = "👍" + """:obj:`str`: Thumbs Up""" + THUMBS_DOWN = "👎" + """:obj:`str`: Thumbs Down""" + RED_HEART = "❤" + """:obj:`str`: Red Heart""" + FIRE = "🔥" + """:obj:`str`: Fire""" + SMILING_FACE_WITH_HEARTS = "🥰" + """:obj:`str`: Smiling Face with Hearts""" + CLAPPING_HANDS = "👏" + """:obj:`str`: Clapping Hands""" + GRINNING_FACE_WITH_SMILING_EYES = "😁" + """:obj:`str`: Grinning face with smiling eyes""" + THINKING_FACE = "🤔" + """:obj:`str`: Thinking face""" + SHOCKED_FACE_WITH_EXPLODING_HEAD = "🤯" + """:obj:`str`: Shocked face with exploding head""" + FACE_SCREAMING_IN_FEAR = "😱" + """:obj:`str`: Face screaming in fear""" + SERIOUS_FACE_WITH_SYMBOLS_COVERING_MOUTH = "🤬" + """:obj:`str`: Serious face with symbols covering mouth""" + CRYING_FACE = "😢" + """:obj:`str`: Crying face""" + PARTY_POPPER = "🎉" + """:obj:`str`: Party popper""" + GRINNING_FACE_WITH_STAR_EYES = "🤩" + """:obj:`str`: Grinning face with star eyes""" + FACE_WITH_OPEN_MOUTH_VOMITING = "🤮" + """:obj:`str`: Face with open mouth vomiting""" + PILE_OF_POO = "💩" + """:obj:`str`: Pile of poo""" + PERSON_WITH_FOLDED_HANDS = "🙏" + """:obj:`str`: Person with folded hands""" + OK_HAND_SIGN = "👌" + """:obj:`str`: Ok hand sign""" + DOVE_OF_PEACE = "🕊" + """:obj:`str`: Dove of peace""" + CLOWN_FACE = "🤡" + """:obj:`str`: Clown face""" + YAWNING_FACE = "🥱" + """:obj:`str`: Yawning face""" + FACE_WITH_UNEVEN_EYES_AND_WAVY_MOUTH = "🥴" + """:obj:`str`: Face with uneven eyes and wavy mouth""" + SMILING_FACE_WITH_HEART_SHAPED_EYES = "😍" + """:obj:`str`: Smiling face with heart-shaped eyes""" + SPOUTING_WHALE = "🐳" + """:obj:`str`: Spouting whale""" + HEART_ON_FIRE = "❤️‍🔥" + """:obj:`str`: Heart on fire""" + NEW_MOON_WITH_FACE = "🌚" + """:obj:`str`: New moon with face""" + HOT_DOG = "🌭" + """:obj:`str`: Hot dog""" + HUNDRED_POINTS_SYMBOL = "💯" + """:obj:`str`: Hundred points symbol""" + ROLLING_ON_THE_FLOOR_LAUGHING = "🤣" + """:obj:`str`: Rolling on the floor laughing""" + HIGH_VOLTAGE_SIGN = "⚡" + """:obj:`str`: High voltage sign""" + BANANA = "🍌" + """:obj:`str`: Banana""" + TROPHY = "🏆" + """:obj:`str`: Trophy""" + BROKEN_HEART = "💔" + """:obj:`str`: Broken heart""" + FACE_WITH_ONE_EYEBROW_RAISED = "🤨" + """:obj:`str`: Face with one eyebrow raised""" + NEUTRAL_FACE = "😐" + """:obj:`str`: Neutral face""" + STRAWBERRY = "🍓" + """:obj:`str`: Strawberry""" + BOTTLE_WITH_POPPING_CORK = "🍾" + """:obj:`str`: Bottle with popping cork""" + KISS_MARK = "💋" + """:obj:`str`: Kiss mark""" + REVERSED_HAND_WITH_MIDDLE_FINGER_EXTENDED = "🖕" + """:obj:`str`: Reversed hand with middle finger extended""" + SMILING_FACE_WITH_HORNS = "😈" + """:obj:`str`: Smiling face with horns""" + SLEEPING_FACE = "😴" + """:obj:`str`: Sleeping face""" + LOUDLY_CRYING_FACE = "😭" + """:obj:`str`: Loudly crying face""" + NERD_FACE = "🤓" + """:obj:`str`: Nerd face""" + GHOST = "👻" + """:obj:`str`: Ghost""" + MAN_TECHNOLOGIST = "👨‍💻" + """:obj:`str`: Man Technologist""" + EYES = "👀" + """:obj:`str`: Eyes""" + JACK_O_LANTERN = "🎃" + """:obj:`str`: Jack-o-lantern""" + SEE_NO_EVIL_MONKEY = "🙈" + """:obj:`str`: See-no-evil monkey""" + SMILING_FACE_WITH_HALO = "😇" + """:obj:`str`: Smiling face with halo""" + FEARFUL_FACE = "😨" + """:obj:`str`: Fearful face""" + HANDSHAKE = "🤝" + """:obj:`str`: Handshake""" + WRITING_HAND = "✍" + """:obj:`str`: Writing hand""" + HUGGING_FACE = "🤗" + """:obj:`str`: Hugging face""" + SALUTING_FACE = "🫡" + """:obj:`str`: Saluting face""" + FATHER_CHRISTMAS = "🎅" + """:obj:`str`: Father christmas""" + CHRISTMAS_TREE = "🎄" + """:obj:`str`: Christmas tree""" + SNOWMAN = "☃" + """:obj:`str`: Snowman""" + NAIL_POLISH = "💅" + """:obj:`str`: Nail polish""" + GRINNING_FACE_WITH_ONE_LARGE_AND_ONE_SMALL_EYE = "🤪" + """:obj:`str`: Grinning face with one large and one small eye""" + MOYAI = "🗿" + """:obj:`str`: Moyai""" + SQUARED_COOL = "🆒" + """:obj:`str`: Squared cool""" + HEART_WITH_ARROW = "💘" + """:obj:`str`: Heart with arrow""" + HEAR_NO_EVIL_MONKEY = "🙉" + """:obj:`str`: Hear-no-evil monkey""" + UNICORN_FACE = "🦄" + """:obj:`str`: Unicorn face""" + FACE_THROWING_A_KISS = "😘" + """:obj:`str`: Face throwing a kiss""" + PILL = "💊" + """:obj:`str`: Pill""" + SPEAK_NO_EVIL_MONKEY = "🙊" + """:obj:`str`: Speak-no-evil monkey""" + SMILING_FACE_WITH_SUNGLASSES = "😎" + """:obj:`str`: Smiling face with sunglasses""" + ALIEN_MONSTER = "👾" + """:obj:`str`: Alien monster""" + MAN_SHRUGGING = "🤷‍♂️" + """:obj:`str`: Man Shrugging""" + SHRUG = "🤷" + """:obj:`str`: Shrug""" + WOMAN_SHRUGGING = "🤷‍♀️" + """:obj:`str`: Woman Shrugging""" + POUTING_FACE = "😡" + """:obj:`str`: Pouting face""" + + +class BackgroundTypeType(StringEnum): + """This enum contains the available types of :class:`telegram.BackgroundType`. The enum + members of this enumeration are instances of :class:`str` and can be treated as such. + + .. versionadded:: 21.2 + """ + + __slots__ = () + + FILL = "fill" + """:obj:`str`: A :class:`telegram.BackgroundType` with fill background.""" + WALLPAPER = "wallpaper" + """:obj:`str`: A :class:`telegram.BackgroundType` with wallpaper background.""" + PATTERN = "pattern" + """:obj:`str`: A :class:`telegram.BackgroundType` with pattern background.""" + CHAT_THEME = "chat_theme" + """:obj:`str`: A :class:`telegram.BackgroundType` with chat_theme background.""" + + +class BackgroundFillType(StringEnum): + """This enum contains the available types of :class:`telegram.BackgroundFill`. The enum + members of this enumeration are instances of :class:`str` and can be treated as such. + + .. versionadded:: 21.2 + """ + + __slots__ = () + + SOLID = "solid" + """:obj:`str`: A :class:`telegram.BackgroundFill` with solid fill.""" + GRADIENT = "gradient" + """:obj:`str`: A :class:`telegram.BackgroundFill` with gradient fill.""" + FREEFORM_GRADIENT = "freeform_gradient" + """:obj:`str`: A :class:`telegram.BackgroundFill` with freeform_gradient fill.""" + + +class ChatSubscriptionLimit(IntEnum): + """This enum contains limitations for + :paramref:`telegram.Bot.create_chat_subscription_invite_link.subscription_period` and + :paramref:`telegram.Bot.create_chat_subscription_invite_link.subscription_price`. + The enum members of this enumeration are instances of :class:`int` and can be treated as such. + + .. versionadded:: 21.5 + """ + + __slots__ = () + + SUBSCRIPTION_PERIOD = 2592000 + """:obj:`int`: The number of seconds the subscription will be active.""" + MIN_PRICE = 1 + """:obj:`int`: Amount of stars a user pays, minimum amount the subscription can be set to.""" + MAX_PRICE = 2500 + """:obj:`int`: Amount of stars a user pays, maximum amount the subscription can be set to.""" diff --git a/error.py b/error.py new file mode 100644 index 0000000000000000000000000000000000000000..6dcc509a8d466bf42b8dfb1d457c42146974fe67 --- /dev/null +++ b/error.py @@ -0,0 +1,265 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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 that represent Telegram errors. + +.. versionchanged:: 20.0 + Replaced ``Unauthorized`` by :class:`Forbidden`. +""" + +__all__ = ( + "BadRequest", + "ChatMigrated", + "Conflict", + "EndPointNotFound", + "Forbidden", + "InvalidToken", + "NetworkError", + "PassportDecryptionError", + "RetryAfter", + "TelegramError", + "TimedOut", +) + +from typing import Optional, Tuple, Union + + +def _lstrip_str(in_s: str, lstr: str) -> str: + """ + Args: + in_s (:obj:`str`): in string + lstr (:obj:`str`): substr to strip from left side + + Returns: + :obj:`str`: The stripped string. + + """ + return in_s[len(lstr) :] if in_s.startswith(lstr) else in_s + + +class TelegramError(Exception): + """ + Base class for Telegram errors. + + Tip: + Objects of this type can be serialized via Python's :mod:`pickle` module and pickled + objects from one version of PTB are usually loadable in future versions. However, we can + not guarantee that this compatibility will always be provided. At least a manual one-time + conversion of the data may be needed on major updates of the library. + + .. seealso:: :wiki:`Exceptions, Warnings and Logging ` + """ + + __slots__ = ("message",) + + def __init__(self, message: str): + super().__init__() + + msg = _lstrip_str(message, "Error: ") + msg = _lstrip_str(msg, "[Error]: ") + msg = _lstrip_str(msg, "Bad Request: ") + if msg != message: + # api_error - capitalize the msg... + msg = msg.capitalize() + self.message: str = msg + + def __str__(self) -> str: + """Gives the string representation of exceptions message. + + Returns: + :obj:`str` + """ + return self.message + + def __repr__(self) -> str: + """Gives an unambiguous string representation of the exception. + + Returns: + :obj:`str` + """ + return f"{self.__class__.__name__}('{self.message}')" + + def __reduce__(self) -> Tuple[type, Tuple[str]]: + """Defines how to serialize the exception for pickle. + + .. seealso:: + :py:meth:`object.__reduce__`, :mod:`pickle`. + + Returns: + :obj:`tuple` + """ + return self.__class__, (self.message,) + + +class Forbidden(TelegramError): + """Raised when the bot has not enough rights to perform the requested action. + + Examples: + :any:`Raw API Bot ` + + .. versionchanged:: 20.0 + This class was previously named ``Unauthorized``. + """ + + __slots__ = () + + +class InvalidToken(TelegramError): + """Raised when the token is invalid. + + Args: + message (:obj:`str`, optional): Any additional information about the exception. + + .. versionadded:: 20.0 + """ + + __slots__ = () + + def __init__(self, message: Optional[str] = None) -> None: + super().__init__("Invalid token" if message is None else message) + + +class EndPointNotFound(TelegramError): + """Raised when the requested endpoint is not found. Only relevant for + :meth:`telegram.Bot.do_api_request`. + + .. versionadded:: 20.8 + """ + + __slots__ = () + + +class NetworkError(TelegramError): + """Base class for exceptions due to networking errors. + + Tip: + This exception (and its subclasses) usually originates from the networking backend + used by :class:`~telegram.request.HTTPXRequest`, or a custom implementation of + :class:`~telegram.request.BaseRequest`. In this case, the original exception can be + accessed via the ``__cause__`` + `attribute `_. + + Examples: + :any:`Raw API Bot ` + + .. seealso:: + :wiki:`Handling network errors ` + """ + + __slots__ = () + + +class BadRequest(NetworkError): + """Raised when Telegram could not process the request correctly.""" + + __slots__ = () + + +class TimedOut(NetworkError): + """Raised when a request took too long to finish. + + .. seealso:: + :wiki:`Handling network errors ` + + Args: + message (:obj:`str`, optional): Any additional information about the exception. + + .. versionadded:: 20.0 + """ + + __slots__ = () + + def __init__(self, message: Optional[str] = None) -> None: + super().__init__(message or "Timed out") + + +class ChatMigrated(TelegramError): + """ + Raised when the requested group chat migrated to supergroup and has a new chat id. + + .. seealso:: + :wiki:`Storing Bot, User and Chat Related Data ` + + Args: + new_chat_id (:obj:`int`): The new chat id of the group. + + Attributes: + new_chat_id (:obj:`int`): The new chat id of the group. + + """ + + __slots__ = ("new_chat_id",) + + def __init__(self, new_chat_id: int): + super().__init__(f"Group migrated to supergroup. New chat id: {new_chat_id}") + self.new_chat_id: int = new_chat_id + + def __reduce__(self) -> Tuple[type, Tuple[int]]: # type: ignore[override] + return self.__class__, (self.new_chat_id,) + + +class RetryAfter(TelegramError): + """ + Raised when flood limits where exceeded. + + .. versionchanged:: 20.0 + :attr:`retry_after` is now an integer to comply with the Bot API. + + Args: + retry_after (:obj:`int`): Time in seconds, after which the bot can retry the request. + + Attributes: + retry_after (:obj:`int`): Time in seconds, after which the bot can retry the request. + + """ + + __slots__ = ("retry_after",) + + def __init__(self, retry_after: int): + super().__init__(f"Flood control exceeded. Retry in {retry_after} seconds") + self.retry_after: int = retry_after + + def __reduce__(self) -> Tuple[type, Tuple[float]]: # type: ignore[override] + return self.__class__, (self.retry_after,) + + +class Conflict(TelegramError): + """Raised when a long poll or webhook conflicts with another one.""" + + __slots__ = () + + def __reduce__(self) -> Tuple[type, Tuple[str]]: + return self.__class__, (self.message,) + + +class PassportDecryptionError(TelegramError): + """Something went wrong with decryption. + + .. versionchanged:: 20.0 + This class was previously named ``TelegramDecryptionError`` and was available via + ``telegram.TelegramDecryptionError``. + """ + + __slots__ = ("_msg",) + + def __init__(self, message: Union[str, Exception]): + super().__init__(f"PassportDecryptionError: {message}") + self._msg = str(message) + + def __reduce__(self) -> Tuple[type, Tuple[str]]: + return self.__class__, (self._msg,) diff --git a/helpers.py b/helpers.py new file mode 100644 index 0000000000000000000000000000000000000000..d303b64a78ba6fb69cc5d613c1f73ca80eeb0feb --- /dev/null +++ b/helpers.py @@ -0,0 +1,198 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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 convenience helper functions. + +.. versionchanged:: 20.0 + Previously, the contents of this module were available through the (no longer existing) + module ``telegram.utils.helpers``. +""" + +__all__ = ( + "create_deep_linked_url", + "effective_message_type", + "escape_markdown", + "mention_html", + "mention_markdown", +) + +import re +from html import escape +from typing import TYPE_CHECKING, Optional, Union + +from telegram._utils.types import MarkdownVersion +from telegram.constants import MessageType + +if TYPE_CHECKING: + from telegram import Message, Update + + +def escape_markdown( + text: str, version: MarkdownVersion = 1, entity_type: Optional[str] = None +) -> str: + """Helper function to escape telegram markup symbols. + + .. versionchanged:: 20.3 + Custom emoji entity escaping is now supported. + + Args: + text (:obj:`str`): The text. + version (:obj:`int` | :obj:`str`): Use to specify the version of telegrams Markdown. + Either ``1`` or ``2``. Defaults to ``1``. + entity_type (:obj:`str`, optional): For the entity types + :tg-const:`telegram.MessageEntity.PRE`, :tg-const:`telegram.MessageEntity.CODE` and + the link part of :tg-const:`telegram.MessageEntity.TEXT_LINK` and + :tg-const:`telegram.MessageEntity.CUSTOM_EMOJI`, only certain characters need to be + escaped in :tg-const:`telegram.constants.ParseMode.MARKDOWN_V2`. See the `official API + documentation `_ for details. + Only valid in combination with ``version=2``, will be ignored else. + """ + if int(version) == 1: + escape_chars = r"_*`[" + elif int(version) == 2: + if entity_type in ["pre", "code"]: + escape_chars = r"\`" + elif entity_type in ["text_link", "custom_emoji"]: + escape_chars = r"\)" + else: + escape_chars = r"\_*[]()~`>#+-=|{}.!" + else: + raise ValueError("Markdown version must be either 1 or 2!") + + return re.sub(f"([{re.escape(escape_chars)}])", r"\\\1", text) + + +def mention_html(user_id: Union[int, str], name: str) -> str: + """ + Helper function to create a user mention as HTML tag. + + Args: + user_id (:obj:`int`): The user's id which you want to mention. + name (:obj:`str`): The name the mention is showing. + + Returns: + :obj:`str`: The inline mention for the user as HTML. + """ + return f'{escape(name)}' + + +def mention_markdown(user_id: Union[int, str], name: str, version: MarkdownVersion = 1) -> str: + """ + Helper function to create a user mention in Markdown syntax. + + Args: + user_id (:obj:`int`): The user's id which you want to mention. + name (:obj:`str`): The name the mention is showing. + version (:obj:`int` | :obj:`str`): Use to specify the version of Telegram's Markdown. + Either ``1`` or ``2``. Defaults to ``1``. + + Returns: + :obj:`str`: The inline mention for the user as Markdown. + """ + tg_link = f"tg://user?id={user_id}" + if version == 1: + return f"[{name}]({tg_link})" + return f"[{escape_markdown(name, version=version)}]({tg_link})" + + +def effective_message_type(entity: Union["Message", "Update"]) -> Optional[str]: + """ + Extracts the type of message as a string identifier from a :class:`telegram.Message` or a + :class:`telegram.Update`. + + Args: + entity (:class:`telegram.Update` | :class:`telegram.Message`): The ``update`` or + ``message`` to extract from. + + Returns: + :obj:`str` | :obj:`None`: One of :class:`telegram.constants.MessageType` if the entity + contains a message that matches one of those types. :obj:`None` otherwise. + + """ + # Importing on file-level yields cyclic Import Errors + from telegram import Message, Update # pylint: disable=import-outside-toplevel + + if isinstance(entity, Message): + message = entity + elif isinstance(entity, Update): + if not entity.effective_message: + return None + message = entity.effective_message + else: + raise TypeError(f"The entity is neither Message nor Update (got: {type(entity)})") + + for message_type in MessageType: + if message[message_type]: + return message_type + + return None + + +def create_deep_linked_url( + bot_username: str, payload: Optional[str] = None, group: bool = False +) -> str: + """ + Creates a deep-linked URL for this :paramref:`~create_deep_linked_url.bot_username` with the + specified :paramref:`~create_deep_linked_url.payload`. See + https://core.telegram.org/bots/features#deep-linking to learn more. + + The :paramref:`~create_deep_linked_url.payload` may consist of the following characters: + ``A-Z, a-z, 0-9, _, -`` + + Note: + Works well in conjunction with + ``CommandHandler("start", callback, filters=filters.Regex('payload'))`` + + Examples: + * ``create_deep_linked_url(bot.get_me().username, "some-params")`` + * :any:`Deep Linking ` + + Args: + bot_username (:obj:`str`): The username to link to. + payload (:obj:`str`, optional): Parameters to encode in the created URL. + group (:obj:`bool`, optional): If :obj:`True` the user is prompted to select a group to + add the bot to. If :obj:`False`, opens a one-on-one conversation with the bot. + Defaults to :obj:`False`. + + Returns: + :obj:`str`: An URL to start the bot with specific parameters. + + Raises: + :exc:`ValueError`: If the length of the :paramref:`payload` exceeds 64 characters, + contains invalid characters, or if the :paramref:`bot_username` is less than 4 + characters. + """ + if bot_username is None or len(bot_username) <= 3: + raise ValueError("You must provide a valid bot_username.") + + base_url = f"https://t.me/{bot_username}" + if not payload: + return base_url + + if len(payload) > 64: + raise ValueError("The deep-linking payload must not exceed 64 characters.") + + if not re.match(r"^[A-Za-z0-9_-]+$", payload): + raise ValueError( + "Only the following characters are allowed for deep-linked " + "URLs: A-Z, a-z, 0-9, _ and -" + ) + + key = "startgroup" if group else "start" + + return f"{base_url}?{key}={payload}" diff --git a/inlinekeyboardbutton.py b/inlinekeyboardbutton.py new file mode 100644 index 0000000000000000000000000000000000000000..cff4df66a21a5956a687a29b006bbb3949c955a5 --- /dev/null +++ b/inlinekeyboardbutton.py @@ -0,0 +1,328 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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 InlineKeyboardButton.""" + +from typing import TYPE_CHECKING, Final, Optional, Union + +from telegram import constants +from telegram._games.callbackgame import CallbackGame +from telegram._loginurl import LoginUrl +from telegram._switchinlinequerychosenchat import SwitchInlineQueryChosenChat +from telegram._telegramobject import TelegramObject +from telegram._utils.types import JSONDict +from telegram._webappinfo import WebAppInfo + +if TYPE_CHECKING: + from telegram import Bot + + +class InlineKeyboardButton(TelegramObject): + """This object represents one button of an inline keyboard. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`text`, :attr:`url`, :attr:`login_url`, :attr:`callback_data`, + :attr:`switch_inline_query`, :attr:`switch_inline_query_current_chat`, :attr:`callback_game`, + :attr:`web_app` and :attr:`pay` are equal. + + Note: + * Exactly one of the optional fields must be used to specify type of the button. + * Mind that :attr:`callback_game` is not + working as expected. Putting a game short name in it might, but is not guaranteed to + work. + * If your bot allows for arbitrary callback data, in keyboards returned in a response + from telegram, :attr:`callback_data` may be an instance of + :class:`telegram.ext.InvalidCallbackData`. This will be the case, if the data + associated with the button was already deleted. + + .. versionadded:: 13.6 + + * Since Bot API 5.5, it's now allowed to mention users by their ID in inline keyboards. + This will only work in Telegram versions released after December 7, 2021. + Older clients will display *unsupported message*. + + Warning: + * If your bot allows your arbitrary callback data, buttons whose callback data is a + non-hashable object will become unhashable. Trying to evaluate ``hash(button)`` will + result in a :class:`TypeError`. + + .. versionchanged:: 13.6 + + * After Bot API 6.1, only ``HTTPS`` links will be allowed in :paramref:`login_url`. + + Examples: + * :any:`Inline Keyboard 1 ` + * :any:`Inline Keyboard 2 ` + + .. seealso:: :class:`telegram.InlineKeyboardMarkup` + + .. versionchanged:: 20.0 + :attr:`web_app` is considered as well when comparing objects of this type in terms of + equality. + + Args: + text (:obj:`str`): Label text on the button. + url (:obj:`str`, optional): HTTP or tg:// url to be opened when the button is pressed. + Links ``tg://user?id=`` can be used to mention a user by + their ID without using a username, if this is allowed by their privacy settings. + + .. versionchanged:: 13.9 + You can now mention a user using ``tg://user?id=``. + login_url (:class:`telegram.LoginUrl`, optional): An ``HTTPS`` URL used to automatically + authorize the user. Can be used as a replacement for the Telegram Login Widget. + + Caution: + Only ``HTTPS`` links are allowed after Bot API 6.1. + callback_data (:obj:`str` | :obj:`object`, optional): Data to be sent in a callback query + 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. + If the bot instance allows arbitrary callback data, anything can be passed. + + Tip: + The value entered here will be available in :attr:`telegram.CallbackQuery.data`. + + .. seealso:: :wiki:`Arbitrary callback_data ` + + 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 + private chats between a user and the bot. Not supported for messages sent on behalf of + a Telegram Business account. + + .. versionadded:: 20.0 + 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 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 + 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. + callback_game (:class:`telegram.CallbackGame`, optional): Description of the game that will + be launched when the user presses the button + + Note: + This type of button **must** always be the first button in the first row. + pay (:obj:`bool`, optional): Specify :obj:`True`, to send a Pay button. + Substrings ``“⭐️”`` and ``“XTR”`` in the buttons's text will be replaced with a + Telegram Star icon. + + 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 (: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 + Business account. + + .. versionadded:: 20.3 + + Tip: + This is similar to :paramref:`switch_inline_query`, but gives more control on + which chats can be selected. + + Caution: + The PTB team has discovered that this field works correctly only if your Telegram + client is released after April 20th 2023. + + Attributes: + text (:obj:`str`): Label text on the button. + url (:obj:`str`): Optional. HTTP or tg:// url to be opened when the button is pressed. + Links ``tg://user?id=`` can be used to mention a user by + their ID without using a username, if this is allowed by their privacy settings. + + .. versionchanged:: 13.9 + You can now mention a user using ``tg://user?id=``. + login_url (:class:`telegram.LoginUrl`): Optional. An ``HTTPS`` URL used to automatically + authorize the user. Can be used as a replacement for the Telegram Login Widget. + + Caution: + Only ``HTTPS`` links are allowed after Bot API 6.1. + callback_data (:obj:`str` | :obj:`object`): Optional. Data to be sent in a callback query + 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 (: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 + private chats between a user and the bot. Not supported for messages sent on behalf of + a Telegram Business account. + + .. versionadded:: 20.0 + 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 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 + 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. + callback_game (:class:`telegram.CallbackGame`): Optional. Description of the game that will + be launched when the user presses the button. + + Note: + This type of button **must** always be the first button in the first row. + pay (:obj:`bool`): Optional. Specify :obj:`True`, to send a Pay button. + Substrings ``“⭐️”`` and ``“XTR”`` in the buttons's text will be replaced with a + Telegram Star icon. + + 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 (: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 + Business account. + + .. versionadded:: 20.3 + + Tip: + This is similar to :attr:`switch_inline_query`, but gives more control on + which chats can be selected. + + Caution: + The PTB team has discovered that this field works correctly only if your Telegram + client is released after April 20th 2023. + """ + + __slots__ = ( + "callback_data", + "callback_game", + "login_url", + "pay", + "switch_inline_query", + "switch_inline_query_chosen_chat", + "switch_inline_query_current_chat", + "text", + "url", + "web_app", + ) + + def __init__( + self, + text: str, + url: Optional[str] = None, + callback_data: Optional[Union[str, object]] = None, + switch_inline_query: Optional[str] = None, + switch_inline_query_current_chat: Optional[str] = None, + callback_game: Optional[CallbackGame] = None, + pay: Optional[bool] = None, + login_url: Optional[LoginUrl] = None, + web_app: Optional[WebAppInfo] = None, + switch_inline_query_chosen_chat: Optional[SwitchInlineQueryChosenChat] = None, + *, + api_kwargs: Optional[JSONDict] = None, + ): + super().__init__(api_kwargs=api_kwargs) + # Required + self.text: str = text + + # Optionals + self.url: Optional[str] = url + self.login_url: Optional[LoginUrl] = login_url + self.callback_data: Optional[Union[str, object]] = callback_data + self.switch_inline_query: Optional[str] = switch_inline_query + self.switch_inline_query_current_chat: Optional[str] = switch_inline_query_current_chat + self.callback_game: Optional[CallbackGame] = callback_game + self.pay: Optional[bool] = pay + self.web_app: Optional[WebAppInfo] = web_app + self.switch_inline_query_chosen_chat: Optional[SwitchInlineQueryChosenChat] = ( + switch_inline_query_chosen_chat + ) + self._id_attrs = () + self._set_id_attrs() + + self._freeze() + + def _set_id_attrs(self) -> None: + self._id_attrs = ( + self.text, + self.url, + self.login_url, + self.callback_data, + self.web_app, + self.switch_inline_query, + self.switch_inline_query_current_chat, + self.callback_game, + self.pay, + ) + + @classmethod + def de_json( + cls, data: Optional[JSONDict], bot: Optional["Bot"] = None + ) -> Optional["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 + ) + + return super().de_json(data=data, bot=bot) + + def update_callback_data(self, callback_data: Union[str, object]) -> None: + """ + Sets :attr:`callback_data` to the passed object. Intended to be used by + :class:`telegram.ext.CallbackDataCache`. + + .. versionadded:: 13.6 + + Args: + callback_data (:class:`object`): The new callback data. + """ + with self._unfrozen(): + self.callback_data = callback_data + self._set_id_attrs() + + MIN_CALLBACK_DATA: Final[int] = constants.InlineKeyboardButtonLimit.MIN_CALLBACK_DATA + """:const:`telegram.constants.InlineKeyboardButtonLimit.MIN_CALLBACK_DATA` + + .. versionadded:: 20.0 + """ + MAX_CALLBACK_DATA: Final[int] = constants.InlineKeyboardButtonLimit.MAX_CALLBACK_DATA + """:const:`telegram.constants.InlineKeyboardButtonLimit.MAX_CALLBACK_DATA` + + .. versionadded:: 20.0 + """ diff --git a/inlinekeyboardmarkup.py b/inlinekeyboardmarkup.py new file mode 100644 index 0000000000000000000000000000000000000000..6857e4d8e3a85614e97ab4460c3bf9b5911a41ec --- /dev/null +++ b/inlinekeyboardmarkup.py @@ -0,0 +1,164 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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 InlineKeyboardMarkup.""" +from typing import TYPE_CHECKING, Optional, Sequence, Tuple + +from telegram._inline.inlinekeyboardbutton import InlineKeyboardButton +from telegram._telegramobject import TelegramObject +from telegram._utils.markup import check_keyboard_type +from telegram._utils.types import JSONDict + +if TYPE_CHECKING: + from telegram import Bot + + +class InlineKeyboardMarkup(TelegramObject): + """ + This object represents an inline keyboard that appears right next to the message it belongs to. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their size of :attr:`inline_keyboard` and all the buttons are equal. + + .. figure:: https://core.telegram.org/file/464001863/110f3/I47qTXAD9Z4.120010/e0\ + ea04f66357b640ec + :align: center + + An inline keyboard on a message + + .. seealso:: + Another kind of keyboard would be the :class:`telegram.ReplyKeyboardMarkup`. + + Examples: + * :any:`Inline Keyboard 1 ` + * :any:`Inline Keyboard 2 ` + + Args: + inline_keyboard (Sequence[Sequence[:class:`telegram.InlineKeyboardButton`]]): Sequence of + button rows, each represented by a sequence of :class:`~telegram.InlineKeyboardButton` + objects. + + .. versionchanged:: 20.0 + |sequenceclassargs| + + Attributes: + inline_keyboard (Tuple[Tuple[:class:`telegram.InlineKeyboardButton`]]): Tuple of + button rows, each represented by a tuple of :class:`~telegram.InlineKeyboardButton` + objects. + + .. versionchanged:: 20.0 + |tupleclassattrs| + + """ + + __slots__ = ("inline_keyboard",) + + def __init__( + self, + inline_keyboard: Sequence[Sequence[InlineKeyboardButton]], + *, + api_kwargs: Optional[JSONDict] = None, + ): + super().__init__(api_kwargs=api_kwargs) + if not check_keyboard_type(inline_keyboard): + raise ValueError( + "The parameter `inline_keyboard` should be a sequence of sequences of " + "InlineKeyboardButtons" + ) + # Required + self.inline_keyboard: Tuple[Tuple[InlineKeyboardButton, ...], ...] = tuple( + tuple(row) for row in inline_keyboard + ) + + self._id_attrs = (self.inline_keyboard,) + + self._freeze() + + @classmethod + def de_json( + cls, data: Optional[JSONDict], bot: Optional["Bot"] = None + ) -> Optional["InlineKeyboardMarkup"]: + """See :meth:`telegram.TelegramObject.de_json`.""" + if not data: + return None + + keyboard = [] + for row in data["inline_keyboard"]: + tmp = [] + for col in row: + btn = InlineKeyboardButton.de_json(col, bot) + if btn: + tmp.append(btn) + keyboard.append(tmp) + + return cls(keyboard) + + @classmethod + def from_button(cls, button: InlineKeyboardButton, **kwargs: object) -> "InlineKeyboardMarkup": + """Shortcut for:: + + InlineKeyboardMarkup([[button]], **kwargs) + + Return an InlineKeyboardMarkup from a single InlineKeyboardButton + + Args: + button (:class:`telegram.InlineKeyboardButton`): The button to use in the markup + + """ + return cls([[button]], **kwargs) # type: ignore[arg-type] + + @classmethod + def from_row( + cls, button_row: Sequence[InlineKeyboardButton], **kwargs: object + ) -> "InlineKeyboardMarkup": + """Shortcut for:: + + InlineKeyboardMarkup([button_row], **kwargs) + + Return an InlineKeyboardMarkup from a single row of InlineKeyboardButtons + + Args: + button_row (Sequence[:class:`telegram.InlineKeyboardButton`]): The button to use + in the markup + + .. versionchanged:: 20.0 + |sequenceargs| + + """ + return cls([button_row], **kwargs) # type: ignore[arg-type] + + @classmethod + def from_column( + cls, button_column: Sequence[InlineKeyboardButton], **kwargs: object + ) -> "InlineKeyboardMarkup": + """Shortcut for:: + + InlineKeyboardMarkup([[button] for button in button_column], **kwargs) + + Return an InlineKeyboardMarkup from a single column of InlineKeyboardButtons + + Args: + button_column (Sequence[:class:`telegram.InlineKeyboardButton`]): The button to use + in the markup + + .. versionchanged:: 20.0 + |sequenceargs| + + """ + button_grid = [[button] for button in button_column] + return cls(button_grid, **kwargs) # type: ignore[arg-type] diff --git a/inlinequery.py b/inlinequery.py new file mode 100644 index 0000000000000000000000000000000000000000..ba29a8646fe889a8228728c0f18aef10dcbdd36a --- /dev/null +++ b/inlinequery.py @@ -0,0 +1,227 @@ +#!/usr/bin/env python +# pylint: disable=too-many-arguments +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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 InlineQuery.""" + +from typing import TYPE_CHECKING, Callable, Final, Optional, Sequence, 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.defaultvalue import DEFAULT_NONE +from telegram._utils.types import JSONDict, ODVInput + +if TYPE_CHECKING: + from telegram import Bot, InlineQueryResult + + +class InlineQuery(TelegramObject): + """ + This object represents an incoming inline query. When the user sends an empty query, your bot + could return some default or trending results. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`id` is equal. + + .. figure:: https://core.telegram.org/file/464001466/10e4a/r4FKyQ7gw5g.134366/f2\ + 606a53d683374703 + :align: center + + Inline queries on Telegram + + .. seealso:: + The :class:`telegram.InlineQueryResult` classes represent the media the user can choose + from (see above figure). + + Note: + In Python :keyword:`from` is a reserved word. Use :paramref:`from_user` instead. + + .. versionchanged:: 20.0 + The following are now keyword-only arguments in Bot methods: + ``{read, write, connect, pool}_timeout``, :paramref:`answer.api_kwargs`, + ``auto_pagination``. Use a named argument for those, + and notice that some positional arguments changed position as a result. + + Args: + id (:obj:`str`): Unique identifier for this query. + from_user (:class:`telegram.User`): Sender. + query (:obj:`str`): Text of the query (up to + :tg-const:`telegram.InlineQuery.MAX_QUERY_LENGTH` characters). + offset (:obj:`str`): Offset of the results to be returned, can be controlled by the bot. + chat_type (:obj:`str`, optional): Type of the chat, from which the inline query was sent. + Can be either :tg-const:`telegram.Chat.SENDER` for a private chat with the inline query + sender, :tg-const:`telegram.Chat.PRIVATE`, :tg-const:`telegram.Chat.GROUP`, + :tg-const:`telegram.Chat.SUPERGROUP` or :tg-const:`telegram.Chat.CHANNEL`. The chat + type should be always known for requests sent from official clients and most + third-party clients, unless the request was sent from a secret chat. + + .. versionadded:: 13.5 + location (:class:`telegram.Location`, optional): Sender location, only for bots that + request user location. + + Attributes: + id (:obj:`str`): Unique identifier for this query. + from_user (:class:`telegram.User`): Sender. + query (:obj:`str`): Text of the query (up to + :tg-const:`telegram.InlineQuery.MAX_QUERY_LENGTH` characters). + offset (:obj:`str`): Offset of the results to be returned, can be controlled by the bot. + chat_type (:obj:`str`): Optional. Type of the chat, from which the inline query was sent. + Can be either :tg-const:`telegram.Chat.SENDER` for a private chat with the inline query + sender, :tg-const:`telegram.Chat.PRIVATE`, :tg-const:`telegram.Chat.GROUP`, + :tg-const:`telegram.Chat.SUPERGROUP` or :tg-const:`telegram.Chat.CHANNEL`. The chat + type should be always known for requests sent from official clients and most + third-party clients, unless the request was sent from a secret chat. + + .. versionadded:: 13.5 + location (:class:`telegram.Location`): Optional. Sender location, only for bots that + request user location. + + """ + + __slots__ = ("chat_type", "from_user", "id", "location", "offset", "query") + + def __init__( + self, + id: str, # pylint: disable=redefined-builtin + from_user: User, + query: str, + offset: str, + location: Optional[Location] = None, + chat_type: Optional[str] = None, + *, + api_kwargs: Optional[JSONDict] = None, + ): + super().__init__(api_kwargs=api_kwargs) + # Required + self.id: str = id + self.from_user: User = from_user + self.query: str = query + self.offset: str = offset + + # Optional + self.location: Optional[Location] = location + self.chat_type: Optional[str] = chat_type + + self._id_attrs = (self.id,) + + self._freeze() + + @classmethod + def de_json( + cls, data: Optional[JSONDict], bot: Optional["Bot"] = None + ) -> Optional["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) + + return super().de_json(data=data, bot=bot) + + async def answer( + self, + results: Union[ + Sequence["InlineQueryResult"], Callable[[int], Optional[Sequence["InlineQueryResult"]]] + ], + cache_time: Optional[int] = None, + is_personal: Optional[bool] = None, + next_offset: Optional[str] = None, + button: Optional[InlineQueryResultsButton] = None, + *, + current_offset: Optional[str] = None, + auto_pagination: bool = False, + 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.answer_inline_query( + update.inline_query.id, + *args, + current_offset=self.offset if auto_pagination else None, + **kwargs + ) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.answer_inline_query`. + + .. versionchanged:: 20.0 + Raises :class:`ValueError` instead of :class:`TypeError`. + + Keyword Args: + auto_pagination (:obj:`bool`, optional): If set to :obj:`True`, :attr:`offset` will be + passed as + :paramref:`current_offset ` to + :meth:`telegram.Bot.answer_inline_query`. + Defaults to :obj:`False`. + + Raises: + ValueError: If both :paramref:`~telegram.Bot.answer_inline_query.current_offset` and + :paramref:`auto_pagination` are supplied. + """ + if current_offset and auto_pagination: + raise ValueError("current_offset and auto_pagination are mutually exclusive!") + return await self.get_bot().answer_inline_query( + inline_query_id=self.id, + current_offset=self.offset if auto_pagination else current_offset, + results=results, + cache_time=cache_time, + is_personal=is_personal, + next_offset=next_offset, + button=button, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + MAX_RESULTS: Final[int] = constants.InlineQueryLimit.RESULTS + """:const:`telegram.constants.InlineQueryLimit.RESULTS` + + .. 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` + + .. versionadded:: 20.0 + """ + MAX_QUERY_LENGTH: Final[int] = constants.InlineQueryLimit.MAX_QUERY_LENGTH + """:const:`telegram.constants.InlineQueryLimit.MAX_QUERY_LENGTH` + + .. versionadded:: 20.0 + """ diff --git a/inlinequeryresult.py b/inlinequeryresult.py new file mode 100644 index 0000000000000000000000000000000000000000..534d255c30578653ca8a507f34c60aa61cd2e835 --- /dev/null +++ b/inlinequeryresult.py @@ -0,0 +1,79 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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/]. +# pylint: disable=redefined-builtin +"""This module contains the classes that represent Telegram InlineQueryResult.""" + +from typing import Final, Optional + +from telegram import constants +from telegram._telegramobject import TelegramObject +from telegram._utils import enum +from telegram._utils.types import JSONDict + + +class InlineQueryResult(TelegramObject): + """Baseclass for the InlineQueryResult* classes. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`id` is equal. + + Note: + All URLs passed in inline query results will be available to end users and therefore must + be assumed to be *public*. + + Examples: + :any:`Inline Bot ` + + Args: + type (:obj:`str`): Type of the result. + id (:obj:`str`): Unique identifier for this result, + :tg-const:`telegram.InlineQueryResult.MIN_ID_LENGTH`- + :tg-const:`telegram.InlineQueryResult.MAX_ID_LENGTH` Bytes. + + Attributes: + type (:obj:`str`): Type of the result. + id (:obj:`str`): Unique identifier for this result, + :tg-const:`telegram.InlineQueryResult.MIN_ID_LENGTH`- + :tg-const:`telegram.InlineQueryResult.MAX_ID_LENGTH` Bytes. + + """ + + __slots__ = ("id", "type") + + def __init__(self, type: str, id: str, *, api_kwargs: Optional[JSONDict] = None): + super().__init__(api_kwargs=api_kwargs) + + # Required + self.type: str = enum.get_member(constants.InlineQueryResultType, type, type) + self.id: str = str(id) + + self._id_attrs = (self.id,) + + self._freeze() + + MIN_ID_LENGTH: Final[int] = constants.InlineQueryResultLimit.MIN_ID_LENGTH + """:const:`telegram.constants.InlineQueryResultLimit.MIN_ID_LENGTH` + + .. versionadded:: 20.0 + """ + MAX_ID_LENGTH: Final[int] = constants.InlineQueryResultLimit.MAX_ID_LENGTH + """:const:`telegram.constants.InlineQueryResultLimit.MAX_ID_LENGTH` + + .. versionadded:: 20.0 + """ diff --git a/inlinequeryresultarticle.py b/inlinequeryresultarticle.py new file mode 100644 index 0000000000000000000000000000000000000000..92c358e77efa99975a3ebbde2e419865c39d1d0d --- /dev/null +++ b/inlinequeryresultarticle.py @@ -0,0 +1,130 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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 the classes that represent Telegram InlineQueryResultArticle.""" + +from typing import TYPE_CHECKING, Optional + +from telegram._inline.inlinekeyboardmarkup import InlineKeyboardMarkup +from telegram._inline.inlinequeryresult import InlineQueryResult +from telegram._utils.types import JSONDict +from telegram.constants import InlineQueryResultType + +if TYPE_CHECKING: + from telegram import InputMessageContent + + +class InlineQueryResultArticle(InlineQueryResult): + """This object represents a Telegram InlineQueryResultArticle. + + Examples: + :any:`Inline Bot ` + + .. versionchanged:: 20.5 + Removed the deprecated arguments and attributes ``thumb_*``. + + Args: + id (:obj:`str`): Unique identifier for this result, + :tg-const:`telegram.InlineQueryResult.MIN_ID_LENGTH`- + :tg-const:`telegram.InlineQueryResult.MAX_ID_LENGTH` Bytes. + title (:obj:`str`): Title of the result. + input_message_content (:class:`telegram.InputMessageContent`): Content of the message to + be sent. + 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. + + .. versionadded:: 20.2 + thumbnail_width (:obj:`int`, optional): Thumbnail width. + + .. versionadded:: 20.2 + thumbnail_height (:obj:`int`, optional): Thumbnail height. + + .. versionadded:: 20.2 + + Attributes: + type (:obj:`str`): :tg-const:`telegram.constants.InlineQueryResultType.ARTICLE`. + id (:obj:`str`): Unique identifier for this result, + :tg-const:`telegram.InlineQueryResult.MIN_ID_LENGTH`- + :tg-const:`telegram.InlineQueryResult.MAX_ID_LENGTH` Bytes. + title (:obj:`str`): Title of the result. + input_message_content (:class:`telegram.InputMessageContent`): Content of the message to + be sent. + 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. + + .. versionadded:: 20.2 + thumbnail_width (:obj:`int`): Optional. Thumbnail width. + + .. versionadded:: 20.2 + thumbnail_height (:obj:`int`): Optional. Thumbnail height. + + .. versionadded:: 20.2 + + """ + + __slots__ = ( + "description", + "hide_url", + "input_message_content", + "reply_markup", + "thumbnail_height", + "thumbnail_url", + "thumbnail_width", + "title", + "url", + ) + + def __init__( + self, + id: str, # pylint: disable=redefined-builtin + title: str, + 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, + thumbnail_height: Optional[int] = None, + *, + api_kwargs: Optional[JSONDict] = None, + ): + # Required + super().__init__(InlineQueryResultType.ARTICLE, id, api_kwargs=api_kwargs) + with self._unfrozen(): + self.title: str = title + self.input_message_content: InputMessageContent = input_message_content + + # 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 + self.thumbnail_height: Optional[int] = thumbnail_height diff --git a/inlinequeryresultaudio.py b/inlinequeryresultaudio.py new file mode 100644 index 0000000000000000000000000000000000000000..69353967adc0fc8b018a5bc8d19bbfef0a5e814b --- /dev/null +++ b/inlinequeryresultaudio.py @@ -0,0 +1,129 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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 the classes that represent Telegram InlineQueryResultAudio.""" +from typing import TYPE_CHECKING, Optional, Sequence, Tuple + +from telegram._inline.inlinekeyboardmarkup import InlineKeyboardMarkup +from telegram._inline.inlinequeryresult import InlineQueryResult +from telegram._messageentity import MessageEntity +from telegram._utils.argumentparsing import parse_sequence_arg +from telegram._utils.defaultvalue import DEFAULT_NONE +from telegram._utils.types import JSONDict, ODVInput +from telegram.constants import InlineQueryResultType + +if TYPE_CHECKING: + from telegram import InputMessageContent + + +class InlineQueryResultAudio(InlineQueryResult): + """ + Represents a link to an mp3 audio file. By default, this audio file will be sent by the user. + Alternatively, you can use :attr:`input_message_content` to send a message with the specified + content instead of the audio. + + .. seealso:: :wiki:`Working with Files and Media ` + + Args: + id (:obj:`str`): Unique identifier for this result, + :tg-const:`telegram.InlineQueryResult.MIN_ID_LENGTH`- + :tg-const:`telegram.InlineQueryResult.MAX_ID_LENGTH` Bytes. + audio_url (:obj:`str`): A valid URL for the audio file. + title (:obj:`str`): Title. + performer (:obj:`str`, optional): Performer. + audio_duration (:obj:`str`, optional): Audio duration in seconds. + caption (:obj:`str`, optional): Caption, + 0-:tg-const:`telegram.constants.MessageLimit.CAPTION_LENGTH` characters after entities + parsing. + parse_mode (:obj:`str`, optional): |parse_mode| + caption_entities (Sequence[:class:`telegram.MessageEntity`], optional): |caption_entities| + + .. versionchanged:: 20.0 + |sequenceclassargs| + reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): Inline keyboard attached + to the message. + input_message_content (:class:`telegram.InputMessageContent`, optional): Content of the + message to be sent instead of the audio. + + Attributes: + type (:obj:`str`): :tg-const:`telegram.constants.InlineQueryResultType.AUDIO`. + id (:obj:`str`): Unique identifier for this result, + :tg-const:`telegram.InlineQueryResult.MIN_ID_LENGTH`- + :tg-const:`telegram.InlineQueryResult.MAX_ID_LENGTH` Bytes. + audio_url (:obj:`str`): A valid URL for the audio file. + title (:obj:`str`): Title. + performer (:obj:`str`): Optional. Performer. + audio_duration (:obj:`str`): Optional. Audio duration in seconds. + caption (:obj:`str`): Optional. Caption, + 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| + + .. versionchanged:: 20.0 + + * |tupleclassattrs| + * |alwaystuple| + reply_markup (:class:`telegram.InlineKeyboardMarkup`): Optional. Inline keyboard attached + to the message. + input_message_content (:class:`telegram.InputMessageContent`): Optional. Content of the + message to be sent instead of the audio. + + """ + + __slots__ = ( + "audio_duration", + "audio_url", + "caption", + "caption_entities", + "input_message_content", + "parse_mode", + "performer", + "reply_markup", + "title", + ) + + def __init__( + self, + id: str, # pylint: disable=redefined-builtin + audio_url: str, + title: str, + performer: Optional[str] = None, + audio_duration: Optional[int] = None, + caption: Optional[str] = None, + reply_markup: Optional[InlineKeyboardMarkup] = None, + input_message_content: Optional["InputMessageContent"] = None, + parse_mode: ODVInput[str] = DEFAULT_NONE, + caption_entities: Optional[Sequence[MessageEntity]] = None, + *, + api_kwargs: Optional[JSONDict] = None, + ): + # Required + super().__init__(InlineQueryResultType.AUDIO, id, api_kwargs=api_kwargs) + with self._unfrozen(): + self.audio_url: str = audio_url + self.title: str = title + + # Optionals + self.performer: Optional[str] = performer + 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.reply_markup: Optional[InlineKeyboardMarkup] = reply_markup + self.input_message_content: Optional[InputMessageContent] = input_message_content diff --git a/inlinequeryresultcachedaudio.py b/inlinequeryresultcachedaudio.py new file mode 100644 index 0000000000000000000000000000000000000000..2fb7cdbb54d78bc8fb5cb2122e64d8a4b6096b87 --- /dev/null +++ b/inlinequeryresultcachedaudio.py @@ -0,0 +1,115 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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 the classes that represent Telegram InlineQueryResultCachedAudio.""" +from typing import TYPE_CHECKING, Optional, Sequence, Tuple + +from telegram._inline.inlinekeyboardmarkup import InlineKeyboardMarkup +from telegram._inline.inlinequeryresult import InlineQueryResult +from telegram._messageentity import MessageEntity +from telegram._utils.argumentparsing import parse_sequence_arg +from telegram._utils.defaultvalue import DEFAULT_NONE +from telegram._utils.types import JSONDict, ODVInput +from telegram.constants import InlineQueryResultType + +if TYPE_CHECKING: + from telegram import InputMessageContent + + +class InlineQueryResultCachedAudio(InlineQueryResult): + """ + Represents a link to an mp3 audio file stored on the Telegram servers. By default, this audio + file will be sent by the user. Alternatively, you can use :attr:`input_message_content` to + send a message with the specified content instead of the audio. + + .. seealso:: :wiki:`Working with Files and Media ` + + Args: + id (:obj:`str`): Unique identifier for this result, + :tg-const:`telegram.InlineQueryResult.MIN_ID_LENGTH`- + :tg-const:`telegram.InlineQueryResult.MAX_ID_LENGTH` Bytes. + audio_file_id (:obj:`str`): A valid file identifier for the audio file. + caption (:obj:`str`, optional): Caption, + 0-:tg-const:`telegram.constants.MessageLimit.CAPTION_LENGTH` characters after entities + parsing. + parse_mode (:obj:`str`, optional): |parse_mode| + caption_entities (Sequence[:class:`telegram.MessageEntity`], optional): |caption_entities| + + .. versionchanged:: 20.0 + |sequenceclassargs| + + reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): Inline keyboard attached + to the message. + input_message_content (:class:`telegram.InputMessageContent`, optional): Content of the + message to be sent instead of the audio. + + Attributes: + type (:obj:`str`): :tg-const:`telegram.constants.InlineQueryResultType.AUDIO`. + id (:obj:`str`): Unique identifier for this result, + :tg-const:`telegram.InlineQueryResult.MIN_ID_LENGTH`- + :tg-const:`telegram.InlineQueryResult.MAX_ID_LENGTH` Bytes. + audio_file_id (:obj:`str`): A valid file identifier for the audio file. + caption (:obj:`str`): Optional. Caption, + 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| + + .. versionchanged:: 20.0 + + * |tupleclassattrs| + * |alwaystuple| + reply_markup (:class:`telegram.InlineKeyboardMarkup`): Optional. Inline keyboard attached + to the message. + input_message_content (:class:`telegram.InputMessageContent`): Optional. Content of the + message to be sent instead of the audio. + + """ + + __slots__ = ( + "audio_file_id", + "caption", + "caption_entities", + "input_message_content", + "parse_mode", + "reply_markup", + ) + + def __init__( + self, + id: str, # pylint: disable=redefined-builtin + audio_file_id: str, + caption: Optional[str] = None, + reply_markup: Optional[InlineKeyboardMarkup] = None, + input_message_content: Optional["InputMessageContent"] = None, + parse_mode: ODVInput[str] = DEFAULT_NONE, + caption_entities: Optional[Sequence[MessageEntity]] = None, + *, + api_kwargs: Optional[JSONDict] = None, + ): + # Required + super().__init__(InlineQueryResultType.AUDIO, id, api_kwargs=api_kwargs) + with self._unfrozen(): + self.audio_file_id: str = audio_file_id + + # Optionals + self.caption: Optional[str] = caption + self.parse_mode: ODVInput[str] = parse_mode + 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/inlinequeryresultcacheddocument.py b/inlinequeryresultcacheddocument.py new file mode 100644 index 0000000000000000000000000000000000000000..b5416c2748c4fdfa22dfb94240b084964f1e802b --- /dev/null +++ b/inlinequeryresultcacheddocument.py @@ -0,0 +1,125 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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 the classes that represent Telegram InlineQueryResultCachedDocument.""" +from typing import TYPE_CHECKING, Optional, Sequence, Tuple + +from telegram._inline.inlinekeyboardmarkup import InlineKeyboardMarkup +from telegram._inline.inlinequeryresult import InlineQueryResult +from telegram._messageentity import MessageEntity +from telegram._utils.argumentparsing import parse_sequence_arg +from telegram._utils.defaultvalue import DEFAULT_NONE +from telegram._utils.types import JSONDict, ODVInput +from telegram.constants import InlineQueryResultType + +if TYPE_CHECKING: + from telegram import InputMessageContent + + +class InlineQueryResultCachedDocument(InlineQueryResult): + """ + Represents a link to a file stored on the Telegram servers. By default, this file will be sent + by the user with an optional caption. Alternatively, you can use :attr:`input_message_content` + to send a message with the specified content instead of the file. + + .. seealso:: :wiki:`Working with Files and Media ` + + Args: + id (:obj:`str`): Unique identifier for this result, + :tg-const:`telegram.InlineQueryResult.MIN_ID_LENGTH`- + :tg-const:`telegram.InlineQueryResult.MAX_ID_LENGTH` Bytes. + title (:obj:`str`): Title for the result. + document_file_id (:obj:`str`): A valid file identifier for the file. + description (:obj:`str`, optional): Short description of the result. + caption (:obj:`str`, optional): Caption of the document to be sent, + 0-:tg-const:`telegram.constants.MessageLimit.CAPTION_LENGTH` characters + after entities parsing. + parse_mode (:obj:`str`, optional): |parse_mode| + caption_entities (Sequence[:class:`telegram.MessageEntity`], optional): |caption_entities| + + .. versionchanged:: 20.0 + |sequenceclassargs| + + reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): Inline keyboard attached + to the message. + input_message_content (:class:`telegram.InputMessageContent`, optional): Content of the + message to be sent instead of the file. + + Attributes: + type (:obj:`str`): :tg-const:`telegram.constants.InlineQueryResultType.DOCUMENT`. + id (:obj:`str`): Unique identifier for this result, + :tg-const:`telegram.InlineQueryResult.MIN_ID_LENGTH`- + :tg-const:`telegram.InlineQueryResult.MAX_ID_LENGTH` Bytes. + title (:obj:`str`): Title for the result. + document_file_id (:obj:`str`): A valid file identifier for the file. + description (:obj:`str`): Optional. Short description of the result. + caption (:obj:`str`): Optional. Caption of the document to be sent, + 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| + + .. versionchanged:: 20.0 + + * |tupleclassattrs| + * |alwaystuple| + reply_markup (:class:`telegram.InlineKeyboardMarkup`): Optional. Inline keyboard attached + to the message. + input_message_content (:class:`telegram.InputMessageContent`): Optional. Content of the + message to be sent instead of the file. + + """ + + __slots__ = ( + "caption", + "caption_entities", + "description", + "document_file_id", + "input_message_content", + "parse_mode", + "reply_markup", + "title", + ) + + def __init__( + self, + id: str, # pylint: disable=redefined-builtin + title: str, + document_file_id: str, + description: Optional[str] = None, + caption: Optional[str] = None, + reply_markup: Optional[InlineKeyboardMarkup] = None, + input_message_content: Optional["InputMessageContent"] = None, + parse_mode: ODVInput[str] = DEFAULT_NONE, + caption_entities: Optional[Sequence[MessageEntity]] = None, + *, + api_kwargs: Optional[JSONDict] = None, + ): + # Required + super().__init__(InlineQueryResultType.DOCUMENT, id, api_kwargs=api_kwargs) + with self._unfrozen(): + self.title: str = title + self.document_file_id: str = document_file_id + + # Optionals + 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.reply_markup: Optional[InlineKeyboardMarkup] = reply_markup + self.input_message_content: Optional[InputMessageContent] = input_message_content diff --git a/inlinequeryresultcachedgif.py b/inlinequeryresultcachedgif.py new file mode 100644 index 0000000000000000000000000000000000000000..9f52347a05c1c1a515b896bc611f899d58581895 --- /dev/null +++ b/inlinequeryresultcachedgif.py @@ -0,0 +1,130 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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 the classes that represent Telegram InlineQueryResultCachedGif.""" +from typing import TYPE_CHECKING, Optional, Sequence, Tuple + +from telegram._inline.inlinekeyboardmarkup import InlineKeyboardMarkup +from telegram._inline.inlinequeryresult import InlineQueryResult +from telegram._messageentity import MessageEntity +from telegram._utils.argumentparsing import parse_sequence_arg +from telegram._utils.defaultvalue import DEFAULT_NONE +from telegram._utils.types import JSONDict, ODVInput +from telegram.constants import InlineQueryResultType + +if TYPE_CHECKING: + from telegram import InputMessageContent + + +class InlineQueryResultCachedGif(InlineQueryResult): + """ + Represents a link to an animated GIF file stored on the Telegram servers. By default, this + animated GIF file will be sent by the user with an optional caption. Alternatively, you can + use :attr:`input_message_content` to send a message with specified content instead of + the animation. + + .. seealso:: :wiki:`Working with Files and Media ` + + Args: + id (:obj:`str`): Unique identifier for this result, + :tg-const:`telegram.InlineQueryResult.MIN_ID_LENGTH`- + :tg-const:`telegram.InlineQueryResult.MAX_ID_LENGTH` Bytes. + gif_file_id (:obj:`str`): A valid file identifier for the GIF file. + title (:obj:`str`, optional): Title for the result. + caption (:obj:`str`, optional): Caption of the GIF file to be sent, + 0-:tg-const:`telegram.constants.MessageLimit.CAPTION_LENGTH` characters + after entities parsing. + parse_mode (:obj:`str`, optional): |parse_mode| + caption_entities (Sequence[:class:`telegram.MessageEntity`], optional): |caption_entities| + + .. versionchanged:: 20.0 + |sequenceclassargs| + + reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): Inline keyboard attached + to the message. + input_message_content (:class:`telegram.InputMessageContent`, optional): Content of the + message to be sent instead of the gif. + show_caption_above_media (:obj:`bool`, optional): Pass |show_cap_above_med| + + .. versionadded:: 21.3 + + 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_file_id (:obj:`str`): A valid file identifier for the GIF file. + title (:obj:`str`): Optional. Title for the result. + caption (:obj:`str`): Optional. Caption of the GIF file to be sent, + 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| + + .. versionchanged:: 20.0 + + * |tupleclassattrs| + * |alwaystuple| + reply_markup (:class:`telegram.InlineKeyboardMarkup`): Optional. Inline keyboard attached + to the message. + input_message_content (:class:`telegram.InputMessageContent`): Optional. Content of the + message to be sent instead of the gif. + show_caption_above_media (:obj:`bool`): Optional. |show_cap_above_med| + + .. versionadded:: 21.3 + + """ + + __slots__ = ( + "caption", + "caption_entities", + "gif_file_id", + "input_message_content", + "parse_mode", + "reply_markup", + "show_caption_above_media", + "title", + ) + + def __init__( + self, + id: str, # pylint: disable=redefined-builtin + gif_file_id: str, + title: Optional[str] = None, + caption: Optional[str] = None, + reply_markup: Optional[InlineKeyboardMarkup] = None, + input_message_content: Optional["InputMessageContent"] = None, + parse_mode: ODVInput[str] = DEFAULT_NONE, + caption_entities: Optional[Sequence[MessageEntity]] = None, + show_caption_above_media: Optional[bool] = None, + *, + api_kwargs: Optional[JSONDict] = None, + ): + # Required + super().__init__(InlineQueryResultType.GIF, id, api_kwargs=api_kwargs) + with self._unfrozen(): + self.gif_file_id: str = gif_file_id + + # Optionals + 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.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/inlinequeryresultcachedmpeg4gif.py b/inlinequeryresultcachedmpeg4gif.py new file mode 100644 index 0000000000000000000000000000000000000000..f750f4df8fdb2b6486ad69b21c8464a845a1eb44 --- /dev/null +++ b/inlinequeryresultcachedmpeg4gif.py @@ -0,0 +1,130 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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 the classes that represent Telegram InlineQueryResultMpeg4Gif.""" +from typing import TYPE_CHECKING, Optional, Sequence, Tuple + +from telegram._inline.inlinekeyboardmarkup import InlineKeyboardMarkup +from telegram._inline.inlinequeryresult import InlineQueryResult +from telegram._messageentity import MessageEntity +from telegram._utils.argumentparsing import parse_sequence_arg +from telegram._utils.defaultvalue import DEFAULT_NONE +from telegram._utils.types import JSONDict, ODVInput +from telegram.constants import InlineQueryResultType + +if TYPE_CHECKING: + from telegram import InputMessageContent + + +class InlineQueryResultCachedMpeg4Gif(InlineQueryResult): + """ + Represents a link to a video animation (H.264/MPEG-4 AVC video without sound) stored on the + Telegram servers. By default, this animated MPEG-4 file will be sent by the user with an + optional caption. Alternatively, you can use :attr:`input_message_content` to send a message + with the specified content instead of the animation. + + .. seealso:: :wiki:`Working with Files and Media ` + + Args: + id (:obj:`str`): Unique identifier for this result, + :tg-const:`telegram.InlineQueryResult.MIN_ID_LENGTH`- + :tg-const:`telegram.InlineQueryResult.MAX_ID_LENGTH` Bytes. + mpeg4_file_id (:obj:`str`): A valid file identifier for the MP4 file. + title (:obj:`str`, optional): Title for the result. + caption (:obj:`str`, optional): Caption of the MPEG-4 file to be sent, + 0-:tg-const:`telegram.constants.MessageLimit.CAPTION_LENGTH` characters + after entities parsing. + parse_mode (:obj:`str`, optional): |parse_mode| + caption_entities (Sequence[:class:`telegram.MessageEntity`], optional): |caption_entities| + + .. versionchanged:: 20.0 + |sequenceclassargs| + + reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): Inline keyboard attached + to the message. + input_message_content (:class:`telegram.InputMessageContent`, optional): Content of the + message to be sent instead of the MPEG-4 file. + show_caption_above_media (:obj:`bool`, optional): Pass |show_cap_above_med| + + .. versionadded:: 21.3 + + 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_file_id (:obj:`str`): A valid file identifier for the MP4 file. + title (:obj:`str`): Optional. Title for the result. + caption (:obj:`str`): Optional. Caption of the MPEG-4 file to be sent, + 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| + + .. versionchanged:: 20.0 + + * |tupleclassattrs| + * |alwaystuple| + reply_markup (:class:`telegram.InlineKeyboardMarkup`): Optional. Inline keyboard attached + to the message. + input_message_content (:class:`telegram.InputMessageContent`): Optional. Content of the + message to be sent instead of the MPEG-4 file. + show_caption_above_media (:obj:`bool`): Optional. |show_cap_above_med| + + .. versionadded:: 21.3 + + """ + + __slots__ = ( + "caption", + "caption_entities", + "input_message_content", + "mpeg4_file_id", + "parse_mode", + "reply_markup", + "show_caption_above_media", + "title", + ) + + def __init__( + self, + id: str, # pylint: disable=redefined-builtin + mpeg4_file_id: str, + title: Optional[str] = None, + caption: Optional[str] = None, + reply_markup: Optional[InlineKeyboardMarkup] = None, + input_message_content: Optional["InputMessageContent"] = None, + parse_mode: ODVInput[str] = DEFAULT_NONE, + caption_entities: Optional[Sequence[MessageEntity]] = None, + show_caption_above_media: Optional[bool] = None, + *, + api_kwargs: Optional[JSONDict] = None, + ): + # Required + super().__init__(InlineQueryResultType.MPEG4GIF, id, api_kwargs=api_kwargs) + with self._unfrozen(): + self.mpeg4_file_id: str = mpeg4_file_id + + # Optionals + 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.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/inlinequeryresultcachedphoto.py b/inlinequeryresultcachedphoto.py new file mode 100644 index 0000000000000000000000000000000000000000..75f292d2e32e14c34c76b5785c852d241cfd518f --- /dev/null +++ b/inlinequeryresultcachedphoto.py @@ -0,0 +1,135 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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 the classes that represent Telegram InlineQueryResultPhoto""" +from typing import TYPE_CHECKING, Optional, Sequence, Tuple + +from telegram._inline.inlinekeyboardmarkup import InlineKeyboardMarkup +from telegram._inline.inlinequeryresult import InlineQueryResult +from telegram._messageentity import MessageEntity +from telegram._utils.argumentparsing import parse_sequence_arg +from telegram._utils.defaultvalue import DEFAULT_NONE +from telegram._utils.types import JSONDict, ODVInput +from telegram.constants import InlineQueryResultType + +if TYPE_CHECKING: + from telegram import InputMessageContent + + +class InlineQueryResultCachedPhoto(InlineQueryResult): + """ + Represents a link to a photo stored on the Telegram servers. By default, this photo will be + sent by the user with an optional caption. Alternatively, you can use + :attr:`input_message_content` to send a message with the specified content instead + of the photo. + + .. seealso:: :wiki:`Working with Files and Media ` + + Args: + id (:obj:`str`): Unique identifier for this result, + :tg-const:`telegram.InlineQueryResult.MIN_ID_LENGTH`- + :tg-const:`telegram.InlineQueryResult.MAX_ID_LENGTH` Bytes. + photo_file_id (:obj:`str`): A valid file identifier of the photo. + title (:obj:`str`, optional): Title for the result. + description (:obj:`str`, optional): Short description of the result. + caption (:obj:`str`, optional): Caption of the photo to be sent, + 0-:tg-const:`telegram.constants.MessageLimit.CAPTION_LENGTH` characters after + entities parsing. + parse_mode (:obj:`str`, optional): |parse_mode| + caption_entities (Sequence[:class:`telegram.MessageEntity`], optional): |caption_entities| + + .. versionchanged:: 20.0 + |sequenceclassargs| + + reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): Inline keyboard attached + to the message. + input_message_content (:class:`telegram.InputMessageContent`, optional): Content of the + message to be sent instead of the photo. + show_caption_above_media (:obj:`bool`, optional): Pass |show_cap_above_med| + + .. versionadded:: 21.3 + + Attributes: + type (:obj:`str`): :tg-const:`telegram.constants.InlineQueryResultType.PHOTO`. + id (:obj:`str`): Unique identifier for this result, + :tg-const:`telegram.InlineQueryResult.MIN_ID_LENGTH`- + :tg-const:`telegram.InlineQueryResult.MAX_ID_LENGTH` Bytes. + photo_file_id (:obj:`str`): A valid file identifier of the photo. + title (:obj:`str`): Optional. Title for the result. + description (:obj:`str`): Optional. Short description of the result. + caption (:obj:`str`): Optional. Caption of the photo to be sent, + 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| + + .. versionchanged:: 20.0 + + * |tupleclassattrs| + * |alwaystuple| + reply_markup (:class:`telegram.InlineKeyboardMarkup`): Optional. Inline keyboard attached + to the message. + input_message_content (:class:`telegram.InputMessageContent`): Optional. Content of the + message to be sent instead of the photo. + show_caption_above_media (:obj:`bool`): Optional. |show_cap_above_med| + + .. versionadded:: 21.3 + + """ + + __slots__ = ( + "caption", + "caption_entities", + "description", + "input_message_content", + "parse_mode", + "photo_file_id", + "reply_markup", + "show_caption_above_media", + "title", + ) + + def __init__( + self, + id: str, # pylint: disable=redefined-builtin + photo_file_id: str, + title: Optional[str] = None, + description: Optional[str] = None, + caption: Optional[str] = None, + reply_markup: Optional[InlineKeyboardMarkup] = None, + input_message_content: Optional["InputMessageContent"] = None, + parse_mode: ODVInput[str] = DEFAULT_NONE, + caption_entities: Optional[Sequence[MessageEntity]] = None, + show_caption_above_media: Optional[bool] = None, + *, + api_kwargs: Optional[JSONDict] = None, + ): + # Required + super().__init__(InlineQueryResultType.PHOTO, id, api_kwargs=api_kwargs) + with self._unfrozen(): + self.photo_file_id: str = photo_file_id + + # Optionals + self.title: Optional[str] = title + 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.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/inlinequeryresultcachedsticker.py b/inlinequeryresultcachedsticker.py new file mode 100644 index 0000000000000000000000000000000000000000..8e8d22544ca3336588485a4247b23d7a0902721e --- /dev/null +++ b/inlinequeryresultcachedsticker.py @@ -0,0 +1,81 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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 the classes that represent Telegram InlineQueryResultCachedSticker.""" + +from typing import TYPE_CHECKING, Optional + +from telegram._inline.inlinekeyboardmarkup import InlineKeyboardMarkup +from telegram._inline.inlinequeryresult import InlineQueryResult +from telegram._utils.types import JSONDict +from telegram.constants import InlineQueryResultType + +if TYPE_CHECKING: + from telegram import InputMessageContent + + +class InlineQueryResultCachedSticker(InlineQueryResult): + """ + Represents a link to a sticker stored on the Telegram servers. By default, this sticker will + be sent by the user. Alternatively, you can use :attr:`input_message_content` to send a + message with the specified content instead of the sticker. + + .. seealso:: :wiki:`Working with Files and Media ` + + Args: + id (:obj:`str`): Unique identifier for this result, + :tg-const:`telegram.InlineQueryResult.MIN_ID_LENGTH`- + :tg-const:`telegram.InlineQueryResult.MAX_ID_LENGTH` Bytes. + sticker_file_id (:obj:`str`): A valid file identifier of the sticker. + reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): Inline keyboard attached + to the message. + input_message_content (:class:`telegram.InputMessageContent`, optional): Content of the + message to be sent instead of the sticker. + + Attributes: + type (:obj:`str`): :tg-const:`telegram.constants.InlineQueryResultType.STICKER`. + id (:obj:`str`): Unique identifier for this result, + :tg-const:`telegram.InlineQueryResult.MIN_ID_LENGTH`- + :tg-const:`telegram.InlineQueryResult.MAX_ID_LENGTH` Bytes. + sticker_file_id (:obj:`str`): A valid file identifier of the sticker. + reply_markup (:class:`telegram.InlineKeyboardMarkup`): Optional. Inline keyboard attached + to the message. + input_message_content (:class:`telegram.InputMessageContent`): Optional. Content of the + message to be sent instead of the sticker. + + """ + + __slots__ = ("input_message_content", "reply_markup", "sticker_file_id") + + def __init__( + self, + id: str, # pylint: disable=redefined-builtin + sticker_file_id: str, + reply_markup: Optional[InlineKeyboardMarkup] = None, + input_message_content: Optional["InputMessageContent"] = None, + *, + api_kwargs: Optional[JSONDict] = None, + ): + # Required + super().__init__(InlineQueryResultType.STICKER, id, api_kwargs=api_kwargs) + with self._unfrozen(): + self.sticker_file_id: str = sticker_file_id + + # Optionals + self.reply_markup: Optional[InlineKeyboardMarkup] = reply_markup + self.input_message_content: Optional[InputMessageContent] = input_message_content diff --git a/inlinequeryresultcachedvideo.py b/inlinequeryresultcachedvideo.py new file mode 100644 index 0000000000000000000000000000000000000000..99a58eebbe566f22cef38a2dd881e00c261b7852 --- /dev/null +++ b/inlinequeryresultcachedvideo.py @@ -0,0 +1,131 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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 the classes that represent Telegram InlineQueryResultCachedVideo.""" +from typing import TYPE_CHECKING, Optional, Sequence, Tuple + +from telegram._inline.inlinekeyboardmarkup import InlineKeyboardMarkup +from telegram._inline.inlinequeryresult import InlineQueryResult +from telegram._messageentity import MessageEntity +from telegram._utils.argumentparsing import parse_sequence_arg +from telegram._utils.defaultvalue import DEFAULT_NONE +from telegram._utils.types import JSONDict, ODVInput +from telegram.constants import InlineQueryResultType + +if TYPE_CHECKING: + from telegram import InputMessageContent + + +class InlineQueryResultCachedVideo(InlineQueryResult): + """ + Represents a link to a video file stored on the Telegram servers. By default, this video file + will be sent by the user with an optional caption. Alternatively, you can use + :attr:`input_message_content` to send a message with the specified content instead + of the video. + + .. seealso:: :wiki:`Working with Files and Media ` + + Args: + id (:obj:`str`): Unique identifier for this result, + :tg-const:`telegram.InlineQueryResult.MIN_ID_LENGTH`- + :tg-const:`telegram.InlineQueryResult.MAX_ID_LENGTH` Bytes. + video_file_id (:obj:`str`): A valid file identifier for the video file. + title (:obj:`str`): Title for the result. + description (:obj:`str`, optional): Short description of the result. + caption (:obj:`str`, optional): Caption of the video to be sent, + 0-:tg-const:`telegram.constants.MessageLimit.CAPTION_LENGTH` characters after + entities parsing. + parse_mode (:obj:`str`, optional): |parse_mode| + caption_entities (Sequence[:class:`telegram.MessageEntity`], optional): |caption_entities| + reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): Inline keyboard attached + to the message. + input_message_content (:class:`telegram.InputMessageContent`, optional): Content of the + message to be sent instead of the video. + show_caption_above_media (:obj:`bool`, optional): Pass |show_cap_above_med| + + .. versionadded:: 21.3 + + Attributes: + type (:obj:`str`): :tg-const:`telegram.constants.InlineQueryResultType.VIDEO`. + id (:obj:`str`): Unique identifier for this result, + :tg-const:`telegram.InlineQueryResult.MIN_ID_LENGTH`- + :tg-const:`telegram.InlineQueryResult.MAX_ID_LENGTH` Bytes. + video_file_id (:obj:`str`): A valid file identifier for the video file. + title (:obj:`str`): Title for the result. + description (:obj:`str`): Optional. Short description of the result. + caption (:obj:`str`): Optional. Caption of the video to be sent, + 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| + + .. versionchanged:: 20.0 + + * |tupleclassattrs| + * |alwaystuple| + reply_markup (:class:`telegram.InlineKeyboardMarkup`): Optional. Inline keyboard attached + to the message. + input_message_content (:class:`telegram.InputMessageContent`): Optional. Content of the + message to be sent instead of the video. + show_caption_above_media (:obj:`bool`): Optional. |show_cap_above_med| + + .. versionadded:: 21.3 + + """ + + __slots__ = ( + "caption", + "caption_entities", + "description", + "input_message_content", + "parse_mode", + "reply_markup", + "show_caption_above_media", + "title", + "video_file_id", + ) + + def __init__( + self, + id: str, # pylint: disable=redefined-builtin + video_file_id: str, + title: str, + description: Optional[str] = None, + caption: Optional[str] = None, + reply_markup: Optional[InlineKeyboardMarkup] = None, + input_message_content: Optional["InputMessageContent"] = None, + parse_mode: ODVInput[str] = DEFAULT_NONE, + caption_entities: Optional[Sequence[MessageEntity]] = None, + show_caption_above_media: Optional[bool] = None, + *, + api_kwargs: Optional[JSONDict] = None, + ): + # Required + super().__init__(InlineQueryResultType.VIDEO, id, api_kwargs=api_kwargs) + with self._unfrozen(): + self.video_file_id: str = video_file_id + self.title: str = title + + # Optionals + 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.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/inlinequeryresultcachedvoice.py b/inlinequeryresultcachedvoice.py new file mode 100644 index 0000000000000000000000000000000000000000..dc8bd2ad3a6bf6abbd0cfc4a38fc4899fa16f9dc --- /dev/null +++ b/inlinequeryresultcachedvoice.py @@ -0,0 +1,120 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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 the classes that represent Telegram InlineQueryResultCachedVoice.""" +from typing import TYPE_CHECKING, Optional, Sequence, Tuple + +from telegram._inline.inlinekeyboardmarkup import InlineKeyboardMarkup +from telegram._inline.inlinequeryresult import InlineQueryResult +from telegram._messageentity import MessageEntity +from telegram._utils.argumentparsing import parse_sequence_arg +from telegram._utils.defaultvalue import DEFAULT_NONE +from telegram._utils.types import JSONDict, ODVInput +from telegram.constants import InlineQueryResultType + +if TYPE_CHECKING: + from telegram import InputMessageContent + + +class InlineQueryResultCachedVoice(InlineQueryResult): + """ + Represents a link to a voice message stored on the Telegram servers. By default, this voice + message will be sent by the user. Alternatively, you can use :attr:`input_message_content` to + send a message with the specified content instead of the voice message. + + .. seealso:: :wiki:`Working with Files and Media ` + + Args: + id (:obj:`str`): Unique identifier for this result, + :tg-const:`telegram.InlineQueryResult.MIN_ID_LENGTH`- + :tg-const:`telegram.InlineQueryResult.MAX_ID_LENGTH` Bytes. + voice_file_id (:obj:`str`): A valid file identifier for the voice message. + title (:obj:`str`): Voice message title. + caption (:obj:`str`, optional): Caption, + 0-:tg-const:`telegram.constants.MessageLimit.CAPTION_LENGTH` characters after entities + parsing. + parse_mode (:obj:`str`, optional): |parse_mode| + caption_entities (Sequence[:class:`telegram.MessageEntity`], optional): + |captionentitiesattr| + + .. versionchanged:: 20.0 + |sequenceclassargs| + reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): Inline keyboard attached + to the message. + input_message_content (:class:`telegram.InputMessageContent`, optional): Content of the + message to be sent instead of the voice message. + + Attributes: + type (:obj:`str`): :tg-const:`telegram.constants.InlineQueryResultType.VOICE`. + id (:obj:`str`): Unique identifier for this result, + :tg-const:`telegram.InlineQueryResult.MIN_ID_LENGTH`- + :tg-const:`telegram.InlineQueryResult.MAX_ID_LENGTH` Bytes. + voice_file_id (:obj:`str`): A valid file identifier for the voice message. + title (:obj:`str`): Voice message title. + caption (:obj:`str`): Optional. Caption, + 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| + + .. versionchanged:: 20.0 + + * |tupleclassattrs| + * |alwaystuple| + reply_markup (:class:`telegram.InlineKeyboardMarkup`): Optional. Inline keyboard attached + to the message. + input_message_content (:class:`telegram.InputMessageContent`): Optional. Content of the + message to be sent instead of the voice message. + + """ + + __slots__ = ( + "caption", + "caption_entities", + "input_message_content", + "parse_mode", + "reply_markup", + "title", + "voice_file_id", + ) + + def __init__( + self, + id: str, # pylint: disable=redefined-builtin + voice_file_id: str, + title: str, + caption: Optional[str] = None, + reply_markup: Optional[InlineKeyboardMarkup] = None, + input_message_content: Optional["InputMessageContent"] = None, + parse_mode: ODVInput[str] = DEFAULT_NONE, + caption_entities: Optional[Sequence[MessageEntity]] = None, + *, + api_kwargs: Optional[JSONDict] = None, + ): + # Required + super().__init__(InlineQueryResultType.VOICE, id, api_kwargs=api_kwargs) + with self._unfrozen(): + self.voice_file_id: str = voice_file_id + self.title: str = title + + # Optionals + self.caption: Optional[str] = caption + self.parse_mode: ODVInput[str] = parse_mode + 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/inlinequeryresultcontact.py b/inlinequeryresultcontact.py new file mode 100644 index 0000000000000000000000000000000000000000..faff47454d32c53f5cf450066d677cff804a8d89 --- /dev/null +++ b/inlinequeryresultcontact.py @@ -0,0 +1,130 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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 the classes that represent Telegram InlineQueryResultContact.""" + +from typing import TYPE_CHECKING, Optional + +from telegram._inline.inlinekeyboardmarkup import InlineKeyboardMarkup +from telegram._inline.inlinequeryresult import InlineQueryResult +from telegram._utils.types import JSONDict +from telegram.constants import InlineQueryResultType + +if TYPE_CHECKING: + from telegram import InputMessageContent + + +class InlineQueryResultContact(InlineQueryResult): + """ + Represents a contact with a phone number. By default, this contact will be sent by the user. + Alternatively, you can use :attr:`input_message_content` to send a message with the specified + content instead of the contact. + + .. versionchanged:: 20.5 + |removed_thumb_wildcard_note| + + Args: + id (:obj:`str`): Unique identifier for this result, + :tg-const:`telegram.InlineQueryResult.MIN_ID_LENGTH`- + :tg-const:`telegram.InlineQueryResult.MAX_ID_LENGTH` Bytes. + phone_number (:obj:`str`): Contact's phone number. + first_name (:obj:`str`): Contact's first name. + last_name (:obj:`str`, optional): Contact's last name. + vcard (:obj:`str`, optional): Additional data about the contact in the form of a vCard, + 0-:tg-const:`telegram.constants.ContactLimit.VCARD` bytes. + reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): Inline keyboard attached + to the message. + input_message_content (:class:`telegram.InputMessageContent`, optional): Content of the + message to be sent instead of the contact. + thumbnail_url (:obj:`str`, optional): Url of the thumbnail for the result. + + .. versionadded:: 20.2 + thumbnail_width (:obj:`int`, optional): Thumbnail width. + + .. versionadded:: 20.2 + thumbnail_height (:obj:`int`, optional): Thumbnail height. + + .. versionadded:: 20.2 + + Attributes: + type (:obj:`str`): :tg-const:`telegram.constants.InlineQueryResultType.CONTACT`. + id (:obj:`str`): Unique identifier for this result, + :tg-const:`telegram.InlineQueryResult.MIN_ID_LENGTH`- + :tg-const:`telegram.InlineQueryResult.MAX_ID_LENGTH` Bytes. + phone_number (:obj:`str`): Contact's phone number. + first_name (:obj:`str`): Contact's first name. + last_name (:obj:`str`): Optional. Contact's last name. + vcard (:obj:`str`): Optional. Additional data about the contact in the form of a vCard, + 0-:tg-const:`telegram.constants.ContactLimit.VCARD` bytes. + reply_markup (:class:`telegram.InlineKeyboardMarkup`): Optional. Inline keyboard attached + to the message. + input_message_content (:class:`telegram.InputMessageContent`): Optional. Content of the + message to be sent instead of the contact. + thumbnail_url (:obj:`str`): Optional. Url of the thumbnail for the result. + + .. versionadded:: 20.2 + thumbnail_width (:obj:`int`): Optional. Thumbnail width. + + .. versionadded:: 20.2 + thumbnail_height (:obj:`int`): Optional. Thumbnail height. + + .. versionadded:: 20.2 + + """ + + __slots__ = ( + "first_name", + "input_message_content", + "last_name", + "phone_number", + "reply_markup", + "thumbnail_height", + "thumbnail_url", + "thumbnail_width", + "vcard", + ) + + def __init__( + self, + id: str, # pylint: disable=redefined-builtin + phone_number: str, + first_name: str, + last_name: Optional[str] = None, + reply_markup: Optional[InlineKeyboardMarkup] = None, + input_message_content: Optional["InputMessageContent"] = None, + vcard: Optional[str] = None, + thumbnail_url: Optional[str] = None, + thumbnail_width: Optional[int] = None, + thumbnail_height: Optional[int] = None, + *, + api_kwargs: Optional[JSONDict] = None, + ): + # Required + super().__init__(InlineQueryResultType.CONTACT, id, api_kwargs=api_kwargs) + with self._unfrozen(): + self.phone_number: str = phone_number + self.first_name: str = first_name + + # Optionals + self.last_name: Optional[str] = last_name + self.vcard: Optional[str] = vcard + self.reply_markup: Optional[InlineKeyboardMarkup] = reply_markup + self.input_message_content: Optional[InputMessageContent] = input_message_content + self.thumbnail_url: Optional[str] = thumbnail_url + self.thumbnail_width: Optional[int] = thumbnail_width + self.thumbnail_height: Optional[int] = thumbnail_height diff --git a/inlinequeryresultdocument.py b/inlinequeryresultdocument.py new file mode 100644 index 0000000000000000000000000000000000000000..e0380440b20a3388b8367d0d335260b03ee555e2 --- /dev/null +++ b/inlinequeryresultdocument.py @@ -0,0 +1,164 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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 the classes that represent Telegram InlineQueryResultDocument""" +from typing import TYPE_CHECKING, Optional, Sequence, Tuple + +from telegram._inline.inlinekeyboardmarkup import InlineKeyboardMarkup +from telegram._inline.inlinequeryresult import InlineQueryResult +from telegram._messageentity import MessageEntity +from telegram._utils.argumentparsing import parse_sequence_arg +from telegram._utils.defaultvalue import DEFAULT_NONE +from telegram._utils.types import JSONDict, ODVInput +from telegram.constants import InlineQueryResultType + +if TYPE_CHECKING: + from telegram import InputMessageContent + + +class InlineQueryResultDocument(InlineQueryResult): + """ + Represents a link to a file. By default, this file will be sent by the user with an optional + caption. Alternatively, you can use :attr:`input_message_content` to send a message with the + specified content instead of the file. Currently, only .PDF and .ZIP files can be sent + using this method. + + .. seealso:: :wiki:`Working with Files and Media ` + + .. versionchanged:: 20.5 + |removed_thumb_wildcard_note| + + Args: + id (:obj:`str`): Unique identifier for this result, + :tg-const:`telegram.InlineQueryResult.MIN_ID_LENGTH`- + :tg-const:`telegram.InlineQueryResult.MAX_ID_LENGTH` Bytes. + title (:obj:`str`): Title for the result. + caption (:obj:`str`, optional): Caption of the document to be sent, + 0-:tg-const:`telegram.constants.MessageLimit.CAPTION_LENGTH` characters + after entities parsing. + parse_mode (:obj:`str`, optional): |parse_mode| + caption_entities (Sequence[:class:`telegram.MessageEntity`], optional): |caption_entities| + + .. versionchanged:: 20.0 + |sequenceclassargs| + + document_url (:obj:`str`): A valid URL for the file. + mime_type (:obj:`str`): Mime type of the content of the file, either "application/pdf" + or "application/zip". + description (:obj:`str`, optional): Short description of the result. + reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): Inline keyboard attached + to the message. + input_message_content (:class:`telegram.InputMessageContent`, optional): Content of the + message to be sent instead of the file. + thumbnail_url (:obj:`str`, optional): URL of the thumbnail (JPEG only) for the file. + + .. versionadded:: 20.2 + thumbnail_width (:obj:`int`, optional): Thumbnail width. + + .. versionadded:: 20.2 + thumbnail_height (:obj:`int`, optional): Thumbnail height. + + .. versionadded:: 20.2 + + + Attributes: + type (:obj:`str`): :tg-const:`telegram.constants.InlineQueryResultType.DOCUMENT`. + id (:obj:`str`): Unique identifier for this result, + :tg-const:`telegram.InlineQueryResult.MIN_ID_LENGTH`- + :tg-const:`telegram.InlineQueryResult.MAX_ID_LENGTH` Bytes. + title (:obj:`str`): Title for the result. + caption (:obj:`str`): Optional. Caption of the document to be sent, + 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| + + .. versionchanged:: 20.0 + + * |tupleclassattrs| + * |alwaystuple| + document_url (:obj:`str`): A valid URL for the file. + mime_type (:obj:`str`): Mime type of the content of the file, either "application/pdf" + or "application/zip". + description (:obj:`str`): Optional. Short description of the result. + reply_markup (:class:`telegram.InlineKeyboardMarkup`): Optional. Inline keyboard attached + to the message. + input_message_content (:class:`telegram.InputMessageContent`): Optional. Content of the + message to be sent instead of the file. + thumbnail_url (:obj:`str`): Optional. URL of the thumbnail (JPEG only) for the file. + + .. versionadded:: 20.2 + thumbnail_width (:obj:`int`): Optional. Thumbnail width. + + .. versionadded:: 20.2 + thumbnail_height (:obj:`int`): Optional. Thumbnail height. + + .. versionadded:: 20.2 + + """ + + __slots__ = ( + "caption", + "caption_entities", + "description", + "document_url", + "input_message_content", + "mime_type", + "parse_mode", + "reply_markup", + "thumbnail_height", + "thumbnail_url", + "thumbnail_width", + "title", + ) + + def __init__( + self, + id: str, # pylint: disable=redefined-builtin + document_url: str, + title: str, + mime_type: str, + caption: Optional[str] = None, + description: Optional[str] = None, + reply_markup: Optional[InlineKeyboardMarkup] = None, + input_message_content: Optional["InputMessageContent"] = None, + parse_mode: ODVInput[str] = DEFAULT_NONE, + caption_entities: Optional[Sequence[MessageEntity]] = None, + thumbnail_url: Optional[str] = None, + thumbnail_width: Optional[int] = None, + thumbnail_height: Optional[int] = None, + *, + api_kwargs: Optional[JSONDict] = None, + ): + # Required + super().__init__(InlineQueryResultType.DOCUMENT, id, api_kwargs=api_kwargs) + with self._unfrozen(): + self.document_url: str = document_url + self.title: str = title + self.mime_type: str = mime_type + + # Optionals + self.caption: Optional[str] = caption + self.parse_mode: ODVInput[str] = parse_mode + 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 + self.thumbnail_url: Optional[str] = thumbnail_url + self.thumbnail_width: Optional[int] = thumbnail_width + self.thumbnail_height: Optional[int] = thumbnail_height diff --git a/inlinequeryresultgame.py b/inlinequeryresultgame.py new file mode 100644 index 0000000000000000000000000000000000000000..aeb78c0f1b4ccbe7c6928c8a35839ec5b173f274 --- /dev/null +++ b/inlinequeryresultgame.py @@ -0,0 +1,66 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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 the classes that represent Telegram InlineQueryResultGame.""" +from typing import Optional + +from telegram._inline.inlinekeyboardmarkup import InlineKeyboardMarkup +from telegram._inline.inlinequeryresult import InlineQueryResult +from telegram._utils.types import JSONDict +from telegram.constants import InlineQueryResultType + + +class InlineQueryResultGame(InlineQueryResult): + """Represents a :class:`telegram.Game`. + + Args: + id (:obj:`str`): Unique identifier for this result, + :tg-const:`telegram.InlineQueryResult.MIN_ID_LENGTH`- + :tg-const:`telegram.InlineQueryResult.MAX_ID_LENGTH` Bytes. + game_short_name (:obj:`str`): Short name of the game. + reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): Inline keyboard attached + to the message. + + Attributes: + type (:obj:`str`): :tg-const:`telegram.constants.InlineQueryResultType.GAME`. + id (:obj:`str`): Unique identifier for this result, + :tg-const:`telegram.InlineQueryResult.MIN_ID_LENGTH`- + :tg-const:`telegram.InlineQueryResult.MAX_ID_LENGTH` Bytes. + game_short_name (:obj:`str`): Short name of the game. + reply_markup (:class:`telegram.InlineKeyboardMarkup`): Optional. Inline keyboard attached + to the message. + + """ + + __slots__ = ("game_short_name", "reply_markup") + + def __init__( + self, + id: str, # pylint: disable=redefined-builtin + game_short_name: str, + reply_markup: Optional[InlineKeyboardMarkup] = None, + *, + api_kwargs: Optional[JSONDict] = None, + ): + # Required + super().__init__(InlineQueryResultType.GAME, id, api_kwargs=api_kwargs) + with self._unfrozen(): + self.id: str = id + self.game_short_name: str = game_short_name + + self.reply_markup: Optional[InlineKeyboardMarkup] = reply_markup diff --git a/inlinequeryresultgif.py b/inlinequeryresultgif.py new file mode 100644 index 0000000000000000000000000000000000000000..e5694e4f856f8e04f8f3947dfb68b0dece1f77cb --- /dev/null +++ b/inlinequeryresultgif.py @@ -0,0 +1,173 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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 the classes that represent Telegram InlineQueryResultGif.""" +from typing import TYPE_CHECKING, Optional, Sequence, Tuple + +from telegram._inline.inlinekeyboardmarkup import InlineKeyboardMarkup +from telegram._inline.inlinequeryresult import InlineQueryResult +from telegram._messageentity import MessageEntity +from telegram._utils.argumentparsing import parse_sequence_arg +from telegram._utils.defaultvalue import DEFAULT_NONE +from telegram._utils.types import JSONDict, ODVInput +from telegram.constants import InlineQueryResultType + +if TYPE_CHECKING: + from telegram import InputMessageContent + + +class InlineQueryResultGif(InlineQueryResult): + """ + Represents a link to an animated GIF file. By default, this animated GIF file will be sent by + the user with optional caption. Alternatively, you can use :attr:`input_message_content` to + send a message with the specified content instead of the animation. + + .. seealso:: :wiki:`Working with Files and Media ` + + .. versionchanged:: 20.5 + |removed_thumb_wildcard_note| + + Args: + 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_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`): URL of the static (JPEG or GIF) or animated (MPEG4) + thumbnail for the result. + + .. 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'``. + + .. versionadded:: 20.2 + title (:obj:`str`, optional): Title for the result. + caption (:obj:`str`, optional): Caption of the GIF file to be sent, + 0-:tg-const:`telegram.constants.MessageLimit.CAPTION_LENGTH` characters + after entities parsing. + parse_mode (:obj:`str`, optional): |parse_mode| + caption_entities (Sequence[:class:`telegram.MessageEntity`], optional): |caption_entities| + + .. versionchanged:: 20.0 + |sequenceclassargs| + + reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): Inline keyboard attached + to the message. + input_message_content (:class:`telegram.InputMessageContent`, optional): Content of the + message to be sent instead of the GIF animation. + show_caption_above_media (:obj:`bool`, optional): Pass |show_cap_above_med| + + .. versionadded:: 21.3 + + 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_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`): URL of the static (JPEG or GIF) or animated (MPEG4) thumbnail + for the result. + + .. versionadded:: 20.2 + 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'``. + + .. versionadded:: 20.2 + title (:obj:`str`): Optional. Title for the result. + caption (:obj:`str`): Optional. Caption of the GIF file to be sent, + 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| + + .. versionchanged:: 20.0 + + * |tupleclassattrs| + * |alwaystuple| + reply_markup (:class:`telegram.InlineKeyboardMarkup`): Optional. Inline keyboard attached + to the message. + input_message_content (:class:`telegram.InputMessageContent`): Optional. Content of the + message to be sent instead of the GIF animation. + show_caption_above_media (:obj:`bool`): Optional. |show_cap_above_med| + + .. versionadded:: 21.3 + + """ + + __slots__ = ( + "caption", + "caption_entities", + "gif_duration", + "gif_height", + "gif_url", + "gif_width", + "input_message_content", + "parse_mode", + "reply_markup", + "show_caption_above_media", + "thumbnail_mime_type", + "thumbnail_url", + "title", + ) + + def __init__( + self, + id: str, # pylint: disable=redefined-builtin + gif_url: str, + thumbnail_url: str, + gif_width: Optional[int] = None, + gif_height: Optional[int] = None, + title: Optional[str] = None, + caption: Optional[str] = None, + reply_markup: Optional[InlineKeyboardMarkup] = None, + input_message_content: Optional["InputMessageContent"] = None, + gif_duration: Optional[int] = None, + parse_mode: ODVInput[str] = DEFAULT_NONE, + caption_entities: Optional[Sequence[MessageEntity]] = None, + thumbnail_mime_type: Optional[str] = None, + show_caption_above_media: Optional[bool] = None, + *, + api_kwargs: Optional[JSONDict] = None, + ): + # Required + super().__init__(InlineQueryResultType.GIF, id, api_kwargs=api_kwargs) + with self._unfrozen(): + self.gif_url: str = gif_url + self.thumbnail_url: str = thumbnail_url + + # Optionals + self.gif_width: Optional[int] = gif_width + self.gif_height: Optional[int] = gif_height + self.gif_duration: Optional[int] = gif_duration + 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.reply_markup: Optional[InlineKeyboardMarkup] = reply_markup + self.input_message_content: Optional[InputMessageContent] = input_message_content + self.thumbnail_mime_type: Optional[str] = thumbnail_mime_type + self.show_caption_above_media: Optional[bool] = show_caption_above_media diff --git a/inlinequeryresultlocation.py b/inlinequeryresultlocation.py new file mode 100644 index 0000000000000000000000000000000000000000..dff2b29a48bed95e778de09ef0a2049482dbd088 --- /dev/null +++ b/inlinequeryresultlocation.py @@ -0,0 +1,207 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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 the classes that represent Telegram InlineQueryResultLocation.""" + +from typing import TYPE_CHECKING, Final, Optional + +from telegram import constants +from telegram._inline.inlinekeyboardmarkup import InlineKeyboardMarkup +from telegram._inline.inlinequeryresult import InlineQueryResult +from telegram._utils.types import JSONDict + +if TYPE_CHECKING: + from telegram import InputMessageContent + + +class InlineQueryResultLocation(InlineQueryResult): + """ + Represents a location on a map. By default, the location will be sent by the user. + Alternatively, you can use :attr:`input_message_content` to send a message with the specified + content instead of the location. + + .. versionchanged:: 20.5 + |removed_thumb_wildcard_note| + + Args: + id (:obj:`str`): Unique identifier for this result, + :tg-const:`telegram.InlineQueryResult.MIN_ID_LENGTH`- + :tg-const:`telegram.InlineQueryResult.MAX_ID_LENGTH` Bytes. + latitude (:obj:`float`): Location latitude in degrees. + longitude (:obj:`float`): Location longitude in degrees. + title (:obj:`str`): Location title. + horizontal_accuracy (:obj:`float`, optional): The radius of uncertainty for the location, + measured in meters; 0- + :tg-const:`telegram.InlineQueryResultLocation.HORIZONTAL_ACCURACY`. + live_period (:obj:`int`, optional): Period in seconds for which the location will be + updated, should be between + :tg-const:`telegram.InlineQueryResultLocation.MIN_LIVE_PERIOD` and + :tg-const:`telegram.InlineQueryResultLocation.MAX_LIVE_PERIOD`. + heading (:obj:`int`, optional): For live locations, a direction in which the user is + moving, in degrees. Must be between + :tg-const:`telegram.InlineQueryResultLocation.MIN_HEADING` and + :tg-const:`telegram.InlineQueryResultLocation.MAX_HEADING` if specified. + proximity_alert_radius (:obj:`int`, optional): For live locations, a maximum distance + for proximity alerts about approaching another chat member, in meters. Must be + between :tg-const:`telegram.InlineQueryResultLocation.MIN_PROXIMITY_ALERT_RADIUS` + and :tg-const:`telegram.InlineQueryResultLocation.MAX_PROXIMITY_ALERT_RADIUS` + if specified. + reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): Inline keyboard attached + to the message. + input_message_content (:class:`telegram.InputMessageContent`, optional): Content of the + message to be sent instead of the location. + thumbnail_url (:obj:`str`, optional): Url of the thumbnail for the result. + + .. versionadded:: 20.2 + thumbnail_width (:obj:`int`, optional): Thumbnail width. + + .. versionadded:: 20.2 + thumbnail_height (:obj:`int`, optional): Thumbnail height. + + .. versionadded:: 20.2 + + Attributes: + type (:obj:`str`): :tg-const:`telegram.constants.InlineQueryResultType.LOCATION`. + id (:obj:`str`): Unique identifier for this result, + :tg-const:`telegram.InlineQueryResult.MIN_ID_LENGTH`- + :tg-const:`telegram.InlineQueryResult.MAX_ID_LENGTH` Bytes. + latitude (:obj:`float`): Location latitude in degrees. + longitude (:obj:`float`): Location longitude in degrees. + title (:obj:`str`): Location title. + horizontal_accuracy (:obj:`float`): Optional. The radius of uncertainty for the location, + measured in meters; 0- + :tg-const:`telegram.InlineQueryResultLocation.HORIZONTAL_ACCURACY`. + live_period (:obj:`int`): Optional. Period in seconds for which the location will be + updated, should be between + :tg-const:`telegram.InlineQueryResultLocation.MIN_LIVE_PERIOD` and + :tg-const:`telegram.InlineQueryResultLocation.MAX_LIVE_PERIOD` or + :tg-const:`telegram.constants.LocationLimit.LIVE_PERIOD_FOREVER` for live + locations that can be edited indefinitely. + heading (:obj:`int`): Optional. For live locations, a direction in which the user is + moving, in degrees. Must be between + :tg-const:`telegram.InlineQueryResultLocation.MIN_HEADING` and + :tg-const:`telegram.InlineQueryResultLocation.MAX_HEADING` if specified. + proximity_alert_radius (:obj:`int`): Optional. For live locations, a maximum distance + for proximity alerts about approaching another chat member, in meters. Must be + between :tg-const:`telegram.InlineQueryResultLocation.MIN_PROXIMITY_ALERT_RADIUS` + and :tg-const:`telegram.InlineQueryResultLocation.MAX_PROXIMITY_ALERT_RADIUS` + if specified. + reply_markup (:class:`telegram.InlineKeyboardMarkup`): Optional. Inline keyboard attached + to the message. + input_message_content (:class:`telegram.InputMessageContent`): Optional. Content of the + message to be sent instead of the location. + thumbnail_url (:obj:`str`): Optional. Url of the thumbnail for the result. + + .. versionadded:: 20.2 + thumbnail_width (:obj:`int`): Optional. Thumbnail width. + + .. versionadded:: 20.2 + thumbnail_height (:obj:`int`): Optional. Thumbnail height. + + .. versionadded:: 20.2 + + """ + + __slots__ = ( + "heading", + "horizontal_accuracy", + "input_message_content", + "latitude", + "live_period", + "longitude", + "proximity_alert_radius", + "reply_markup", + "thumbnail_height", + "thumbnail_url", + "thumbnail_width", + "title", + ) + + def __init__( + self, + id: str, # pylint: disable=redefined-builtin + latitude: float, + longitude: float, + title: str, + live_period: Optional[int] = None, + reply_markup: Optional[InlineKeyboardMarkup] = None, + input_message_content: Optional["InputMessageContent"] = None, + horizontal_accuracy: Optional[float] = None, + heading: Optional[int] = None, + proximity_alert_radius: Optional[int] = None, + thumbnail_url: Optional[str] = None, + thumbnail_width: Optional[int] = None, + thumbnail_height: Optional[int] = None, + *, + api_kwargs: Optional[JSONDict] = None, + ): + # Required + super().__init__(constants.InlineQueryResultType.LOCATION, id, api_kwargs=api_kwargs) + with self._unfrozen(): + self.latitude: float = latitude + self.longitude: float = longitude + self.title: str = title + + # Optionals + self.live_period: Optional[int] = live_period + self.reply_markup: Optional[InlineKeyboardMarkup] = reply_markup + self.input_message_content: Optional[InputMessageContent] = input_message_content + self.thumbnail_url: Optional[str] = thumbnail_url + self.thumbnail_width: Optional[int] = thumbnail_width + self.thumbnail_height: Optional[int] = thumbnail_height + self.horizontal_accuracy: Optional[float] = horizontal_accuracy + self.heading: Optional[int] = heading + self.proximity_alert_radius: Optional[int] = ( + int(proximity_alert_radius) if proximity_alert_radius else None + ) + + HORIZONTAL_ACCURACY: Final[int] = constants.LocationLimit.HORIZONTAL_ACCURACY + """:const:`telegram.constants.LocationLimit.HORIZONTAL_ACCURACY` + + .. versionadded:: 20.0 + """ + MIN_HEADING: Final[int] = constants.LocationLimit.MIN_HEADING + """:const:`telegram.constants.LocationLimit.MIN_HEADING` + + .. versionadded:: 20.0 + """ + MAX_HEADING: Final[int] = constants.LocationLimit.MAX_HEADING + """:const:`telegram.constants.LocationLimit.MAX_HEADING` + + .. versionadded:: 20.0 + """ + MIN_LIVE_PERIOD: Final[int] = constants.LocationLimit.MIN_LIVE_PERIOD + """:const:`telegram.constants.LocationLimit.MIN_LIVE_PERIOD` + + .. versionadded:: 20.0 + """ + MAX_LIVE_PERIOD: Final[int] = constants.LocationLimit.MAX_LIVE_PERIOD + """:const:`telegram.constants.LocationLimit.MAX_LIVE_PERIOD` + + .. versionadded:: 20.0 + """ + MIN_PROXIMITY_ALERT_RADIUS: Final[int] = constants.LocationLimit.MIN_PROXIMITY_ALERT_RADIUS + """:const:`telegram.constants.LocationLimit.MIN_PROXIMITY_ALERT_RADIUS` + + .. versionadded:: 20.0 + """ + MAX_PROXIMITY_ALERT_RADIUS: Final[int] = constants.LocationLimit.MAX_PROXIMITY_ALERT_RADIUS + """:const:`telegram.constants.LocationLimit.MAX_PROXIMITY_ALERT_RADIUS` + + .. versionadded:: 20.0 + """ diff --git a/inlinequeryresultmpeg4gif.py b/inlinequeryresultmpeg4gif.py new file mode 100644 index 0000000000000000000000000000000000000000..9e27ab949df81f44939fd82a3c6ee1b225f20023 --- /dev/null +++ b/inlinequeryresultmpeg4gif.py @@ -0,0 +1,175 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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 the classes that represent Telegram InlineQueryResultMpeg4Gif.""" +from typing import TYPE_CHECKING, Optional, Sequence, Tuple + +from telegram._inline.inlinekeyboardmarkup import InlineKeyboardMarkup +from telegram._inline.inlinequeryresult import InlineQueryResult +from telegram._messageentity import MessageEntity +from telegram._utils.argumentparsing import parse_sequence_arg +from telegram._utils.defaultvalue import DEFAULT_NONE +from telegram._utils.types import JSONDict, ODVInput +from telegram.constants import InlineQueryResultType + +if TYPE_CHECKING: + from telegram import InputMessageContent + + +class InlineQueryResultMpeg4Gif(InlineQueryResult): + """ + Represents a link to a video animation (H.264/MPEG-4 AVC video without sound). By default, this + animated MPEG-4 file will be sent by the user with optional caption. Alternatively, you can + use :attr:`input_message_content` to send a message with the specified content instead of the + animation. + + .. seealso:: :wiki:`Working with Files and Media ` + + .. versionchanged:: 20.5 + |removed_thumb_wildcard_note| + + Args: + 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_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`): URL of the static (JPEG or GIF) or animated (MPEG4) + thumbnail for the result. + + .. 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'``. + + .. versionadded:: 20.2 + title (:obj:`str`, optional): Title for the result. + caption (:obj:`str`, optional): Caption of the MPEG-4 file to be sent, + 0-:tg-const:`telegram.constants.MessageLimit.CAPTION_LENGTH` characters + after entities parsing. + parse_mode (:obj:`str`, optional): |parse_mode| + caption_entities (Sequence[:class:`telegram.MessageEntity`], optional): + |captionentitiesattr| + + .. versionchanged:: 20.0 + |sequenceclassargs| + + reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): Inline keyboard attached + to the message. + input_message_content (:class:`telegram.InputMessageContent`, optional): Content of the + message to be sent instead of the video animation. + show_caption_above_media (:obj:`bool`, optional): Pass |show_cap_above_med| + + .. versionadded:: 21.3 + + 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_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`): URL of the static (JPEG or GIF) or animated (MPEG4) thumbnail + for the result. + + .. versionadded:: 20.2 + 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'``. + + .. versionadded:: 20.2 + title (:obj:`str`): Optional. Title for the result. + caption (:obj:`str`): Optional. Caption of the MPEG-4 file to be sent, + 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| + + .. versionchanged:: 20.0 + + * |tupleclassattrs| + * |alwaystuple| + + reply_markup (:class:`telegram.InlineKeyboardMarkup`): Optional. Inline keyboard attached + to the message. + input_message_content (:class:`telegram.InputMessageContent`): Optional. Content of the + message to be sent instead of the video animation. + show_caption_above_media (:obj:`bool`): Optional. |show_cap_above_med| + + .. versionadded:: 21.3 + """ + + __slots__ = ( + "caption", + "caption_entities", + "input_message_content", + "mpeg4_duration", + "mpeg4_height", + "mpeg4_url", + "mpeg4_width", + "parse_mode", + "reply_markup", + "show_caption_above_media", + "thumbnail_mime_type", + "thumbnail_url", + "title", + ) + + def __init__( + self, + id: str, # pylint: disable=redefined-builtin + mpeg4_url: str, + thumbnail_url: str, + mpeg4_width: Optional[int] = None, + mpeg4_height: Optional[int] = None, + title: Optional[str] = None, + caption: Optional[str] = None, + reply_markup: Optional[InlineKeyboardMarkup] = None, + input_message_content: Optional["InputMessageContent"] = None, + mpeg4_duration: Optional[int] = None, + parse_mode: ODVInput[str] = DEFAULT_NONE, + caption_entities: Optional[Sequence[MessageEntity]] = None, + thumbnail_mime_type: Optional[str] = None, + show_caption_above_media: Optional[bool] = None, + *, + api_kwargs: Optional[JSONDict] = None, + ): + # Required + super().__init__(InlineQueryResultType.MPEG4GIF, id, api_kwargs=api_kwargs) + with self._unfrozen(): + self.mpeg4_url: str = mpeg4_url + self.thumbnail_url: str = thumbnail_url + + # Optional + self.mpeg4_width: Optional[int] = mpeg4_width + self.mpeg4_height: Optional[int] = mpeg4_height + self.mpeg4_duration: Optional[int] = mpeg4_duration + 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.reply_markup: Optional[InlineKeyboardMarkup] = reply_markup + self.input_message_content: Optional[InputMessageContent] = input_message_content + self.thumbnail_mime_type: Optional[str] = thumbnail_mime_type + self.show_caption_above_media: Optional[bool] = show_caption_above_media diff --git a/inlinequeryresultphoto.py b/inlinequeryresultphoto.py new file mode 100644 index 0000000000000000000000000000000000000000..b74adf218e382f0e43708a85a3b9a4dc56074e85 --- /dev/null +++ b/inlinequeryresultphoto.py @@ -0,0 +1,160 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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 the classes that represent Telegram InlineQueryResultPhoto.""" +from typing import TYPE_CHECKING, Optional, Sequence, Tuple + +from telegram._inline.inlinekeyboardmarkup import InlineKeyboardMarkup +from telegram._inline.inlinequeryresult import InlineQueryResult +from telegram._messageentity import MessageEntity +from telegram._utils.argumentparsing import parse_sequence_arg +from telegram._utils.defaultvalue import DEFAULT_NONE +from telegram._utils.types import JSONDict, ODVInput +from telegram.constants import InlineQueryResultType + +if TYPE_CHECKING: + from telegram import InputMessageContent + + +class InlineQueryResultPhoto(InlineQueryResult): + """ + Represents a link to a photo. By default, this photo will be sent by the user with optional + caption. Alternatively, you can use :attr:`input_message_content` to send a message with the + specified content instead of the photo. + + .. seealso:: :wiki:`Working with Files and Media ` + + .. versionchanged:: 20.5 + |removed_thumb_url_note| + + Args: + id (:obj:`str`): Unique identifier for this result, + :tg-const:`telegram.InlineQueryResult.MIN_ID_LENGTH`- + :tg-const:`telegram.InlineQueryResult.MAX_ID_LENGTH` Bytes. + photo_url (:obj:`str`): A valid URL of the photo. Photo must be in JPEG format. Photo size + must not exceed 5MB. + thumbnail_url (:obj:`str`): URL of the thumbnail for the photo. + + .. versionadded:: 20.2 + + ..versionchanged:: 20.5 + |thumbnail_url_mandatory| + + photo_width (:obj:`int`, optional): Width of the photo. + photo_height (:obj:`int`, optional): Height of the photo. + title (:obj:`str`, optional): Title for the result. + description (:obj:`str`, optional): Short description of the result. + caption (:obj:`str`, optional): Caption of the photo to be sent, + 0-:tg-const:`telegram.constants.MessageLimit.CAPTION_LENGTH` characters after + entities parsing. + parse_mode (:obj:`str`, optional): |parse_mode| + caption_entities (Sequence[:class:`telegram.MessageEntity`], optional): |caption_entities| + + .. versionchanged:: 20.0 + |sequenceclassargs| + + reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): Inline keyboard attached + to the message. + input_message_content (:class:`telegram.InputMessageContent`, optional): Content of the + message to be sent instead of the photo. + show_caption_above_media (:obj:`bool`, optional): Pass |show_cap_above_med| + + .. versionadded:: 21.3 + + Attributes: + type (:obj:`str`): :tg-const:`telegram.constants.InlineQueryResultType.PHOTO`. + id (:obj:`str`): Unique identifier for this result, + :tg-const:`telegram.InlineQueryResult.MIN_ID_LENGTH`- + :tg-const:`telegram.InlineQueryResult.MAX_ID_LENGTH` Bytes. + photo_url (:obj:`str`): A valid URL of the photo. Photo must be in JPEG format. Photo size + must not exceed 5MB. + thumbnail_url (:obj:`str`): URL of the thumbnail for the photo. + photo_width (:obj:`int`): Optional. Width of the photo. + photo_height (:obj:`int`): Optional. Height of the photo. + title (:obj:`str`): Optional. Title for the result. + description (:obj:`str`): Optional. Short description of the result. + caption (:obj:`str`): Optional. Caption of the photo to be sent, + 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| + + .. versionchanged:: 20.0 + + * |tupleclassattrs| + * |alwaystuple| + reply_markup (:class:`telegram.InlineKeyboardMarkup`): Optional. Inline keyboard attached + to the message. + input_message_content (:class:`telegram.InputMessageContent`): Optional. Content of the + message to be sent instead of the photo. + show_caption_above_media (:obj:`bool`): Optional. |show_cap_above_med| + + .. versionadded:: 21.3 + + """ + + __slots__ = ( + "caption", + "caption_entities", + "description", + "input_message_content", + "parse_mode", + "photo_height", + "photo_url", + "photo_width", + "reply_markup", + "show_caption_above_media", + "thumbnail_url", + "title", + ) + + def __init__( + self, + id: str, # pylint: disable=redefined-builtin + photo_url: str, + thumbnail_url: str, + photo_width: Optional[int] = None, + photo_height: Optional[int] = None, + title: Optional[str] = None, + description: Optional[str] = None, + caption: Optional[str] = None, + reply_markup: Optional[InlineKeyboardMarkup] = None, + input_message_content: Optional["InputMessageContent"] = None, + parse_mode: ODVInput[str] = DEFAULT_NONE, + caption_entities: Optional[Sequence[MessageEntity]] = None, + show_caption_above_media: Optional[bool] = None, + *, + api_kwargs: Optional[JSONDict] = None, + ): + # Required + super().__init__(InlineQueryResultType.PHOTO, id, api_kwargs=api_kwargs) + with self._unfrozen(): + self.photo_url: str = photo_url + self.thumbnail_url: str = thumbnail_url + + # Optionals + self.photo_width: Optional[int] = photo_width + self.photo_height: Optional[int] = photo_height + self.title: Optional[str] = title + 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.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/inlinequeryresultsbutton.py b/inlinequeryresultsbutton.py new file mode 100644 index 0000000000000000000000000000000000000000..ae0b404e1f8b6e7a9e1c64ebee55206dfe4fde04 --- /dev/null +++ b/inlinequeryresultsbutton.py @@ -0,0 +1,118 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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 the class that represent a Telegram InlineQueryResultsButton.""" + +from typing import TYPE_CHECKING, Final, Optional + +from telegram import constants +from telegram._telegramobject import TelegramObject +from telegram._utils.types import JSONDict +from telegram._webappinfo import WebAppInfo + +if TYPE_CHECKING: + from telegram import Bot + + +class InlineQueryResultsButton(TelegramObject): + """This object represents a button to be shown above inline query results. You **must** use + exactly one of the optional fields. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`text`, :attr:`web_app` and :attr:`start_parameter` are equal. + + Args: + text (:obj:`str`): Label text on the button. + 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 switch back to the inline mode + using the method + `switchInlineQuery `_ + inside the Web App. + start_parameter (:obj:`str`, optional): Deep-linking parameter for the + :guilabel:`/start` message sent to the bot when user presses the switch button. + :tg-const:`telegram.InlineQuery.MIN_SWITCH_PM_TEXT_LENGTH`- + :tg-const:`telegram.InlineQuery.MAX_SWITCH_PM_TEXT_LENGTH` characters, + only ``A-Z``, ``a-z``, ``0-9``, ``_`` and ``-`` are allowed. + + Example: + An inline bot that sends YouTube videos can ask the user to connect the bot to + their YouTube account to adapt search results accordingly. To do this, it displays + a 'Connect your YouTube account' button above the results, or even before showing + any. The user presses the button, switches to a private chat with the bot and, in + doing so, passes a start parameter that instructs the bot to return an OAuth link. + Once done, the bot can offer a switch_inline button so that the user can easily + return to the chat where they wanted to use the bot's inline capabilities. + + Attributes: + text (:obj:`str`): Label text on the button. + 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 switch back to the inline mode + using the method ``web_app_switch_inline_query`` inside the Web App. + start_parameter (:obj:`str`): Optional. Deep-linking parameter for the + :guilabel:`/start` message sent to the bot when user presses the switch button. + :tg-const:`telegram.InlineQuery.MIN_SWITCH_PM_TEXT_LENGTH`- + :tg-const:`telegram.InlineQuery.MAX_SWITCH_PM_TEXT_LENGTH` characters, + only ``A-Z``, ``a-z``, ``0-9``, ``_`` and ``-`` are allowed. + + """ + + __slots__ = ("start_parameter", "text", "web_app") + + def __init__( + self, + text: str, + web_app: Optional[WebAppInfo] = None, + start_parameter: Optional[str] = None, + *, + api_kwargs: Optional[JSONDict] = None, + ): + super().__init__(api_kwargs=api_kwargs) + + # Required + self.text: str = text + + # Optional + self.web_app: Optional[WebAppInfo] = web_app + self.start_parameter: Optional[str] = start_parameter + + self._id_attrs = (self.text, self.web_app, self.start_parameter) + + self._freeze() + + @classmethod + def de_json( + cls, data: Optional[JSONDict], bot: Optional["Bot"] = None + ) -> Optional["InlineQueryResultsButton"]: + """See :meth:`telegram.TelegramObject.de_json`.""" + if not data: + return None + + data["web_app"] = WebAppInfo.de_json(data.get("web_app"), bot) + + return super().de_json(data=data, bot=bot) + + MIN_START_PARAMETER_LENGTH: Final[int] = ( + constants.InlineQueryResultsButtonLimit.MIN_START_PARAMETER_LENGTH + ) + """:const:`telegram.constants.InlineQueryResultsButtonLimit.MIN_START_PARAMETER_LENGTH`""" + MAX_START_PARAMETER_LENGTH: Final[int] = ( + constants.InlineQueryResultsButtonLimit.MAX_START_PARAMETER_LENGTH + ) + """:const:`telegram.constants.InlineQueryResultsButtonLimit.MAX_START_PARAMETER_LENGTH`""" diff --git a/inlinequeryresultvenue.py b/inlinequeryresultvenue.py new file mode 100644 index 0000000000000000000000000000000000000000..60af4024f86e4c88256a889815bbb07ff2efde57 --- /dev/null +++ b/inlinequeryresultvenue.py @@ -0,0 +1,160 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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 the classes that represent Telegram InlineQueryResultVenue.""" + +from typing import TYPE_CHECKING, Optional + +from telegram._inline.inlinekeyboardmarkup import InlineKeyboardMarkup +from telegram._inline.inlinequeryresult import InlineQueryResult +from telegram._utils.types import JSONDict +from telegram.constants import InlineQueryResultType + +if TYPE_CHECKING: + from telegram import InputMessageContent + + +class InlineQueryResultVenue(InlineQueryResult): + """ + Represents a venue. By default, the venue will be sent by the user. Alternatively, you can + use :attr:`input_message_content` to send a message with the specified content instead of the + venue. + + Note: + Foursquare details and Google Pace details are mutually exclusive. However, this + behaviour is undocumented and might be changed by Telegram. + + .. versionchanged:: 20.5 + |removed_thumb_wildcard_note| + + Args: + id (:obj:`str`): Unique identifier for this result, + :tg-const:`telegram.InlineQueryResult.MIN_ID_LENGTH`- + :tg-const:`telegram.InlineQueryResult.MAX_ID_LENGTH` Bytes. + latitude (:obj:`float`): Latitude of the venue location in degrees. + longitude (:obj:`float`): Longitude of the venue location in degrees. + title (:obj:`str`): Title of the venue. + address (:obj:`str`): Address of the venue. + foursquare_id (:obj:`str`, optional): Foursquare identifier of the venue if known. + foursquare_type (:obj:`str`, optional): Foursquare type of the venue, if known. + (For example, "arts_entertainment/default", "arts_entertainment/aquarium" or + "food/icecream".) + google_place_id (:obj:`str`, optional): Google Places identifier of the venue. + google_place_type (:obj:`str`, optional): Google Places type of the venue. (See + `supported types `_.) + reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): Inline keyboard attached + to the message. + input_message_content (:class:`telegram.InputMessageContent`, optional): Content of the + message to be sent instead of the venue. + thumbnail_url (:obj:`str`, optional): Url of the thumbnail for the result. + + .. versionadded:: 20.2 + thumbnail_width (:obj:`int`, optional): Thumbnail width. + + .. versionadded:: 20.2 + thumbnail_height (:obj:`int`, optional): Thumbnail height. + + .. versionadded:: 20.2 + + Attributes: + type (:obj:`str`): :tg-const:`telegram.constants.InlineQueryResultType.VENUE`. + id (:obj:`str`): Unique identifier for this result, + :tg-const:`telegram.InlineQueryResult.MIN_ID_LENGTH`- + :tg-const:`telegram.InlineQueryResult.MAX_ID_LENGTH` Bytes. + latitude (:obj:`float`): Latitude of the venue location in degrees. + longitude (:obj:`float`): Longitude of the venue location in degrees. + title (:obj:`str`): Title of the venue. + address (:obj:`str`): Address of the venue. + foursquare_id (:obj:`str`): Optional. Foursquare identifier of the venue if known. + foursquare_type (:obj:`str`): Optional. Foursquare type of the venue, if known. + (For example, "arts_entertainment/default", "arts_entertainment/aquarium" or + "food/icecream".) + google_place_id (:obj:`str`): Optional. Google Places identifier of the venue. + google_place_type (:obj:`str`): Optional. Google Places type of the venue. (See + `supported types `_.) + reply_markup (:class:`telegram.InlineKeyboardMarkup`): Optional. Inline keyboard attached + to the message. + input_message_content (:class:`telegram.InputMessageContent`): Optional. Content of the + message to be sent instead of the venue. + thumbnail_url (:obj:`str`): Optional. Url of the thumbnail for the result. + + .. versionadded:: 20.2 + thumbnail_width (:obj:`int`): Optional. Thumbnail width. + + .. versionadded:: 20.2 + thumbnail_height (:obj:`int`): Optional. Thumbnail height. + + .. versionadded:: 20.2 + + """ + + __slots__ = ( + "address", + "foursquare_id", + "foursquare_type", + "google_place_id", + "google_place_type", + "input_message_content", + "latitude", + "longitude", + "reply_markup", + "thumbnail_height", + "thumbnail_url", + "thumbnail_width", + "title", + ) + + def __init__( + self, + id: str, # pylint: disable=redefined-builtin + latitude: float, + longitude: float, + title: str, + address: str, + foursquare_id: Optional[str] = None, + foursquare_type: Optional[str] = None, + reply_markup: Optional[InlineKeyboardMarkup] = None, + input_message_content: Optional["InputMessageContent"] = None, + google_place_id: Optional[str] = None, + google_place_type: Optional[str] = None, + thumbnail_url: Optional[str] = None, + thumbnail_width: Optional[int] = None, + thumbnail_height: Optional[int] = None, + *, + api_kwargs: Optional[JSONDict] = None, + ): + # Required + super().__init__(InlineQueryResultType.VENUE, id, api_kwargs=api_kwargs) + with self._unfrozen(): + self.latitude: float = latitude + self.longitude: float = longitude + self.title: str = title + self.address: str = address + + # Optional + self.foursquare_id: Optional[str] = foursquare_id + self.foursquare_type: Optional[str] = foursquare_type + self.google_place_id: Optional[str] = google_place_id + self.google_place_type: Optional[str] = google_place_type + self.reply_markup: Optional[InlineKeyboardMarkup] = reply_markup + self.input_message_content: Optional[InputMessageContent] = input_message_content + self.thumbnail_url: Optional[str] = thumbnail_url + self.thumbnail_width: Optional[int] = thumbnail_width + self.thumbnail_height: Optional[int] = thumbnail_height diff --git a/inlinequeryresultvideo.py b/inlinequeryresultvideo.py new file mode 100644 index 0000000000000000000000000000000000000000..bb01c1ac1bd7b83ef5b5388e5a25cbdceb9960e7 --- /dev/null +++ b/inlinequeryresultvideo.py @@ -0,0 +1,181 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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 the classes that represent Telegram InlineQueryResultVideo.""" +from typing import TYPE_CHECKING, Optional, Sequence, Tuple + +from telegram._inline.inlinekeyboardmarkup import InlineKeyboardMarkup +from telegram._inline.inlinequeryresult import InlineQueryResult +from telegram._messageentity import MessageEntity +from telegram._utils.argumentparsing import parse_sequence_arg +from telegram._utils.defaultvalue import DEFAULT_NONE +from telegram._utils.types import JSONDict, ODVInput +from telegram.constants import InlineQueryResultType + +if TYPE_CHECKING: + from telegram import InputMessageContent + + +class InlineQueryResultVideo(InlineQueryResult): + """ + Represents a link to a page containing an embedded video player or a video file. By default, + this video file will be sent by the user with an optional caption. Alternatively, you can use + :attr:`input_message_content` to send a message with the specified content instead of + the video. + + Note: + If an InlineQueryResultVideo message contains an embedded video (e.g., YouTube), you must + replace its content using :attr:`input_message_content`. + + .. seealso:: :wiki:`Working with Files and Media ` + + .. versionchanged:: 20.5 + |removed_thumb_url_note| + + Args: + id (:obj:`str`): Unique identifier for this result, + :tg-const:`telegram.InlineQueryResult.MIN_ID_LENGTH`- + :tg-const:`telegram.InlineQueryResult.MAX_ID_LENGTH` Bytes. + video_url (:obj:`str`): A valid URL for the embedded video player or video file. + mime_type (:obj:`str`): Mime type of the content of video url, "text/html" or "video/mp4". + thumbnail_url (:obj:`str`, optional): URL of the thumbnail (JPEG only) for the video. + + .. versionadded:: 20.2 + + ..versionchanged:: 20.5 + |thumbnail_url_mandatory| + + title (:obj:`str`): Title for the result. + caption (:obj:`str`, optional): Caption of the video to be sent, + 0-:tg-const:`telegram.constants.MessageLimit.CAPTION_LENGTH` characters after entities + parsing. + parse_mode (:obj:`str`, optional): |parse_mode| + caption_entities (Sequence[:class:`telegram.MessageEntity`], optional): |caption_entities| + + .. versionchanged:: 20.0 + |sequenceclassargs| + + video_width (:obj:`int`, optional): Video width. + video_height (:obj:`int`, optional): Video height. + video_duration (:obj:`int`, optional): Video duration in seconds. + description (:obj:`str`, optional): Short description of the result. + reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): Inline keyboard attached + to the message. + input_message_content (:class:`telegram.InputMessageContent`, optional): Content of the + message to be sent instead of the video. This field is required if + ``InlineQueryResultVideo`` is used to send an HTML-page as a result + (e.g., a YouTube video). + show_caption_above_media (:obj:`bool`, optional): Pass |show_cap_above_med| + + .. versionadded:: 21.3 + + Attributes: + type (:obj:`str`): :tg-const:`telegram.constants.InlineQueryResultType.VIDEO`. + id (:obj:`str`): Unique identifier for this result, + :tg-const:`telegram.InlineQueryResult.MIN_ID_LENGTH`- + :tg-const:`telegram.InlineQueryResult.MAX_ID_LENGTH` Bytes. + video_url (:obj:`str`): A valid URL for the embedded video player or video file. + mime_type (:obj:`str`): Mime type of the content of video url, "text/html" or "video/mp4". + thumbnail_url (:obj:`str`): URL of the thumbnail (JPEG only) for the video. + + .. versionadded:: 20.2 + title (:obj:`str`): Title for the result. + caption (:obj:`str`): Optional. Caption of the video to be sent, + 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| + + .. versionchanged:: 20.0 + + * |tupleclassattrs| + * |alwaystuple| + + video_width (:obj:`int`): Optional. Video width. + video_height (:obj:`int`): Optional. Video height. + video_duration (:obj:`int`): Optional. Video duration in seconds. + description (:obj:`str`): Optional. Short description of the result. + reply_markup (:class:`telegram.InlineKeyboardMarkup`): Optional. Inline keyboard attached + to the message. + input_message_content (:class:`telegram.InputMessageContent`): Optional. Content of the + message to be sent instead of the video. This field is required if + ``InlineQueryResultVideo`` is used to send an HTML-page as a result + (e.g., a YouTube video). + show_caption_above_media (:obj:`bool`): Optional. |show_cap_above_med| + + .. versionadded:: 21.3 + + """ + + __slots__ = ( + "caption", + "caption_entities", + "description", + "input_message_content", + "mime_type", + "parse_mode", + "reply_markup", + "show_caption_above_media", + "thumbnail_url", + "title", + "video_duration", + "video_height", + "video_url", + "video_width", + ) + + def __init__( + self, + id: str, # pylint: disable=redefined-builtin + video_url: str, + mime_type: str, + thumbnail_url: str, + title: str, + caption: Optional[str] = None, + video_width: Optional[int] = None, + video_height: Optional[int] = None, + video_duration: Optional[int] = None, + description: Optional[str] = None, + reply_markup: Optional[InlineKeyboardMarkup] = None, + input_message_content: Optional["InputMessageContent"] = None, + parse_mode: ODVInput[str] = DEFAULT_NONE, + caption_entities: Optional[Sequence[MessageEntity]] = None, + show_caption_above_media: Optional[bool] = None, + *, + api_kwargs: Optional[JSONDict] = None, + ): + # Required + super().__init__(InlineQueryResultType.VIDEO, id, api_kwargs=api_kwargs) + with self._unfrozen(): + self.video_url: str = video_url + self.mime_type: str = mime_type + self.thumbnail_url: str = thumbnail_url + self.title: str = title + + # Optional + self.caption: Optional[str] = caption + self.parse_mode: ODVInput[str] = parse_mode + self.caption_entities: Tuple[MessageEntity, ...] = parse_sequence_arg(caption_entities) + self.video_width: Optional[int] = video_width + self.video_height: Optional[int] = video_height + self.video_duration: Optional[int] = video_duration + self.description: Optional[str] = description + 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/inlinequeryresultvoice.py b/inlinequeryresultvoice.py new file mode 100644 index 0000000000000000000000000000000000000000..d33f31b34d86e6e78764a27178fa6de22c2a7314 --- /dev/null +++ b/inlinequeryresultvoice.py @@ -0,0 +1,126 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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 the classes that represent Telegram InlineQueryResultVoice.""" +from typing import TYPE_CHECKING, Optional, Sequence, Tuple + +from telegram._inline.inlinekeyboardmarkup import InlineKeyboardMarkup +from telegram._inline.inlinequeryresult import InlineQueryResult +from telegram._messageentity import MessageEntity +from telegram._utils.argumentparsing import parse_sequence_arg +from telegram._utils.defaultvalue import DEFAULT_NONE +from telegram._utils.types import JSONDict, ODVInput +from telegram.constants import InlineQueryResultType + +if TYPE_CHECKING: + from telegram import InputMessageContent + + +class InlineQueryResultVoice(InlineQueryResult): + """ + Represents a link to a voice recording in an .ogg container encoded with OPUS. By default, + this voice recording will be sent by the user. Alternatively, you can use + :attr:`input_message_content` to send a message with the specified content instead of + the voice message. + + .. seealso:: :wiki:`Working with Files and Media ` + + Args: + id (:obj:`str`): Unique identifier for this result, + :tg-const:`telegram.InlineQueryResult.MIN_ID_LENGTH`- + :tg-const:`telegram.InlineQueryResult.MAX_ID_LENGTH` Bytes. + voice_url (:obj:`str`): A valid URL for the voice recording. + title (:obj:`str`): Recording title. + caption (:obj:`str`, optional): Caption, + 0-:tg-const:`telegram.constants.MessageLimit.CAPTION_LENGTH` characters after entities + parsing. + parse_mode (:obj:`str`, optional): |parse_mode| + caption_entities (Sequence[:class:`telegram.MessageEntity`], optional): |caption_entities| + + .. versionchanged:: 20.0 + |sequenceclassargs| + + voice_duration (:obj:`int`, optional): Recording duration in seconds. + reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): Inline keyboard attached + to the message. + input_message_content (:class:`telegram.InputMessageContent`, optional): Content of the + message to be sent instead of the voice recording. + + Attributes: + type (:obj:`str`): :tg-const:`telegram.constants.InlineQueryResultType.VOICE`. + id (:obj:`str`): Unique identifier for this result, + :tg-const:`telegram.InlineQueryResult.MIN_ID_LENGTH`- + :tg-const:`telegram.InlineQueryResult.MAX_ID_LENGTH` Bytes. + voice_url (:obj:`str`): A valid URL for the voice recording. + title (:obj:`str`): Recording title. + caption (:obj:`str`): Optional. Caption, + 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| + + .. versionchanged:: 20.0 + + * |tupleclassattrs| + * |alwaystuple| + voice_duration (:obj:`int`): Optional. Recording duration in seconds. + reply_markup (:class:`telegram.InlineKeyboardMarkup`): Optional. Inline keyboard attached + to the message. + input_message_content (:class:`telegram.InputMessageContent`): Optional. Content of the + message to be sent instead of the voice recording. + + """ + + __slots__ = ( + "caption", + "caption_entities", + "input_message_content", + "parse_mode", + "reply_markup", + "title", + "voice_duration", + "voice_url", + ) + + def __init__( + self, + id: str, # pylint: disable=redefined-builtin + voice_url: str, + title: str, + voice_duration: Optional[int] = None, + caption: Optional[str] = None, + reply_markup: Optional[InlineKeyboardMarkup] = None, + input_message_content: Optional["InputMessageContent"] = None, + parse_mode: ODVInput[str] = DEFAULT_NONE, + caption_entities: Optional[Sequence[MessageEntity]] = None, + *, + api_kwargs: Optional[JSONDict] = None, + ): + # Required + super().__init__(InlineQueryResultType.VOICE, id, api_kwargs=api_kwargs) + with self._unfrozen(): + self.voice_url: str = voice_url + self.title: str = title + + # Optional + self.voice_duration: Optional[int] = voice_duration + self.caption: Optional[str] = caption + self.parse_mode: ODVInput[str] = parse_mode + 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/inputcontactmessagecontent.py b/inputcontactmessagecontent.py new file mode 100644 index 0000000000000000000000000000000000000000..4060232bbed5e4d41fc97993ca8bb6938cb01527 --- /dev/null +++ b/inputcontactmessagecontent.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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 the classes that represent Telegram InputContactMessageContent.""" +from typing import Optional + +from telegram._inline.inputmessagecontent import InputMessageContent +from telegram._utils.types import JSONDict + + +class InputContactMessageContent(InputMessageContent): + """Represents the content of a contact message to be sent as the result of an inline query. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`phone_number` is equal. + + Args: + phone_number (:obj:`str`): Contact's phone number. + first_name (:obj:`str`): Contact's first name. + last_name (:obj:`str`, optional): Contact's last name. + vcard (:obj:`str`, optional): Additional data about the contact in the form of a vCard, + 0-:tg-const:`telegram.constants.ContactLimit.VCARD` bytes. + + Attributes: + phone_number (:obj:`str`): Contact's phone number. + first_name (:obj:`str`): Contact's first name. + last_name (:obj:`str`): Optional. Contact's last name. + vcard (:obj:`str`): Optional. Additional data about the contact in the form of a vCard, + 0-:tg-const:`telegram.constants.ContactLimit.VCARD` bytes. + + """ + + __slots__ = ("first_name", "last_name", "phone_number", "vcard") + + def __init__( + self, + phone_number: str, + first_name: str, + last_name: Optional[str] = None, + vcard: Optional[str] = None, + *, + api_kwargs: Optional[JSONDict] = None, + ): + super().__init__(api_kwargs=api_kwargs) + with self._unfrozen(): + # Required + self.phone_number: str = phone_number + self.first_name: str = first_name + # Optionals + self.last_name: Optional[str] = last_name + self.vcard: Optional[str] = vcard + + self._id_attrs = (self.phone_number,) diff --git a/inputinvoicemessagecontent.py b/inputinvoicemessagecontent.py new file mode 100644 index 0000000000000000000000000000000000000000..101e0184b5730b00e39d2d18e8542f6797e147fb --- /dev/null +++ b/inputinvoicemessagecontent.py @@ -0,0 +1,267 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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 a class that represents a Telegram InputInvoiceMessageContent.""" +from typing import TYPE_CHECKING, Optional, Sequence, Tuple + +from telegram._inline.inputmessagecontent import InputMessageContent +from telegram._payment.labeledprice import LabeledPrice +from telegram._utils.argumentparsing import parse_sequence_arg +from telegram._utils.types import JSONDict + +if TYPE_CHECKING: + from telegram import Bot + + +class InputInvoiceMessageContent(InputMessageContent): + """ + Represents the content of a invoice message to be sent as the result of an inline query. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`title`, :attr:`description`, :attr:`payload`, + :attr:`provider_token`, :attr:`currency` and :attr:`prices` are equal. + + .. versionadded:: 13.5 + + Args: + 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. + :tg-const:`telegram.Invoice.MIN_DESCRIPTION_LENGTH`- + :tg-const:`telegram.Invoice.MAX_DESCRIPTION_LENGTH` characters. + 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 it for your internal processes. + provider_token (:obj:`str`): Payment 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. + currency (:obj:`str`): Three-letter ISO 4217 currency code, see more on + `currencies `_. + Pass ``XTR`` for payments in |tg_stars|. + prices (Sequence[:class:`telegram.LabeledPrice`]): Price breakdown, a list of + components (e.g. product price, tax, discount, delivery cost, delivery tax, bonus, + etc.). Must contain exactly one item for payments in |tg_stars|. + + .. versionchanged:: 20.0 + |sequenceclassargs| + + 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`` parameter in + `currencies.json `_, it + shows the number of digits past the decimal point for each currency (2 for the majority + of currencies). Defaults to ``0``. Not supported for payments in |tg_stars|. + suggested_tip_amounts (Sequence[:obj:`int`], optional): An array of suggested + amounts of tip in the *smallest units* of the currency (integer, **not** float/double). + At most 4 suggested tip amounts can be specified. The suggested tip amounts must be + positive, passed in a strictly increased order and must not exceed + :attr:`max_tip_amount`. + + .. versionchanged:: 20.0 + + * |tupleclassattrs| + * |alwaystuple| + + provider_data (:obj:`str`, optional): An object for data about the invoice, + which will be shared with the payment provider. A detailed description of the required + fields should be provided by the payment provider. + photo_url (:obj:`str`, optional): URL of the product photo for the invoice. Can be a photo + of the goods or a marketing image for a service. People like it better when they see + what they are paying for. + photo_size (:obj:`int`, optional): Photo size. + photo_width (:obj:`int`, optional): Photo width. + photo_height (:obj:`int`, optional): Photo height. + need_name (:obj:`bool`, optional): Pass :obj:`True`, if you require the user's full + name to complete the order. Ignored for payments in |tg_stars|. + need_phone_number (:obj:`bool`, optional): Pass :obj:`True`, if you require the user's + phone number to complete the order. Ignored for payments in |tg_stars|. + need_email (:obj:`bool`, optional): Pass :obj:`True`, if you require the user's email + address to complete the order. Ignored for payments in |tg_stars|. + need_shipping_address (:obj:`bool`, optional): Pass :obj:`True`, if you require the + user's shipping address to complete the order. Ignored for payments in |tg_stars| + send_phone_number_to_provider (:obj:`bool`, optional): Pass :obj:`True`, if user's + phone number should be sent to provider. Ignored for payments in |tg_stars|. + send_email_to_provider (:obj:`bool`, optional): Pass :obj:`True`, if user's email + address should be sent to provider. Ignored for payments in |tg_stars|. + is_flexible (:obj:`bool`, optional): Pass :obj:`True`, if the final price depends on + the shipping method. Ignored for payments in |tg_stars|. + + Attributes: + 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. + :tg-const:`telegram.Invoice.MIN_DESCRIPTION_LENGTH`- + :tg-const:`telegram.Invoice.MAX_DESCRIPTION_LENGTH` characters. + 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 it for your internal processes. + provider_token (:obj:`str`): Payment provider token, obtained via + `@Botfather `_. Pass an empty string for payments in `Telegram + Stars `_. + currency (:obj:`str`): Three-letter ISO 4217 currency code, see more on + `currencies `_. + Pass ``XTR`` for payments in |tg_stars|. + prices (Tuple[:class:`telegram.LabeledPrice`]): Price breakdown, a list of + components (e.g. product price, tax, discount, delivery cost, delivery tax, bonus, + etc.). Must contain exactly one item for payments in |tg_stars|. + + .. versionchanged:: 20.0 + |tupleclassattrs| + + 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`` ``max_tip_amount`` is ``145``. See the ``exp`` parameter in + `currencies.json `_, it + shows the number of digits past the decimal point for each currency (2 for the majority + of currencies). Defaults to ``0``. Not supported for payments in |tg_stars|. + suggested_tip_amounts (Tuple[:obj:`int`]): Optional. An array of suggested + amounts of tip in the *smallest units* of the currency (integer, **not** float/double). + At most 4 suggested tip amounts can be specified. The suggested tip amounts must be + positive, passed in a strictly increased order and must not exceed + :attr:`max_tip_amount`. + + .. versionchanged:: 20.0 + |tupleclassattrs| + + provider_data (:obj:`str`): Optional. An object for data about the invoice, + which will be shared with the payment provider. A detailed description of the required + fields should be provided by the payment provider. + photo_url (:obj:`str`): Optional. URL of the product photo for the invoice. Can be a photo + of the goods or a marketing image for a service. People like it better when they see + what they are paying for. + photo_size (:obj:`int`): Optional. Photo size. + photo_width (:obj:`int`): Optional. Photo width. + photo_height (:obj:`int`): Optional. Photo height. + need_name (:obj:`bool`): Optional. Pass :obj:`True`, if you require the user's full name to + complete the order. Ignored for payments in |tg_stars|. + need_phone_number (:obj:`bool`): Optional. Pass :obj:`True`, if you require the user's + phone number to complete the order. Ignored for payments in |tg_stars|. + need_email (:obj:`bool`): Optional. Pass :obj:`True`, if you require the user's email + address to complete the order. Ignored for payments in |tg_stars|. + need_shipping_address (:obj:`bool`): Optional. Pass :obj:`True`, if you require the user's + shipping address to complete the order. Ignored for payments in |tg_stars|. + send_phone_number_to_provider (:obj:`bool`): Optional. Pass :obj:`True`, if user's phone + number should be sent to provider. Ignored for payments in |tg_stars|. + send_email_to_provider (:obj:`bool`): Optional. Pass :obj:`True`, if user's email address + should be sent to provider. Ignored for payments in |tg_stars|. + is_flexible (:obj:`bool`): Optional. Pass :obj:`True`, if the final price depends on the + shipping method. Ignored for payments in |tg_stars|. + + """ + + __slots__ = ( + "currency", + "description", + "is_flexible", + "max_tip_amount", + "need_email", + "need_name", + "need_phone_number", + "need_shipping_address", + "payload", + "photo_height", + "photo_size", + "photo_url", + "photo_width", + "prices", + "provider_data", + "provider_token", + "send_email_to_provider", + "send_phone_number_to_provider", + "suggested_tip_amounts", + "title", + ) + + def __init__( + self, + title: str, + description: str, + payload: str, + provider_token: Optional[str], # This arg is now optional since Bot API 7.4 + currency: str, + prices: Sequence[LabeledPrice], + max_tip_amount: Optional[int] = None, + suggested_tip_amounts: Optional[Sequence[int]] = None, + provider_data: Optional[str] = None, + photo_url: Optional[str] = None, + photo_size: Optional[int] = None, + photo_width: Optional[int] = None, + photo_height: Optional[int] = None, + need_name: Optional[bool] = None, + need_phone_number: Optional[bool] = None, + need_email: Optional[bool] = None, + need_shipping_address: Optional[bool] = None, + send_phone_number_to_provider: Optional[bool] = None, + send_email_to_provider: Optional[bool] = None, + is_flexible: Optional[bool] = None, + *, + api_kwargs: Optional[JSONDict] = None, + ): + super().__init__(api_kwargs=api_kwargs) + with self._unfrozen(): + # Required + self.title: str = title + self.description: str = description + self.payload: str = payload + self.provider_token: Optional[str] = provider_token + self.currency: str = currency + self.prices: Tuple[LabeledPrice, ...] = parse_sequence_arg(prices) + # Optionals + self.max_tip_amount: Optional[int] = max_tip_amount + self.suggested_tip_amounts: Tuple[int, ...] = parse_sequence_arg(suggested_tip_amounts) + self.provider_data: Optional[str] = provider_data + self.photo_url: Optional[str] = photo_url + self.photo_size: Optional[int] = photo_size + self.photo_width: Optional[int] = photo_width + self.photo_height: Optional[int] = photo_height + self.need_name: Optional[bool] = need_name + self.need_phone_number: Optional[bool] = need_phone_number + self.need_email: Optional[bool] = need_email + self.need_shipping_address: Optional[bool] = need_shipping_address + self.send_phone_number_to_provider: Optional[bool] = send_phone_number_to_provider + self.send_email_to_provider: Optional[bool] = send_email_to_provider + self.is_flexible: Optional[bool] = is_flexible + + self._id_attrs = ( + self.title, + self.description, + self.payload, + self.provider_token, + self.currency, + self.prices, + ) + + @classmethod + def de_json( + cls, data: Optional[JSONDict], bot: Optional["Bot"] = None + ) -> Optional["InputInvoiceMessageContent"]: + """See :meth:`telegram.TelegramObject.de_json`.""" + data = cls._parse_data(data) + + if not data: + return None + + data["prices"] = LabeledPrice.de_list(data.get("prices"), bot) + + return super().de_json(data=data, bot=bot) diff --git a/inputlocationmessagecontent.py b/inputlocationmessagecontent.py new file mode 100644 index 0000000000000000000000000000000000000000..d9642c485c5f21716e8749a797aeaa5131390e20 --- /dev/null +++ b/inputlocationmessagecontent.py @@ -0,0 +1,150 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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 the classes that represent Telegram InputLocationMessageContent.""" + +from typing import Final, Optional + +from telegram import constants +from telegram._inline.inputmessagecontent import InputMessageContent +from telegram._utils.types import JSONDict + + +class InputLocationMessageContent(InputMessageContent): + # fmt: off + """ + Represents the content of a location message to be sent as the result of an inline query. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`latitude` and :attr:`longitude` are equal. + + Args: + latitude (:obj:`float`): Latitude of the location in degrees. + longitude (:obj:`float`): Longitude of the location in degrees. + horizontal_accuracy (:obj:`float`, optional): The radius of uncertainty for the location, + measured in meters; 0- + :tg-const:`telegram.InputLocationMessageContent.HORIZONTAL_ACCURACY`. + live_period (:obj:`int`, optional): Period in seconds for which the location will be + updated, should be between + :tg-const:`telegram.InputLocationMessageContent.MIN_LIVE_PERIOD` and + :tg-const:`telegram.InputLocationMessageContent.MAX_LIVE_PERIOD` or + :tg-const:`telegram.constants.LocationLimit.LIVE_PERIOD_FOREVER` for live + locations that can be edited indefinitely. + heading (:obj:`int`, optional): For live locations, a direction in which the user is + moving, in degrees. Must be between + :tg-const:`telegram.InputLocationMessageContent.MIN_HEADING` and + :tg-const:`telegram.InputLocationMessageContent.MAX_HEADING` if specified. + proximity_alert_radius (:obj:`int`, optional): For live locations, a maximum distance + for proximity alerts about approaching another chat member, in meters. Must be + between :tg-const:`telegram.InputLocationMessageContent.MIN_PROXIMITY_ALERT_RADIUS` + and :tg-const:`telegram.InputLocationMessageContent.MAX_PROXIMITY_ALERT_RADIUS` + if specified. + + Attributes: + latitude (:obj:`float`): Latitude of the location in degrees. + longitude (:obj:`float`): Longitude of the location in degrees. + horizontal_accuracy (:obj:`float`): Optional. The radius of uncertainty for the location, + measured in meters; 0- + :tg-const:`telegram.InputLocationMessageContent.HORIZONTAL_ACCURACY`. + live_period (:obj:`int`): Optional. Period in seconds for which the location can be + updated, should be between + :tg-const:`telegram.InputLocationMessageContent.MIN_LIVE_PERIOD` and + :tg-const:`telegram.InputLocationMessageContent.MAX_LIVE_PERIOD`. + heading (:obj:`int`): Optional. For live locations, a direction in which the user is + moving, in degrees. Must be between + :tg-const:`telegram.InputLocationMessageContent.MIN_HEADING` and + :tg-const:`telegram.InputLocationMessageContent.MAX_HEADING` if specified. + proximity_alert_radius (:obj:`int`): Optional. For live locations, a maximum distance + for proximity alerts about approaching another chat member, in meters. Must be + between :tg-const:`telegram.InputLocationMessageContent.MIN_PROXIMITY_ALERT_RADIUS` + and :tg-const:`telegram.InputLocationMessageContent.MAX_PROXIMITY_ALERT_RADIUS` + if specified. + + """ + + __slots__ = ( + "heading", + "horizontal_accuracy", + "latitude", + "live_period", + "longitude", + "proximity_alert_radius") + # fmt: on + + def __init__( + self, + latitude: float, + longitude: float, + live_period: Optional[int] = None, + horizontal_accuracy: Optional[float] = None, + heading: Optional[int] = None, + proximity_alert_radius: Optional[int] = None, + *, + api_kwargs: Optional[JSONDict] = None, + ): + super().__init__(api_kwargs=api_kwargs) + with self._unfrozen(): + # Required + self.latitude: float = latitude + self.longitude: float = longitude + + # Optionals + self.live_period: Optional[int] = live_period + self.horizontal_accuracy: Optional[float] = horizontal_accuracy + self.heading: Optional[int] = heading + self.proximity_alert_radius: Optional[int] = ( + int(proximity_alert_radius) if proximity_alert_radius else None + ) + + self._id_attrs = (self.latitude, self.longitude) + + HORIZONTAL_ACCURACY: Final[int] = constants.LocationLimit.HORIZONTAL_ACCURACY + """:const:`telegram.constants.LocationLimit.HORIZONTAL_ACCURACY` + + .. versionadded:: 20.0 + """ + MIN_HEADING: Final[int] = constants.LocationLimit.MIN_HEADING + """:const:`telegram.constants.LocationLimit.MIN_HEADING` + + .. versionadded:: 20.0 + """ + MAX_HEADING: Final[int] = constants.LocationLimit.MAX_HEADING + """:const:`telegram.constants.LocationLimit.MAX_HEADING` + + .. versionadded:: 20.0 + """ + MIN_LIVE_PERIOD: Final[int] = constants.LocationLimit.MIN_LIVE_PERIOD + """:const:`telegram.constants.LocationLimit.MIN_LIVE_PERIOD` + + .. versionadded:: 20.0 + """ + MAX_LIVE_PERIOD: Final[int] = constants.LocationLimit.MAX_LIVE_PERIOD + """:const:`telegram.constants.LocationLimit.MAX_LIVE_PERIOD` + + .. versionadded:: 20.0 + """ + MIN_PROXIMITY_ALERT_RADIUS: Final[int] = constants.LocationLimit.MIN_PROXIMITY_ALERT_RADIUS + """:const:`telegram.constants.LocationLimit.MIN_PROXIMITY_ALERT_RADIUS` + + .. versionadded:: 20.0 + """ + MAX_PROXIMITY_ALERT_RADIUS: Final[int] = constants.LocationLimit.MAX_PROXIMITY_ALERT_RADIUS + """:const:`telegram.constants.LocationLimit.MAX_PROXIMITY_ALERT_RADIUS` + + .. versionadded:: 20.0 + """ diff --git a/inputmessagecontent.py b/inputmessagecontent.py new file mode 100644 index 0000000000000000000000000000000000000000..40088f5a4392f0319fa861ff03468c844dab8b7f --- /dev/null +++ b/inputmessagecontent.py @@ -0,0 +1,42 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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 the classes that represent Telegram InputMessageContent.""" + +from typing import Optional + +from telegram._telegramobject import TelegramObject +from telegram._utils.types import JSONDict + + +class InputMessageContent(TelegramObject): + """Base class for Telegram InputMessageContent Objects. + + See: :class:`telegram.InputContactMessageContent`, + :class:`telegram.InputInvoiceMessageContent`, + :class:`telegram.InputLocationMessageContent`, :class:`telegram.InputTextMessageContent` and + :class:`telegram.InputVenueMessageContent` for more details. + + """ + + __slots__ = () + + def __init__(self, *, api_kwargs: Optional[JSONDict] = None) -> None: + super().__init__(api_kwargs=api_kwargs) + + self._freeze() diff --git a/inputtextmessagecontent.py b/inputtextmessagecontent.py new file mode 100644 index 0000000000000000000000000000000000000000..475f9c5bb28ee3748f112edc06c0a784bf5d501d --- /dev/null +++ b/inputtextmessagecontent.py @@ -0,0 +1,115 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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 the classes that represent Telegram InputTextMessageContent.""" +from typing import TYPE_CHECKING, Optional, Sequence, Tuple + +from telegram._inline.inputmessagecontent import InputMessageContent +from telegram._messageentity import MessageEntity +from telegram._utils.argumentparsing import parse_lpo_and_dwpp, parse_sequence_arg +from telegram._utils.defaultvalue import DEFAULT_NONE +from telegram._utils.types import JSONDict, ODVInput + +if TYPE_CHECKING: + from telegram._linkpreviewoptions import LinkPreviewOptions + + +class InputTextMessageContent(InputMessageContent): + """ + Represents the content of a text message to be sent as the result of an inline query. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`message_text` is equal. + + Examples: + :any:`Inline Bot ` + + Args: + message_text (:obj:`str`): Text of the message to be sent, + :tg-const:`telegram.constants.MessageLimit.MIN_TEXT_LENGTH`- + :tg-const:`telegram.constants.MessageLimit.MAX_TEXT_LENGTH` characters after entities + parsing. + parse_mode (:obj:`str`, optional): |parse_mode| + entities (Sequence[:class:`telegram.MessageEntity`], optional): |caption_entities| + + .. versionchanged:: 20.0 + |sequenceclassargs| + + link_preview_options (:obj:`LinkPreviewOptions`, optional): Link preview generation + options for the message. Mutually exclusive with + :paramref:`disable_web_page_preview`. + + .. versionadded:: 20.8 + + Keyword Args: + disable_web_page_preview (:obj:`bool`, optional): Disables link previews for links in the + sent message. Convenience parameter for setting :paramref:`link_preview_options`. + Mutually exclusive with :paramref:`link_preview_options`. + + .. versionchanged:: 20.8 + Bot API 7.0 introduced :paramref:`link_preview_options` replacing this + argument. PTB will automatically convert this argument to that one, but + for advanced options, please use :paramref:`link_preview_options` directly. + + .. versionchanged:: 21.0 + |keyword_only_arg| + + Attributes: + message_text (:obj:`str`): Text of the message to be sent, + :tg-const:`telegram.constants.MessageLimit.MIN_TEXT_LENGTH`- + :tg-const:`telegram.constants.MessageLimit.MAX_TEXT_LENGTH` characters after entities + parsing. + parse_mode (:obj:`str`): Optional. |parse_mode| + entities (Tuple[:class:`telegram.MessageEntity`]): Optional. |captionentitiesattr| + + .. versionchanged:: 20.0 + + * |tupleclassattrs| + * |alwaystuple| + link_preview_options (:obj:`LinkPreviewOptions`): Optional. Link preview generation + options for the message. + + .. versionadded:: 20.8 + + """ + + __slots__ = ("entities", "link_preview_options", "message_text", "parse_mode") + + def __init__( + self, + message_text: str, + parse_mode: ODVInput[str] = DEFAULT_NONE, + entities: Optional[Sequence[MessageEntity]] = None, + link_preview_options: ODVInput["LinkPreviewOptions"] = DEFAULT_NONE, + *, + disable_web_page_preview: Optional[bool] = None, + api_kwargs: Optional[JSONDict] = None, + ): + super().__init__(api_kwargs=api_kwargs) + + with self._unfrozen(): + # Required + self.message_text: str = message_text + # Optionals + self.parse_mode: ODVInput[str] = parse_mode + self.entities: Tuple[MessageEntity, ...] = parse_sequence_arg(entities) + self.link_preview_options: ODVInput[LinkPreviewOptions] = parse_lpo_and_dwpp( + disable_web_page_preview, link_preview_options + ) + + self._id_attrs = (self.message_text,) diff --git a/inputvenuemessagecontent.py b/inputvenuemessagecontent.py new file mode 100644 index 0000000000000000000000000000000000000000..016969b2256a3bad6ac6d05e330874b45b58e694 --- /dev/null +++ b/inputvenuemessagecontent.py @@ -0,0 +1,108 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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 the classes that represent Telegram InputVenueMessageContent.""" +from typing import Optional + +from telegram._inline.inputmessagecontent import InputMessageContent +from telegram._utils.types import JSONDict + + +class InputVenueMessageContent(InputMessageContent): + """Represents the content of a venue message to be sent as the result of an inline query. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`latitude`, :attr:`longitude` and :attr:`title` + are equal. + + Note: + Foursquare details and Google Pace details are mutually exclusive. However, this + behaviour is undocumented and might be changed by Telegram. + + Args: + latitude (:obj:`float`): Latitude of the location in degrees. + longitude (:obj:`float`): Longitude of the location in degrees. + title (:obj:`str`): Name of the venue. + address (:obj:`str`): Address of the venue. + foursquare_id (:obj:`str`, optional): Foursquare identifier of the venue, if known. + foursquare_type (:obj:`str`, optional): Foursquare type of the venue, if known. + (For example, "arts_entertainment/default", "arts_entertainment/aquarium" or + "food/icecream".) + google_place_id (:obj:`str`, optional): Google Places identifier of the venue. + google_place_type (:obj:`str`, optional): Google Places type of the venue. (See + `supported types `_.) + + Attributes: + latitude (:obj:`float`): Latitude of the location in degrees. + longitude (:obj:`float`): Longitude of the location in degrees. + title (:obj:`str`): Name of the venue. + address (:obj:`str`): Address of the venue. + foursquare_id (:obj:`str`): Optional. Foursquare identifier of the venue, if known. + foursquare_type (:obj:`str`): Optional. Foursquare type of the venue, if known. + (For example, "arts_entertainment/default", "arts_entertainment/aquarium" or + "food/icecream".) + google_place_id (:obj:`str`): Optional. Google Places identifier of the venue. + google_place_type (:obj:`str`): Optional. Google Places type of the venue. (See + `supported types `_.) + + """ + + __slots__ = ( + "address", + "foursquare_id", + "foursquare_type", + "google_place_id", + "google_place_type", + "latitude", + "longitude", + "title", + ) + + def __init__( + self, + latitude: float, + longitude: float, + title: str, + address: str, + foursquare_id: Optional[str] = None, + foursquare_type: Optional[str] = None, + google_place_id: Optional[str] = None, + google_place_type: Optional[str] = None, + *, + api_kwargs: Optional[JSONDict] = None, + ): + super().__init__(api_kwargs=api_kwargs) + with self._unfrozen(): + # Required + self.latitude: float = latitude + self.longitude: float = longitude + self.title: str = title + self.address: str = address + # Optionals + self.foursquare_id: Optional[str] = foursquare_id + self.foursquare_type: Optional[str] = foursquare_type + self.google_place_id: Optional[str] = google_place_id + self.google_place_type: Optional[str] = google_place_type + + self._id_attrs = ( + self.latitude, + self.longitude, + self.title, + ) diff --git a/py.typed b/py.typed new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000000000000000000000000000000000000..80edfde44f80a77a68331373aed43fc3e29bd63d --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,244 @@ +# PACKAGING +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +dynamic = ["version"] +name = "python-telegram-bot" +description = "We have made you a wrapper you can't refuse" +readme = "README.rst" +requires-python = ">=3.8" +license = "LGPL-3.0-only" +license-files = { paths = ["LICENSE", "LICENSE.dual", "LICENSE.lesser"] } +authors = [ + { name = "Leandro Toledo", email = "devs@python-telegram-bot.org" } +] +keywords = [ + "python", + "telegram", + "bot", + "api", + "wrapper", +] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)", + "Operating System :: OS Independent", + "Topic :: Software Development :: Libraries :: Python Modules", + "Topic :: Communications :: Chat", + "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", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", +] +dependencies = [ + "httpx ~= 0.27", +] + +[project.urls] +"Homepage" = "https://python-telegram-bot.org" +"Documentation" = "https://docs.python-telegram-bot.org" +"Bug Tracker" = "https://github.com/python-telegram-bot/python-telegram-bot/issues" +"Source Code" = "https://github.com/python-telegram-bot/python-telegram-bot" +"News" = "https://t.me/pythontelegrambotchannel" +"Changelog" = "https://docs.python-telegram-bot.org/en/stable/changelog.html" +"Support" = "https://t.me/pythontelegrambotgroup" + +[project.optional-dependencies] +# Make sure to install those as additional_dependencies in the +# pre-commit hooks for pylint & mypy +# Also update the readme accordingly +# +# When dependencies release new versions and tests succeed, we should try to expand the allowed +# versions and only increase the lower bound if necessary +# +# When adding new groups, make sure to update `ext` and `all` accordingly + +# Optional dependencies for production +all = [ + "python-telegram-bot[ext,http2,passport,socks]", +] +callback-data = [ + # Cachetools doesn't have a strict stability policy. Let's be cautious for now. + "cachetools>=5.3.3,<5.6.0", +] +ext = [ + "python-telegram-bot[callback-data,job-queue,rate-limiter,webhooks]", +] +http2 = [ + "httpx[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", +] +passport = [ + "cryptography!=3.4,!=3.4.1,!=3.4.2,!=3.4.3,>=39.0.1", + # cffi is a dependency of cryptography and added support for python 3.13 in 1.17.0rc1 + "cffi >= 1.17.0rc1; python_version > '3.12'" +] +rate-limiter = [ + "aiolimiter~=1.1.0", +] +socks = [ + "httpx[socks]", +] +webhooks = [ + # tornado is rather stable, but let's not allow the next major release without prior testing + "tornado~=6.4", +] + + +# HATCH +[tool.hatch.version] +# dynamically evaluates the `__version__` variable in that file +source = "code" +path = "telegram/_version.py" +search-paths = ["telegram"] + +[tool.hatch.build] +packages = ["telegram"] + +# BLACK: +[tool.black] +line-length = 99 + +# ISORT: +[tool.isort] # black config +profile = "black" +line_length = 99 + +# RUFF: +[tool.ruff] +line-length = 99 +show-fixes = true + +[tool.ruff.lint] +preview = true +explicit-preview-rules = true # TODO: Drop this when RUF022 and RUF023 are out of preview +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", "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", "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"] +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" +] + +[tool.pylint.main] +# run pylint across multiple cpu cores to speed it up- +# https://pylint.pycqa.org/en/latest/user_guide/run.html?#parallel-execution to know more +jobs = 0 + +[tool.pylint.classes] +exclude-protected = ["_unfrozen"] + +# PYTEST: +[tool.pytest.ini_options] +testpaths = ["tests"] +addopts = "--no-success-flaky-report -rX" +filterwarnings = [ + "error", + "ignore::DeprecationWarning", + 'ignore:Tasks created via `Application\.create_task` while the application is not running', + "ignore::ResourceWarning", + # TODO: Write so good code that we don't need to ignore ResourceWarnings anymore + # Unfortunately due to https://github.com/pytest-dev/pytest/issues/8343 we can't have this here + # and instead do a trick directly in tests/conftest.py + # ignore::telegram.utils.deprecate.TelegramDeprecationWarning +] +markers = [ + "dev", # If you want to test a specific test, use this + "no_req", + "req", +] +asyncio_mode = "auto" +log_format = "%(funcName)s - Line %(lineno)d - %(message)s" +# log_level = "DEBUG" # uncomment to see DEBUG logs + +# MYPY: +[tool.mypy] +warn_unused_ignores = true +warn_unused_configs = true +disallow_untyped_defs = true +disallow_incomplete_defs = true +disallow_untyped_decorators = true +show_error_codes = true +python_version = "3.8" + +# 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 +# `if self.text is None: raise RuntimeError()` +[[tool.mypy.overrides]] +module = [ + "telegram._callbackquery", + "telegram._file", + "telegram._message", + "telegram._files.file" +] +strict_optional = false + +# type hinting for asyncio in webhookhandler is a bit tricky because it depends on the OS +[[tool.mypy.overrides]] +module = "telegram.ext._utils.webhookhandler" +warn_unused_ignores = false + +# The libs listed below are only used for the `customwebhookbot_*.py` examples +# let's just ignore type checking for them for now +[[tool.mypy.overrides]] +module = [ + "flask.*", + "quart.*", + "starlette.*", + "uvicorn.*", + "asgiref.*", + "django.*", + "apscheduler.*", # not part of `customwebhookbot_*.py` examples +] +ignore_missing_imports = true + +# COVERAGE: +[tool.coverage.run] +branch = true +source = ["telegram"] +parallel = true +concurrency = ["thread", "multiprocessing"] +omit = [ + "tests/", + "telegram/__main__.py" +] + +[tool.coverage.report] +exclude_also = [ + "@overload", + "@abstractmethod", + "if TYPE_CHECKING:" +] diff --git a/requirements-dev-all.txt b/requirements-dev-all.txt new file mode 100644 index 0000000000000000000000000000000000000000..995e067c4206ce5e8fc1fecd9cfd3e9cf8810a0a --- /dev/null +++ b/requirements-dev-all.txt @@ -0,0 +1,5 @@ +-e .[all] +# needed for pre-commit hooks in the git commit command +pre-commit +-r requirements-unit-tests.txt +-r docs/requirements-docs.txt diff --git a/requirements-unit-tests.txt b/requirements-unit-tests.txt new file mode 100644 index 0000000000000000000000000000000000000000..df1e83b45732101d569e699ced1e834f397d9777 --- /dev/null +++ b/requirements-unit-tests.txt @@ -0,0 +1,19 @@ +-e . + +# required for building the wheels for releases +build + +# For the test suite +pytest==8.3.2 + +# needed because pytest doesn't come with native support for coroutines as tests +pytest-asyncio==0.21.2 + +# xdist runs tests in parallel +pytest-xdist==3.6.1 + +# Used for flaky tests (flaky decorator) +flaky>=3.8.1 + +# used in test_official for parsing tg docs +beautifulsoup4 \ No newline at end of file diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000000000000000000000000000000000000..c24e78bc4e1eae7666d466572cf1bea1e6a6642a --- /dev/null +++ b/setup.cfg @@ -0,0 +1,5 @@ +[flake8] +max-line-length = 99 +ignore = W503, W605 +extend-ignore = E203, E704 +exclude = docs/source/conf.py diff --git a/test_birthdate.py b/test_birthdate.py new file mode 100644 index 0000000000000000000000000000000000000000..c22ebd9affd6fabbde02cb640c0f6b66ab3af642 --- /dev/null +++ b/test_birthdate.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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/]. +from datetime import date + +import pytest + +from telegram import Birthdate +from tests.auxil.slots import mro_slots + + +class BirthdateTestBase: + day = 1 + month = 1 + year = 2022 + + +@pytest.fixture(scope="module") +def birthdate(): + return Birthdate(BirthdateTestBase.day, BirthdateTestBase.month, BirthdateTestBase.year) + + +class TestBirthdateWithoutRequest(BirthdateTestBase): + def test_slot_behaviour(self, birthdate): + for attr in birthdate.__slots__: + assert getattr(birthdate, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(birthdate)) == len(set(mro_slots(birthdate))), "duplicate slot" + + def test_to_dict(self, birthdate): + bd_dict = birthdate.to_dict() + assert isinstance(bd_dict, dict) + assert bd_dict["day"] == self.day + assert bd_dict["month"] == self.month + assert bd_dict["year"] == self.year + + def test_de_json(self, bot): + json_dict = {"day": self.day, "month": self.month, "year": self.year} + bd = Birthdate.de_json(json_dict, bot) + assert isinstance(bd, Birthdate) + assert bd.day == self.day + assert bd.month == self.month + assert bd.year == self.year + + def test_equality(self): + bd1 = Birthdate(1, 1, 2022) + bd2 = Birthdate(1, 1, 2022) + bd3 = Birthdate(1, 1, 2023) + bd4 = Birthdate(1, 2, 2022) + + assert bd1 == bd2 + assert hash(bd1) == hash(bd2) + + assert bd1 == bd3 + assert hash(bd1) == hash(bd3) + + assert bd1 != bd4 + assert hash(bd1) != hash(bd4) + + def test_to_date(self, birthdate): + assert isinstance(birthdate.to_date(), date) + assert birthdate.to_date() == date(self.year, self.month, self.day) + new_bd = birthdate.to_date(2023) + assert new_bd == date(2023, self.month, self.day) + + def test_to_date_no_year(self): + bd = Birthdate(1, 1) + with pytest.raises(ValueError, match="The `year` argument is required"): + bd.to_date() diff --git a/test_bot.py b/test_bot.py new file mode 100644 index 0000000000000000000000000000000000000000..705b14c6d4d774b2650daf70ea91217fdf022e0e --- /dev/null +++ b/test_bot.py @@ -0,0 +1,4298 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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/]. +import asyncio +import copy +import datetime as dtm +import inspect +import logging +import pickle +import socket +import time +from collections import defaultdict +from http import HTTPStatus +from io import BytesIO +from typing import Tuple + +import httpx +import pytest + +from telegram import ( + Bot, + BotCommand, + BotCommandScopeChat, + BotDescription, + BotName, + BotShortDescription, + BusinessConnection, + CallbackQuery, + Chat, + ChatAdministratorRights, + ChatFullInfo, + ChatPermissions, + Dice, + InlineKeyboardButton, + InlineKeyboardMarkup, + InlineQueryResultArticle, + InlineQueryResultDocument, + InlineQueryResultsButton, + InlineQueryResultVoice, + InputFile, + InputMediaDocument, + InputMediaPhoto, + InputMessageContent, + InputPollOption, + InputTextMessageContent, + LabeledPrice, + LinkPreviewOptions, + MenuButton, + MenuButtonCommands, + MenuButtonDefault, + MenuButtonWebApp, + Message, + MessageEntity, + Poll, + PollOption, + ReactionTypeCustomEmoji, + ReactionTypeEmoji, + ReplyParameters, + SentWebAppMessage, + ShippingOption, + StarTransaction, + StarTransactions, + Update, + User, + WebAppInfo, +) +from telegram._utils.datetime import UTC, from_timestamp, to_timestamp +from telegram._utils.defaultvalue import DEFAULT_NONE +from telegram._utils.strings import to_camel_case +from telegram.constants import ( + ChatAction, + InlineQueryLimit, + InlineQueryResultType, + MenuButtonType, + ParseMode, + ReactionEmoji, +) +from telegram.error import BadRequest, EndPointNotFound, InvalidToken, NetworkError +from telegram.ext import ExtBot, InvalidCallbackData +from telegram.helpers import escape_markdown +from telegram.request import BaseRequest, HTTPXRequest, RequestData +from telegram.warnings import PTBDeprecationWarning, PTBUserWarning +from tests.auxil.bot_method_checks import check_defaults_handling +from tests.auxil.ci_bots import FALLBACKS +from tests.auxil.envvars import GITHUB_ACTION, TEST_WITH_OPT_DEPS +from tests.auxil.files import data_file +from tests.auxil.networking import NonchalantHttpxRequest, expect_bad_request +from tests.auxil.pytest_classes import PytestBot, PytestExtBot, make_bot +from tests.auxil.slots import mro_slots + +from ._files.test_photo import photo_file +from .auxil.build_messages import make_message + + +@pytest.fixture +async def message(bot, chat_id): # mostly used in tests for edit_message + out = await bot.send_message( + chat_id, "Text", disable_web_page_preview=True, disable_notification=True + ) + out._unfreeze() + return out + + +@pytest.fixture(scope="module") +async def media_message(bot, chat_id): + with data_file("telegram.ogg").open("rb") as f: + return await bot.send_voice(chat_id, voice=f, caption="my caption", read_timeout=10) + + +@pytest.fixture(scope="module") +def chat_permissions(): + return ChatPermissions(can_send_messages=False, can_change_info=False, can_invite_users=False) + + +def inline_results_callback(page=None): + if not page: + return [InlineQueryResultArticle(i, str(i), None) for i in range(1, 254)] + if page <= 5: + return [ + InlineQueryResultArticle(i, str(i), None) + for i in range(page * 5 + 1, (page + 1) * 5 + 1) + ] + return None + + +@pytest.fixture(scope="module") +def inline_results(): + return inline_results_callback() + + +BASE_GAME_SCORE = 60 # Base game score for game tests + +xfail = pytest.mark.xfail( + bool(GITHUB_ACTION), # This condition is only relevant for github actions game tests. + reason=( + "Can fail due to race conditions when multiple test suites " + "with the same bot token are run at the same time" + ), +) + + +def bot_methods(ext_bot=True, include_camel_case=False, include_do_api_request=False): + arg_values = [] + ids = [] + non_api_methods = [ + "de_json", + "de_list", + "to_dict", + "to_json", + "parse_data", + "get_bot", + "set_bot", + "initialize", + "shutdown", + "insert_callback_data", + ] + if not include_do_api_request: + non_api_methods.append("do_api_request") + + classes = (Bot, ExtBot) if ext_bot else (Bot,) + for cls in classes: + for name, attribute in inspect.getmembers(cls, predicate=inspect.isfunction): + if name.startswith("_") or name in non_api_methods: + continue + if not include_camel_case and any(x.isupper() for x in name): + continue + arg_values.append((cls, name, attribute)) + ids.append(f"{cls.__name__}.{name}") + + return pytest.mark.parametrize( + argnames="bot_class, bot_method_name,bot_method", argvalues=arg_values, ids=ids + ) + + +class InputMessageContentLPO(InputMessageContent): + """ + This is here to cover the case of InputMediaContent classes in testing answer_ilq that have + `link_preview_options` but not `parse_mode`. Unlikely to ever happen, but better be save + than sorry … + """ + + __slots__ = ("entities", "link_preview_options", "message_text", "parse_mode") + + def __init__( + self, + message_text: str, + link_preview_options=DEFAULT_NONE, + *, + api_kwargs=None, + ): + super().__init__(api_kwargs=api_kwargs) + self._unfreeze() + self.message_text = message_text + self.link_preview_options = link_preview_options + + +class TestBotWithoutRequest: + """ + Most are executed on tg.ext.ExtBot, as that class only extends the functionality of tg.bot + + Behavior for init of ExtBot with missing optional dependency cachetools (for CallbackDataCache) + is tested in `test_callbackdatacache` + """ + + test_flag = None + + @pytest.fixture(autouse=True) + def _reset(self): + self.test_flag = None + + @pytest.mark.parametrize("bot_class", [Bot, ExtBot]) + def test_slot_behaviour(self, bot_class, bot): + inst = bot_class(bot.token) + for attr in inst.__slots__: + assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" + + async def test_no_token_passed(self): + with pytest.raises(InvalidToken, match="You must pass the token"): + Bot("") + + async def test_repr(self): + bot = Bot(token="some_token", base_file_url="") + assert repr(bot) == "Bot[token=some_token]" + + async def test_to_dict(self, bot): + to_dict_bot = bot.to_dict() + + assert isinstance(to_dict_bot, dict) + assert to_dict_bot["id"] == bot.id + assert to_dict_bot["username"] == bot.username + assert to_dict_bot["first_name"] == bot.first_name + if bot.last_name: + assert to_dict_bot["last_name"] == bot.last_name + + async def test_initialize_and_shutdown(self, bot: PytestExtBot, monkeypatch): + async def initialize(*args, **kwargs): + self.test_flag = ["initialize"] + + async def stop(*args, **kwargs): + self.test_flag.append("stop") + + temp_bot = PytestBot(token=bot.token, request=NonchalantHttpxRequest()) + orig_stop = temp_bot.request.shutdown + + try: + monkeypatch.setattr(temp_bot.request, "initialize", initialize) + monkeypatch.setattr(temp_bot.request, "shutdown", stop) + await temp_bot.initialize() + assert self.test_flag == ["initialize"] + assert temp_bot.bot == bot.bot + + await temp_bot.shutdown() + assert self.test_flag == ["initialize", "stop"] + finally: + await orig_stop() + + async def test_multiple_inits_and_shutdowns(self, bot, monkeypatch): + self.received = defaultdict(int) + + async def initialize(*args, **kwargs): + self.received["init"] += 1 + + async def shutdown(*args, **kwargs): + self.received["shutdown"] += 1 + + monkeypatch.setattr(HTTPXRequest, "initialize", initialize) + monkeypatch.setattr(HTTPXRequest, "shutdown", shutdown) + + test_bot = PytestBot(bot.token) + await test_bot.initialize() + await test_bot.initialize() + await test_bot.initialize() + await test_bot.shutdown() + await test_bot.shutdown() + await test_bot.shutdown() + + # 2 instead of 1 since we have to request objects for each bot + assert self.received["init"] == 2 + assert self.received["shutdown"] == 2 + + async def test_context_manager(self, monkeypatch, bot): + async def initialize(): + self.test_flag = ["initialize"] + + async def shutdown(*args): + self.test_flag.append("stop") + + monkeypatch.setattr(bot, "initialize", initialize) + monkeypatch.setattr(bot, "shutdown", shutdown) + + async with bot: + pass + + assert self.test_flag == ["initialize", "stop"] + + async def test_context_manager_exception_on_init(self, monkeypatch, bot): + async def initialize(): + raise RuntimeError("initialize") + + async def shutdown(): + self.test_flag = "stop" + + monkeypatch.setattr(bot, "initialize", initialize) + monkeypatch.setattr(bot, "shutdown", shutdown) + + with pytest.raises(RuntimeError, match="initialize"): + async with bot: + pass + + assert self.test_flag == "stop" + + async def test_equality(self): + async with make_bot(token=FALLBACKS[0]["token"]) as a, make_bot( + token=FALLBACKS[0]["token"] + ) as b, Bot(token=FALLBACKS[0]["token"]) as c, make_bot(token=FALLBACKS[1]["token"]) as d: + e = Update(123456789) + f = Bot(token=FALLBACKS[0]["token"]) + + assert a == b + assert hash(a) == hash(b) + assert a is not b + + assert a == c + assert hash(a) == hash(c) + + assert a != d + assert hash(a) != hash(d) + + assert a != e + assert hash(a) != hash(e) + + # We cant check equality for unintialized Bot object + assert hash(a) != hash(f) + + @pytest.mark.parametrize( + "attribute", + [ + "id", + "username", + "first_name", + "last_name", + "name", + "can_join_groups", + "can_read_all_group_messages", + "supports_inline_queries", + "link", + ], + ) + async def test_get_me_and_properties_not_initialized(self, bot: Bot, attribute): + bot = Bot(token=bot.token) + try: + with pytest.raises(RuntimeError, match="not properly initialized"): + bot[attribute] + finally: + await bot.shutdown() + + async def test_get_me_and_properties(self, bot): + get_me_bot = await ExtBot(bot.token).get_me() + + assert isinstance(get_me_bot, User) + assert get_me_bot.id == bot.id + assert get_me_bot.username == bot.username + assert get_me_bot.first_name == bot.first_name + assert get_me_bot.last_name == bot.last_name + assert get_me_bot.name == bot.name + assert get_me_bot.can_join_groups == bot.can_join_groups + assert get_me_bot.can_read_all_group_messages == bot.can_read_all_group_messages + assert get_me_bot.supports_inline_queries == bot.supports_inline_queries + assert f"https://t.me/{get_me_bot.username}" == bot.link + + def test_bot_pickling_error(self, bot): + with pytest.raises(pickle.PicklingError, match="Bot objects cannot be pickled"): + pickle.dumps(bot) + + def test_bot_deepcopy_error(self, bot): + with pytest.raises(TypeError, match="Bot objects cannot be deepcopied"): + copy.deepcopy(bot) + + @pytest.mark.parametrize( + ("cls", "logger_name"), [(Bot, "telegram.Bot"), (ExtBot, "telegram.ext.ExtBot")] + ) + async def test_bot_method_logging(self, bot: PytestExtBot, cls, logger_name, caplog): + # Second argument makes sure that we ignore logs from e.g. httpx + with caplog.at_level(logging.DEBUG, logger="telegram"): + await cls(bot.token).get_me() + # Only for stabilizing this test- + if len(caplog.records) == 4: + for idx, record in enumerate(caplog.records): + print(record) + if record.getMessage().startswith("Task was destroyed but it is pending"): + caplog.records.pop(idx) + if record.getMessage().startswith("Task exception was never retrieved"): + caplog.records.pop(idx) + assert len(caplog.records) == 2 + + assert all(caplog.records[i].name == logger_name for i in [-1, 0]) + assert ( + caplog.records[0] + .getMessage() + .startswith("Calling Bot API endpoint `getMe` with parameters `{}`") + ) + assert ( + caplog.records[-1] + .getMessage() + .startswith("Call to Bot API endpoint `getMe` finished with return value") + ) + + @bot_methods() + def test_camel_case_aliases(self, bot_class, bot_method_name, bot_method): + camel_case_name = to_camel_case(bot_method_name) + camel_case_function = getattr(bot_class, camel_case_name, False) + assert camel_case_function is not False, f"{camel_case_name} not found" + assert camel_case_function is bot_method, f"{camel_case_name} is not {bot_method}" + + @bot_methods(include_do_api_request=True) + def test_coroutine_functions(self, bot_class, bot_method_name, bot_method): + """Check that all bot methods are defined as async def ...""" + meth = getattr(bot_method, "__wrapped__", bot_method) # to unwrap the @_log decorator + assert inspect.iscoroutinefunction(meth), f"{bot_method_name} must be a coroutine function" + + @bot_methods(include_do_api_request=True) + def test_api_kwargs_and_timeouts_present(self, bot_class, bot_method_name, bot_method): + """Check that all bot methods have `api_kwargs` and timeout params.""" + param_names = inspect.signature(bot_method).parameters.keys() + params = ("pool_timeout", "read_timeout", "connect_timeout", "write_timeout", "api_kwargs") + + for param in params: + assert param in param_names, f"{bot_method_name} is missing the parameter `{param}`" + + rate_arg = "rate_limit_args" + if bot_method_name.replace("_", "").lower() != "getupdates" and bot_class is ExtBot: + assert rate_arg in param_names, f"{bot_method} is missing the parameter `{rate_arg}`" + + @bot_methods(ext_bot=False) + async def test_defaults_handling( + self, + bot_class, + bot_method_name: str, + bot_method, + bot: PytestExtBot, + raw_bot: PytestBot, + ): + """ + Here we check that the bot methods handle tg.ext.Defaults correctly. This has two parts: + + 1. Check that ExtBot actually inserts the defaults values correctly + 2. Check that tg.Bot just replaces `DefaultValue(obj)` with `obj`, i.e. that it doesn't + pass any `DefaultValue` instances to Request. See the docstring of + tg.Bot._insert_defaults for details on why we need that + + As for most defaults, + we can't really check the effect, we just check if we're passing the correct kwargs to + Request.post. As bot method tests a scattered across the different test files, we do + this here in one place. + + The same test is also run for all the shortcuts (Message.reply_text) etc in the + corresponding tests. + + Finally, there are some tests for Defaults.{parse_mode, quote, allow_sending_without_reply} + at the appropriate places, as those are the only things we can actually check. + """ + # Mocking get_me within check_defaults_handling messes with the cached values like + # Bot.{bot, username, id, …}` unless we return the expected User object. + return_value = bot.bot if bot_method_name.lower().replace("_", "") == "getme" else None + + # Check that ExtBot does the right thing + bot_method = getattr(bot, bot_method_name) + raw_bot_method = getattr(raw_bot, bot_method_name) + assert await check_defaults_handling(bot_method, bot, return_value=return_value) + assert await check_defaults_handling(raw_bot_method, raw_bot, return_value=return_value) + + @pytest.mark.parametrize( + ("name", "method"), inspect.getmembers(Bot, predicate=inspect.isfunction) + ) + def test_ext_bot_signature(self, name, method): + """ + Here we make sure that all methods of ext.ExtBot have the same signature as the + corresponding methods of tg.Bot. + """ + # Some methods of ext.ExtBot + global_extra_args = {"rate_limit_args"} + extra_args_per_method = defaultdict( + set, {"__init__": {"arbitrary_callback_data", "defaults", "rate_limiter"}} + ) + different_hints_per_method = defaultdict(set, {"__setattr__": {"ext_bot"}}) + + signature = inspect.signature(method) + ext_signature = inspect.signature(getattr(ExtBot, name)) + + assert ( + ext_signature.return_annotation == signature.return_annotation + ), f"Wrong return annotation for method {name}" + assert ( + set(signature.parameters) + == set(ext_signature.parameters) - global_extra_args - extra_args_per_method[name] + ), f"Wrong set of parameters for method {name}" + for param_name, param in signature.parameters.items(): + if param_name in different_hints_per_method[name]: + continue + assert ( + param.annotation == ext_signature.parameters[param_name].annotation + ), f"Wrong annotation for parameter {param_name} of method {name}" + assert ( + param.default == ext_signature.parameters[param_name].default + ), f"Wrong default value for parameter {param_name} of method {name}" + assert ( + param.kind == ext_signature.parameters[param_name].kind + ), f"Wrong parameter kind for parameter {param_name} of method {name}" + + async def test_unknown_kwargs(self, bot, monkeypatch): + async def post(url, request_data: RequestData, *args, **kwargs): + data = request_data.json_parameters + if not all([data["unknown_kwarg_1"] == "7", data["unknown_kwarg_2"] == "5"]): + pytest.fail("got wrong parameters") + return True + + monkeypatch.setattr(bot.request, "post", post) + await bot.send_message( + 123, "text", api_kwargs={"unknown_kwarg_1": 7, "unknown_kwarg_2": 5} + ) + + async def test_get_updates_deserialization_error(self, bot, monkeypatch, caplog): + async def faulty_do_request(*args, **kwargs): + return ( + HTTPStatus.OK, + b'{"ok": true, "result": [{"update_id": "1", "message": "unknown_format"}]}', + ) + + monkeypatch.setattr(HTTPXRequest, "do_request", faulty_do_request) + + bot = PytestExtBot(get_updates_request=HTTPXRequest(), token=bot.token) + + with caplog.at_level(logging.CRITICAL), pytest.raises(AttributeError): + await bot.get_updates() + + assert len(caplog.records) == 1 + assert caplog.records[0].name == "telegram.ext.ExtBot" + assert caplog.records[0].levelno == logging.CRITICAL + assert caplog.records[0].getMessage() == ( + "Error while parsing updates! Received data was " + "[{'update_id': '1', 'message': 'unknown_format'}]" + ) + assert caplog.records[0].exc_info[0] is AttributeError + + async def test_answer_web_app_query(self, bot, raw_bot, monkeypatch): + params = False + + # For now just test that our internals pass the correct data + + async def make_assertion(url, request_data: RequestData, *args, **kwargs): + nonlocal params + params = request_data.parameters == { + "web_app_query_id": "12345", + "result": { + "title": "title", + "input_message_content": { + "message_text": "text", + }, + "type": InlineQueryResultType.ARTICLE, + "id": "1", + }, + } + return SentWebAppMessage("321").to_dict() + + # We test different result types more thoroughly for answer_inline_query, so we just + # use the one type here + result = InlineQueryResultArticle("1", "title", InputTextMessageContent("text")) + copied_result = copy.copy(result) + + ext_bot = bot + for bot_type in (ext_bot, raw_bot): + # We need to test 1) below both the bot and raw_bot and setting this up with + # pytest.parametrize appears to be difficult ... + monkeypatch.setattr(bot_type.request, "post", make_assertion) + web_app_msg = await bot_type.answer_web_app_query("12345", result) + assert params, "something went wrong with passing arguments to the request" + assert isinstance(web_app_msg, SentWebAppMessage) + assert web_app_msg.inline_message_id == "321" + + # 1) + # make sure that the results were not edited in-place + assert result == copied_result + assert ( + result.input_message_content.parse_mode + == copied_result.input_message_content.parse_mode + ) + + @pytest.mark.parametrize( + "default_bot", + [{"parse_mode": "Markdown", "disable_web_page_preview": True}], + indirect=True, + ) + @pytest.mark.parametrize( + ("ilq_result", "expected_params"), + [ + ( + InlineQueryResultArticle("1", "title", InputTextMessageContent("text")), + { + "web_app_query_id": "12345", + "result": { + "title": "title", + "input_message_content": { + "message_text": "text", + "parse_mode": "Markdown", + "link_preview_options": { + "is_disabled": True, + }, + }, + "type": InlineQueryResultType.ARTICLE, + "id": "1", + }, + }, + ), + ( + InlineQueryResultArticle( + "1", + "title", + InputTextMessageContent( + "text", parse_mode="HTML", disable_web_page_preview=False + ), + ), + { + "web_app_query_id": "12345", + "result": { + "title": "title", + "input_message_content": { + "message_text": "text", + "parse_mode": "HTML", + "link_preview_options": { + "is_disabled": False, + }, + }, + "type": InlineQueryResultType.ARTICLE, + "id": "1", + }, + }, + ), + ( + InlineQueryResultArticle( + "1", + "title", + InputTextMessageContent( + "text", parse_mode=None, disable_web_page_preview="False" + ), + ), + { + "web_app_query_id": "12345", + "result": { + "title": "title", + "input_message_content": { + "message_text": "text", + "link_preview_options": { + "is_disabled": "False", + }, + }, + "type": InlineQueryResultType.ARTICLE, + "id": "1", + }, + }, + ), + ], + ) + async def test_answer_web_app_query_defaults( + self, default_bot, ilq_result, expected_params, monkeypatch + ): + bot = default_bot + params = False + + # For now just test that our internals pass the correct data + + async def make_assertion(url, request_data: RequestData, *args, **kwargs): + nonlocal params + params = request_data.parameters == expected_params + return SentWebAppMessage("321").to_dict() + + monkeypatch.setattr(bot.request, "post", make_assertion) + + # We test different result types more thoroughly for answer_inline_query, so we just + # use the one type here + copied_result = copy.copy(ilq_result) + + web_app_msg = await bot.answer_web_app_query("12345", ilq_result) + assert params, "something went wrong with passing arguments to the request" + assert isinstance(web_app_msg, SentWebAppMessage) + assert web_app_msg.inline_message_id == "321" + + # make sure that the results were not edited in-place + assert ilq_result == copied_result + assert ( + ilq_result.input_message_content.parse_mode + == copied_result.input_message_content.parse_mode + ) + + # TODO: Needs improvement. We need incoming inline query to test answer. + @pytest.mark.parametrize("button_type", ["start", "web_app"]) + async def test_answer_inline_query(self, monkeypatch, bot, raw_bot, button_type): + # For now just test that our internals pass the correct data + async def make_assertion(url, request_data: RequestData, *args, **kwargs): + expected = { + "cache_time": 300, + "results": [ + { + "title": "first", + "id": "11", + "type": "article", + "input_message_content": {"message_text": "first"}, + }, + { + "title": "second", + "id": "12", + "type": "article", + "input_message_content": {"message_text": "second"}, + }, + { + "title": "test_result", + "id": "123", + "type": "document", + "document_url": ( + "https://raw.githubusercontent.com/python-telegram-bot" + "/logos/master/logo/png/ptb-logo_240.png" + ), + "mime_type": "image/png", + "caption": "ptb_logo", + "input_message_content": {"message_text": "imc"}, + }, + ], + "next_offset": "42", + "inline_query_id": 1234, + "is_personal": True, + } + + if button_type == "start": + button_dict = {"text": "button_text", "start_parameter": "start_parameter"} + else: + button_dict = { + "text": "button_text", + "web_app": {"url": "https://python-telegram-bot.org"}, + } + + expected["button"] = button_dict + + return request_data.parameters == expected + + results = [ + InlineQueryResultArticle("11", "first", InputTextMessageContent("first")), + InlineQueryResultArticle("12", "second", InputMessageContentLPO("second")), + InlineQueryResultDocument( + id="123", + document_url=( + "https://raw.githubusercontent.com/python-telegram-bot/logos/master/" + "logo/png/ptb-logo_240.png" + ), + title="test_result", + mime_type="image/png", + caption="ptb_logo", + input_message_content=InputMessageContentLPO("imc"), + ), + ] + + if button_type == "start": + button = InlineQueryResultsButton( + text="button_text", start_parameter="start_parameter" + ) + elif button_type == "web_app": + button = InlineQueryResultsButton( + text="button_text", web_app=WebAppInfo("https://python-telegram-bot.org") + ) + else: + button = None + + copied_results = copy.copy(results) + ext_bot = bot + for bot_type in (ext_bot, raw_bot): + # We need to test 1) below both the bot and raw_bot and setting this up with + # pytest.parametrize appears to be difficult ... + monkeypatch.setattr(bot_type.request, "post", make_assertion) + assert await bot_type.answer_inline_query( + 1234, + results=results, + cache_time=300, + is_personal=True, + next_offset="42", + button=button, + ) + + # 1) + # make sure that the results were not edited in-place + assert results == copied_results + for idx, result in enumerate(results): + if hasattr(result, "parse_mode"): + assert result.parse_mode == copied_results[idx].parse_mode + if hasattr(result, "input_message_content"): + assert getattr(result.input_message_content, "parse_mode", None) == getattr( + copied_results[idx].input_message_content, "parse_mode", None + ) + assert getattr( + result.input_message_content, "disable_web_page_preview", None + ) == getattr( + copied_results[idx].input_message_content, "disable_web_page_preview", None + ) + + monkeypatch.delattr(bot_type.request, "post") + + async def test_answer_inline_query_no_default_parse_mode(self, monkeypatch, bot): + async def make_assertion(url, request_data: RequestData, *args, **kwargs): + return request_data.parameters == { + "cache_time": 300, + "results": [ + { + "title": "first", + "id": "11", + "type": "article", + "input_message_content": {"message_text": "first"}, + }, + { + "title": "second", + "id": "12", + "type": "article", + "input_message_content": {"message_text": "second"}, + }, + { + "title": "test_result", + "id": "123", + "type": "document", + "document_url": ( + "https://raw.githubusercontent.com/" + "python-telegram-bot/logos/master/logo/png/" + "ptb-logo_240.png" + ), + "mime_type": "image/png", + "caption": "ptb_logo", + "input_message_content": {"message_text": "imc"}, + }, + ], + "next_offset": "42", + "inline_query_id": 1234, + "is_personal": True, + } + + monkeypatch.setattr(bot.request, "post", make_assertion) + results = [ + InlineQueryResultArticle("11", "first", InputTextMessageContent("first")), + InlineQueryResultArticle("12", "second", InputMessageContentLPO("second")), + InlineQueryResultDocument( + id="123", + document_url=( + "https://raw.githubusercontent.com/python-telegram-bot/logos/master/" + "logo/png/ptb-logo_240.png" + ), + title="test_result", + mime_type="image/png", + caption="ptb_logo", + input_message_content=InputMessageContentLPO("imc"), + ), + ] + + copied_results = copy.copy(results) + assert await bot.answer_inline_query( + 1234, + results=results, + cache_time=300, + is_personal=True, + next_offset="42", + ) + # make sure that the results were not edited in-place + assert results == copied_results + for idx, result in enumerate(results): + if hasattr(result, "parse_mode"): + assert result.parse_mode == copied_results[idx].parse_mode + if hasattr(result, "input_message_content"): + assert getattr(result.input_message_content, "parse_mode", None) == getattr( + copied_results[idx].input_message_content, "parse_mode", None + ) + assert getattr( + result.input_message_content, "disable_web_page_preview", None + ) == getattr( + copied_results[idx].input_message_content, "disable_web_page_preview", None + ) + + @pytest.mark.parametrize( + "default_bot", + [{"parse_mode": "Markdown", "disable_web_page_preview": True}], + indirect=True, + ) + async def test_answer_inline_query_default_parse_mode(self, monkeypatch, default_bot): + async def make_assertion(url, request_data: RequestData, *args, **kwargs): + return request_data.parameters == { + "cache_time": 300, + "results": [ + { + "title": "first", + "id": "11", + "type": InlineQueryResultType.ARTICLE, + "input_message_content": { + "message_text": "first", + "parse_mode": "Markdown", + "link_preview_options": { + "is_disabled": True, + }, + }, + }, + { + "title": "second", + "id": "12", + "type": InlineQueryResultType.ARTICLE, + "input_message_content": { + "message_text": "second", + "link_preview_options": { + "is_disabled": True, + }, + }, + }, + { + "title": "test_result", + "id": "123", + "type": InlineQueryResultType.DOCUMENT, + "document_url": ( + "https://raw.githubusercontent.com/" + "python-telegram-bot/logos/master/logo/png/" + "ptb-logo_240.png" + ), + "mime_type": "image/png", + "caption": "ptb_logo", + "parse_mode": "Markdown", + "input_message_content": { + "message_text": "imc", + "link_preview_options": { + "is_disabled": True, + }, + "parse_mode": "Markdown", + }, + }, + ], + "next_offset": "42", + "inline_query_id": 1234, + "is_personal": True, + } + + monkeypatch.setattr(default_bot.request, "post", make_assertion) + results = [ + InlineQueryResultArticle("11", "first", InputTextMessageContent("first")), + InlineQueryResultArticle("12", "second", InputMessageContentLPO("second")), + InlineQueryResultDocument( + id="123", + document_url=( + "https://raw.githubusercontent.com/python-telegram-bot/logos/master/" + "logo/png/ptb-logo_240.png" + ), + title="test_result", + mime_type="image/png", + caption="ptb_logo", + input_message_content=InputTextMessageContent("imc"), + ), + ] + + copied_results = copy.copy(results) + assert await default_bot.answer_inline_query( + 1234, + results=results, + cache_time=300, + is_personal=True, + next_offset="42", + ) + # make sure that the results were not edited in-place + assert results == copied_results + for idx, result in enumerate(results): + if hasattr(result, "parse_mode"): + assert result.parse_mode == copied_results[idx].parse_mode + if hasattr(result, "input_message_content"): + assert getattr(result.input_message_content, "parse_mode", None) == getattr( + copied_results[idx].input_message_content, "parse_mode", None + ) + assert getattr( + result.input_message_content, "disable_web_page_preview", None + ) == getattr( + copied_results[idx].input_message_content, "disable_web_page_preview", None + ) + + @pytest.mark.parametrize( + ("current_offset", "num_results", "id_offset", "expected_next_offset"), + [ + ("", InlineQueryLimit.RESULTS, 1, 1), + (1, InlineQueryLimit.RESULTS, 51, 2), + (5, 3, 251, ""), + ], + ) + async def test_answer_inline_query_current_offset_1( + self, + monkeypatch, + bot, + inline_results, + current_offset, + num_results, + id_offset, + expected_next_offset, + ): + # For now just test that our internals pass the correct data + async def make_assertion(url, request_data: RequestData, *args, **kwargs): + data = request_data.parameters + results = data["results"] + length_matches = len(results) == num_results + ids_match = all(int(res["id"]) == id_offset + i for i, res in enumerate(results)) + next_offset_matches = data["next_offset"] == str(expected_next_offset) + return length_matches and ids_match and next_offset_matches + + monkeypatch.setattr(bot.request, "post", make_assertion) + + assert await bot.answer_inline_query( + 1234, results=inline_results, current_offset=current_offset + ) + + async def test_answer_inline_query_current_offset_2(self, monkeypatch, bot, inline_results): + # For now just test that our internals pass the correct data + async def make_assertion(url, request_data: RequestData, *args, **kwargs): + data = request_data.parameters + results = data["results"] + length_matches = len(results) == InlineQueryLimit.RESULTS + ids_match = all(int(res["id"]) == 1 + i for i, res in enumerate(results)) + next_offset_matches = data["next_offset"] == "1" + return length_matches and ids_match and next_offset_matches + + monkeypatch.setattr(bot.request, "post", make_assertion) + + assert await bot.answer_inline_query(1234, results=inline_results, current_offset=0) + + inline_results = inline_results[:30] + + async def make_assertion(url, request_data: RequestData, *args, **kwargs): + data = request_data.parameters + results = data["results"] + length_matches = len(results) == 30 + ids_match = all(int(res["id"]) == 1 + i for i, res in enumerate(results)) + next_offset_matches = not data["next_offset"] + return length_matches and ids_match and next_offset_matches + + monkeypatch.setattr(bot.request, "post", make_assertion) + + assert await bot.answer_inline_query(1234, results=inline_results, current_offset=0) + + async def test_answer_inline_query_current_offset_callback(self, monkeypatch, bot): + # For now just test that our internals pass the correct data + async def make_assertion(url, request_data: RequestData, *args, **kwargs): + data = request_data.parameters + results = data["results"] + length = len(results) == 5 + ids = all(int(res["id"]) == 6 + i for i, res in enumerate(results)) + next_offset = data["next_offset"] == "2" + return length and ids and next_offset + + monkeypatch.setattr(bot.request, "post", make_assertion) + + assert await bot.answer_inline_query( + 1234, results=inline_results_callback, current_offset=1 + ) + + async def make_assertion(url, request_data: RequestData, *args, **kwargs): + data = request_data.parameters + results = data["results"] + length = results == [] + next_offset = not data["next_offset"] + return length and next_offset + + monkeypatch.setattr(bot.request, "post", make_assertion) + + assert await bot.answer_inline_query( + 1234, results=inline_results_callback, current_offset=6 + ) + + async def test_send_edit_message_mutually_exclusive_link_preview(self, bot, chat_id): + """Test that link_preview is mutually exclusive with disable_web_page_preview.""" + with pytest.raises(ValueError, match="`link_preview_options` are mutually exclusive"): + await bot.send_message( + chat_id, "text", disable_web_page_preview=True, link_preview_options="something" + ) + + with pytest.raises(ValueError, match="`link_preview_options` are mutually exclusive"): + await bot.edit_message_text( + "text", chat_id, 1, disable_web_page_preview=True, link_preview_options="something" + ) + + async def test_rtm_aswr_mutually_exclusive_reply_parameters(self, bot, chat_id): + """Test that reply_to_message_id and allow_sending_without_reply are mutually exclusive + with reply_parameters.""" + with pytest.raises(ValueError, match="`reply_to_message_id` and"): + await bot.send_message(chat_id, "text", reply_to_message_id=1, reply_parameters=True) + + with pytest.raises(ValueError, match="`allow_sending_without_reply` and"): + await bot.send_message( + chat_id, "text", allow_sending_without_reply=True, reply_parameters=True + ) + + # Test with copy message + with pytest.raises(ValueError, match="`reply_to_message_id` and"): + await bot.copy_message( + chat_id, chat_id, 1, reply_to_message_id=1, reply_parameters=True + ) + + with pytest.raises(ValueError, match="`allow_sending_without_reply` and"): + await bot.copy_message( + chat_id, chat_id, 1, allow_sending_without_reply=True, reply_parameters=True + ) + + # Test with send media group + media = InputMediaPhoto(photo_file) + with pytest.raises(ValueError, match="`reply_to_message_id` and"): + await bot.send_media_group( + chat_id, media, reply_to_message_id=1, reply_parameters=True + ) + + with pytest.raises(ValueError, match="`allow_sending_without_reply` and"): + await bot.send_media_group( + chat_id, media, allow_sending_without_reply=True, reply_parameters=True + ) + + # get_file is tested multiple times in the test_*media* modules. + # Here we only test the behaviour for bot apis in local mode + async def test_get_file_local_mode(self, bot, monkeypatch): + path = str(data_file("game.gif")) + + async def make_assertion(*args, **kwargs): + return { + "file_id": None, + "file_unique_id": None, + "file_size": None, + "file_path": path, + } + + monkeypatch.setattr(bot, "_post", make_assertion) + + resulting_path = (await bot.get_file("file_id")).file_path + assert bot.token not in resulting_path + assert resulting_path == path + + # TODO: Needs improvement. No feasible way to test until bots can add members. + async def test_ban_chat_member(self, monkeypatch, bot): + async def make_assertion(url, request_data: RequestData, *args, **kwargs): + data = request_data.json_parameters + chat_id = data["chat_id"] == "2" + user_id = data["user_id"] == "32" + until_date = data.get("until_date", "1577887200") == "1577887200" + revoke_msgs = data.get("revoke_messages", "true") == "true" + return chat_id and user_id and until_date and revoke_msgs + + monkeypatch.setattr(bot.request, "post", make_assertion) + until = from_timestamp(1577887200) + + assert await bot.ban_chat_member(2, 32) + assert await bot.ban_chat_member(2, 32, until_date=until) + assert await bot.ban_chat_member(2, 32, until_date=1577887200) + assert await bot.ban_chat_member(2, 32, revoke_messages=True) + + async def test_ban_chat_member_default_tz(self, monkeypatch, tz_bot): + until = dtm.datetime(2020, 1, 11, 16, 13) + until_timestamp = to_timestamp(until, tzinfo=tz_bot.defaults.tzinfo) + + async def make_assertion(url, request_data: RequestData, *args, **kwargs): + data = request_data.parameters + chat_id = data["chat_id"] == 2 + user_id = data["user_id"] == 32 + until_date = data.get("until_date", until_timestamp) == until_timestamp + return chat_id and user_id and until_date + + monkeypatch.setattr(tz_bot.request, "post", make_assertion) + + assert await tz_bot.ban_chat_member(2, 32) + assert await tz_bot.ban_chat_member(2, 32, until_date=until) + assert await tz_bot.ban_chat_member(2, 32, until_date=until_timestamp) + + async def test_ban_chat_sender_chat(self, monkeypatch, bot): + # For now, we just test that we pass the correct data to TG + async def make_assertion(url, request_data: RequestData, *args, **kwargs): + data = request_data.parameters + chat_id = data["chat_id"] == 2 + sender_chat_id = data["sender_chat_id"] == 32 + return chat_id and sender_chat_id + + monkeypatch.setattr(bot.request, "post", make_assertion) + assert await bot.ban_chat_sender_chat(2, 32) + + # TODO: Needs improvement. + @pytest.mark.parametrize("only_if_banned", [True, False, None]) + async def test_unban_chat_member(self, monkeypatch, bot, only_if_banned): + async def make_assertion(url, request_data: RequestData, *args, **kwargs): + data = request_data.parameters + chat_id = data["chat_id"] == 2 + user_id = data["user_id"] == 32 + o_i_b = data.get("only_if_banned", None) == only_if_banned + return chat_id and user_id and o_i_b + + monkeypatch.setattr(bot.request, "post", make_assertion) + + assert await bot.unban_chat_member(2, 32, only_if_banned=only_if_banned) + + async def test_unban_chat_sender_chat(self, monkeypatch, bot): + async def make_assertion(url, request_data: RequestData, *args, **kwargs): + data = request_data.json_parameters + chat_id = data["chat_id"] == "2" + sender_chat_id = data["sender_chat_id"] == "32" + return chat_id and sender_chat_id + + monkeypatch.setattr(bot.request, "post", make_assertion) + assert await bot.unban_chat_sender_chat(2, 32) + + async def test_set_chat_permissions(self, monkeypatch, bot, chat_permissions): + async def make_assertion(url, request_data: RequestData, *args, **kwargs): + data = request_data.json_parameters + chat_id = data["chat_id"] == "2" + permissions = data["permissions"] == chat_permissions.to_json() + use_independent_chat_permissions = data["use_independent_chat_permissions"] + return chat_id and permissions and use_independent_chat_permissions + + monkeypatch.setattr(bot.request, "post", make_assertion) + + assert await bot.set_chat_permissions(2, chat_permissions, True) + + async def test_set_chat_administrator_custom_title(self, monkeypatch, bot): + async def make_assertion(url, request_data: RequestData, *args, **kwargs): + data = request_data.parameters + chat_id = data["chat_id"] == 2 + user_id = data["user_id"] == 32 + custom_title = data["custom_title"] == "custom_title" + return chat_id and user_id and custom_title + + monkeypatch.setattr(bot.request, "post", make_assertion) + assert await bot.set_chat_administrator_custom_title(2, 32, "custom_title") + + # TODO: Needs improvement. Need an incoming callbackquery to test + async def test_answer_callback_query(self, monkeypatch, bot): + # For now just test that our internals pass the correct data + async def make_assertion(url, request_data: RequestData, *args, **kwargs): + return request_data.parameters == { + "callback_query_id": 23, + "show_alert": True, + "url": "no_url", + "cache_time": 1, + "text": "answer", + } + + monkeypatch.setattr(bot.request, "post", make_assertion) + + assert await bot.answer_callback_query( + 23, text="answer", show_alert=True, url="no_url", cache_time=1 + ) + + @pytest.mark.parametrize("drop_pending_updates", [True, False]) + async def test_set_webhook_delete_webhook_drop_pending_updates( + self, bot, drop_pending_updates, monkeypatch + ): + async def make_assertion(url, request_data: RequestData, *args, **kwargs): + data = request_data.parameters + return data.get("drop_pending_updates") == drop_pending_updates + + monkeypatch.setattr(bot.request, "post", make_assertion) + + assert await bot.set_webhook("", drop_pending_updates=drop_pending_updates) + assert await bot.delete_webhook(drop_pending_updates=drop_pending_updates) + + @pytest.mark.parametrize("local_file", ["str", "Path", False]) + async def test_set_webhook_params(self, bot, monkeypatch, local_file): + # actually making calls to TG is done in + # test_set_webhook_get_webhook_info_and_delete_webhook. Sadly secret_token can't be tested + # there so we have this function \o/ + async def make_assertion(*args, **_): + kwargs = args[1] + + if local_file is False: + cert_assertion = ( + kwargs["certificate"].input_file_content + == data_file("sslcert.pem").read_bytes() + ) + else: + cert_assertion = data_file("sslcert.pem").as_uri() + + return ( + kwargs["url"] == "example.com" + and cert_assertion + and kwargs["max_connections"] == 7 + and kwargs["allowed_updates"] == ["messages"] + and kwargs["ip_address"] == "127.0.0.1" + and kwargs["drop_pending_updates"] + and kwargs["secret_token"] == "SoSecretToken" + ) + + monkeypatch.setattr(bot, "_post", make_assertion) + + cert_path = data_file("sslcert.pem") + if local_file == "str": + certificate = str(cert_path) + elif local_file == "Path": + certificate = cert_path + else: + certificate = cert_path.read_bytes() + + assert await bot.set_webhook( + "example.com", + certificate, + 7, + ["messages"], + "127.0.0.1", + True, + "SoSecretToken", + ) + + # TODO: Needs improvement. Need incoming shipping queries to test + async def test_answer_shipping_query_ok(self, monkeypatch, bot): + # For now just test that our internals pass the correct data + async def make_assertion(url, request_data: RequestData, *args, **kwargs): + return request_data.parameters == { + "shipping_query_id": 1, + "ok": True, + "shipping_options": [ + {"title": "option1", "prices": [{"label": "price", "amount": 100}], "id": 1} + ], + } + + monkeypatch.setattr(bot.request, "post", make_assertion) + shipping_options = ShippingOption(1, "option1", [LabeledPrice("price", 100)]) + assert await bot.answer_shipping_query(1, True, shipping_options=[shipping_options]) + + async def test_answer_shipping_query_error_message(self, monkeypatch, bot): + # For now just test that our internals pass the correct data + async def make_assertion(url, request_data: RequestData, *args, **kwargs): + return request_data.parameters == { + "shipping_query_id": 1, + "error_message": "Not enough fish", + "ok": False, + } + + monkeypatch.setattr(bot.request, "post", make_assertion) + assert await bot.answer_shipping_query(1, False, error_message="Not enough fish") + + # TODO: Needs improvement. Need incoming pre checkout queries to test + async def test_answer_pre_checkout_query_ok(self, monkeypatch, bot): + # For now just test that our internals pass the correct data + async def make_assertion(url, request_data: RequestData, *args, **kwargs): + return request_data.parameters == {"pre_checkout_query_id": 1, "ok": True} + + monkeypatch.setattr(bot.request, "post", make_assertion) + assert await bot.answer_pre_checkout_query(1, True) + + async def test_answer_pre_checkout_query_error_message(self, monkeypatch, bot): + # For now just test that our internals pass the correct data + async def make_assertion(url, request_data: RequestData, *args, **kwargs): + return request_data.parameters == { + "pre_checkout_query_id": 1, + "error_message": "Not enough fish", + "ok": False, + } + + monkeypatch.setattr(bot.request, "post", make_assertion) + assert await bot.answer_pre_checkout_query(1, False, error_message="Not enough fish") + + async def test_restrict_chat_member(self, bot, chat_permissions, monkeypatch): + async def make_assertion(url, request_data: RequestData, *args, **kwargs): + data = request_data.json_parameters + chat_id = data["chat_id"] == "@chat" + user_id = data["user_id"] == "2" + permissions = data["permissions"] == chat_permissions.to_json() + until_date = data["until_date"] == "200" + use_independent_chat_permissions = data["use_independent_chat_permissions"] + return ( + chat_id + and user_id + and permissions + and until_date + and use_independent_chat_permissions + ) + + monkeypatch.setattr(bot.request, "post", make_assertion) + + assert await bot.restrict_chat_member("@chat", 2, chat_permissions, 200, True) + + async def test_restrict_chat_member_default_tz( + self, monkeypatch, tz_bot, channel_id, chat_permissions + ): + until = dtm.datetime(2020, 1, 11, 16, 13) + until_timestamp = to_timestamp(until, tzinfo=tz_bot.defaults.tzinfo) + + async def make_assertion(url, request_data: RequestData, *args, **kwargs): + return request_data.parameters.get("until_date", until_timestamp) == until_timestamp + + monkeypatch.setattr(tz_bot.request, "post", make_assertion) + + assert await tz_bot.restrict_chat_member(channel_id, 95205500, chat_permissions) + assert await tz_bot.restrict_chat_member( + channel_id, 95205500, chat_permissions, until_date=until + ) + assert await tz_bot.restrict_chat_member( + channel_id, 95205500, chat_permissions, until_date=until_timestamp + ) + + @pytest.mark.parametrize("local_mode", [True, False]) + async def test_set_chat_photo_local_files(self, monkeypatch, bot, chat_id, local_mode): + try: + bot._local_mode = local_mode + # For just test that the correct paths are passed as we have no local bot API set up + self.test_flag = False + file = data_file("telegram.jpg") + expected = file.as_uri() + + async def make_assertion(_, data, *args, **kwargs): + if local_mode: + self.test_flag = data.get("photo") == expected + else: + self.test_flag = isinstance(data.get("photo"), InputFile) + + monkeypatch.setattr(bot, "_post", make_assertion) + await bot.set_chat_photo(chat_id, file) + assert self.test_flag + finally: + bot._local_mode = False + + async def test_timeout_propagation_explicit(self, monkeypatch, bot, chat_id): + # Use BaseException that's not a subclass of Exception such that + # OkException should not be caught anywhere + class OkException(BaseException): + pass + + timeout = 42 + + async def do_request(*args, **kwargs): + obj = kwargs.get("read_timeout") + if obj == timeout: + raise OkException + + return 200, b'{"ok": true, "result": []}' + + monkeypatch.setattr(bot.request, "do_request", do_request) + + # Test file uploading + with pytest.raises(OkException): + await bot.send_photo( + chat_id, data_file("telegram.jpg").open("rb"), read_timeout=timeout + ) + + # Test JSON submission + with pytest.raises(OkException): + await bot.get_chat_administrators(chat_id, read_timeout=timeout) + + async def test_timeout_propagation_implicit(self, monkeypatch, bot, chat_id): + # Use BaseException that's not a subclass of Exception such that + # OkException should not be caught anywhere + class OkException(BaseException): + pass + + async def request(*args, **kwargs): + timeout = kwargs.get("timeout") + if timeout.write == 20: + raise OkException + + return 200, b'{"ok": true, "result": []}' + + monkeypatch.setattr(httpx.AsyncClient, "request", request) + + # Test file uploading + with pytest.raises(OkException): + await bot.send_photo(chat_id, data_file("telegram.jpg").open("rb")) + + async def test_log_out(self, monkeypatch, bot): + # We don't actually make a request as to not break the test setup + async def assertion(url, request_data: RequestData, *args, **kwargs): + return request_data.json_parameters == {} and url.split("/")[-1] == "logOut" + + monkeypatch.setattr(bot.request, "post", assertion) + + assert await bot.log_out() + + async def test_close(self, monkeypatch, bot): + # We don't actually make a request as to not break the test setup + async def assertion(url, request_data: RequestData, *args, **kwargs): + return request_data.json_parameters == {} and url.split("/")[-1] == "close" + + monkeypatch.setattr(bot.request, "post", assertion) + + assert await bot.close() + + @pytest.mark.parametrize("json_keyboard", [True, False]) + @pytest.mark.parametrize("caption", ["Test", "", None]) + async def test_copy_message( + self, monkeypatch, bot, chat_id, media_message, json_keyboard, caption + ): + keyboard = InlineKeyboardMarkup( + [[InlineKeyboardButton(text="test", callback_data="test2")]] + ) + + async def post(url, request_data: RequestData, *args, **kwargs): + data = request_data.parameters + if not all( + [ + data["chat_id"] == chat_id, + data["from_chat_id"] == chat_id, + data["message_id"] == media_message.message_id, + data.get("caption") == caption, + data["parse_mode"] == ParseMode.HTML, + data["reply_parameters"] + == ReplyParameters(message_id=media_message.message_id).to_dict(), + ( + data["reply_markup"] == keyboard.to_json() + if json_keyboard + else keyboard.to_dict() + ), + data["disable_notification"] is True, + data["caption_entities"] + == [MessageEntity(MessageEntity.BOLD, 0, 4).to_dict()], + data["protect_content"] is True, + data["message_thread_id"] == 1, + ] + ): + pytest.fail("I got wrong parameters in post") + return data + + monkeypatch.setattr(bot.request, "post", post) + await bot.copy_message( + chat_id, + from_chat_id=chat_id, + message_id=media_message.message_id, + caption=caption, + caption_entities=[MessageEntity(MessageEntity.BOLD, 0, 4)], + parse_mode=ParseMode.HTML, + reply_to_message_id=media_message.message_id, + reply_markup=keyboard.to_json() if json_keyboard else keyboard, + disable_notification=True, + protect_content=True, + message_thread_id=1, + ) + + # In the following tests we check that get_updates inserts callback data correctly if necessary + # The same must be done in the webhook updater. This is tested over at test_updater.py, but + # here we test more extensively. + + @pytest.mark.parametrize( + ("acd_in", "maxsize"), + [(True, 1024), (False, 1024), (0, 0), (None, None)], + ) + async def test_callback_data_maxsize(self, bot_info, acd_in, maxsize): + async with make_bot(bot_info, arbitrary_callback_data=acd_in) as acd_bot: + if acd_in is not False: + assert acd_bot.callback_data_cache.maxsize == maxsize + else: + assert acd_bot.callback_data_cache is None + + async def test_arbitrary_callback_data_no_insert(self, monkeypatch, cdc_bot): + """Updates that don't need insertion shouldn't fail obviously""" + bot = cdc_bot + + async def post(*args, **kwargs): + update = Update( + 17, + poll=Poll( + "42", + "question", + options=[PollOption("option", 0)], + total_voter_count=0, + is_closed=False, + is_anonymous=True, + type=Poll.REGULAR, + allows_multiple_answers=False, + ), + ) + return [update.to_dict()] + + try: + monkeypatch.setattr(BaseRequest, "post", post) + updates = await bot.get_updates(timeout=1) + + assert len(updates) == 1 + assert updates[0].update_id == 17 + assert updates[0].poll.id == "42" + finally: + bot.callback_data_cache.clear_callback_data() + bot.callback_data_cache.clear_callback_queries() + + @pytest.mark.parametrize( + "message_type", ["channel_post", "edited_channel_post", "message", "edited_message"] + ) + async def test_arbitrary_callback_data_pinned_message_reply_to_message( + self, cdc_bot, monkeypatch, message_type + ): + bot = cdc_bot + + reply_markup = InlineKeyboardMarkup.from_button( + InlineKeyboardButton(text="text", callback_data="callback_data") + ) + + message = Message( + 1, + dtm.datetime.utcnow(), + None, + reply_markup=bot.callback_data_cache.process_keyboard(reply_markup), + ) + message._unfreeze() + # We do to_dict -> de_json to make sure those aren't the same objects + message.pinned_message = Message.de_json(message.to_dict(), bot) + + async def post(*args, **kwargs): + update = Update( + 17, + **{ + message_type: Message( + 1, + dtm.datetime.utcnow(), + None, + pinned_message=message, + reply_to_message=Message.de_json(message.to_dict(), bot), + ) + }, + ) + return [update.to_dict()] + + try: + monkeypatch.setattr(BaseRequest, "post", post) + updates = await bot.get_updates(timeout=1) + + assert isinstance(updates, tuple) + assert len(updates) == 1 + + effective_message = updates[0][message_type] + assert ( + effective_message.reply_to_message.reply_markup.inline_keyboard[0][0].callback_data + == "callback_data" + ) + assert ( + effective_message.pinned_message.reply_markup.inline_keyboard[0][0].callback_data + == "callback_data" + ) + + pinned_message = effective_message.reply_to_message.pinned_message + assert ( + pinned_message.reply_markup.inline_keyboard[0][0].callback_data == "callback_data" + ) + + finally: + bot.callback_data_cache.clear_callback_data() + bot.callback_data_cache.clear_callback_queries() + + async def test_get_updates_invalid_callback_data(self, cdc_bot, monkeypatch): + bot = cdc_bot + + async def post(*args, **kwargs): + return [ + Update( + 17, + callback_query=CallbackQuery( + id=1, + from_user=None, + chat_instance=123, + data="invalid data", + message=Message( + 1, + from_user=User(1, "", False), + date=dtm.datetime.utcnow(), + chat=Chat(1, ""), + text="Webhook", + ), + ), + ).to_dict() + ] + + try: + monkeypatch.setattr(BaseRequest, "post", post) + updates = await bot.get_updates(timeout=1) + + assert isinstance(updates, tuple) + assert len(updates) == 1 + assert isinstance(updates[0].callback_query.data, InvalidCallbackData) + + finally: + # Reset b/c bots scope is session + bot.callback_data_cache.clear_callback_data() + bot.callback_data_cache.clear_callback_queries() + + # TODO: Needs improvement. We need incoming inline query to test answer. + async def test_replace_callback_data_answer_inline_query(self, monkeypatch, cdc_bot, chat_id): + bot = cdc_bot + + # For now just test that our internals pass the correct data + async def make_assertion( + endpoint, + data=None, + *args, + **kwargs, + ): + inline_keyboard = data["results"][0]["reply_markup"].inline_keyboard + assertion_1 = inline_keyboard[0][1] == no_replace_button + assertion_2 = inline_keyboard[0][0] != replace_button + keyboard, button = ( + inline_keyboard[0][0].callback_data[:32], + inline_keyboard[0][0].callback_data[32:], + ) + assertion_3 = ( + bot.callback_data_cache._keyboard_data[keyboard].button_data[button] + == "replace_test" + ) + assertion_4 = data["results"][1].reply_markup is None + return assertion_1 and assertion_2 and assertion_3 and assertion_4 + + try: + replace_button = InlineKeyboardButton(text="replace", callback_data="replace_test") + no_replace_button = InlineKeyboardButton( + text="no_replace", url="http://python-telegram-bot.org/" + ) + reply_markup = InlineKeyboardMarkup.from_row( + [ + replace_button, + no_replace_button, + ] + ) + + bot.username # call this here so `bot.get_me()` won't be called after mocking + monkeypatch.setattr(bot, "_post", make_assertion) + results = [ + InlineQueryResultArticle( + "11", "first", InputTextMessageContent("first"), reply_markup=reply_markup + ), + InlineQueryResultVoice( + "22", + "https://python-telegram-bot.org/static/testfiles/telegram.ogg", + title="second", + ), + ] + + assert await bot.answer_inline_query(chat_id, results=results) + + finally: + bot.callback_data_cache.clear_callback_data() + bot.callback_data_cache.clear_callback_queries() + + @pytest.mark.parametrize( + "message_type", ["channel_post", "edited_channel_post", "message", "edited_message"] + ) + @pytest.mark.parametrize("self_sender", [True, False]) + async def test_arbitrary_callback_data_via_bot( + self, cdc_bot, monkeypatch, self_sender, message_type + ): + bot = cdc_bot + reply_markup = InlineKeyboardMarkup.from_button( + InlineKeyboardButton(text="text", callback_data="callback_data") + ) + + reply_markup = bot.callback_data_cache.process_keyboard(reply_markup) + message = Message( + 1, + dtm.datetime.utcnow(), + None, + reply_markup=reply_markup, + via_bot=bot.bot if self_sender else User(1, "first", False), + ) + + async def post(*args, **kwargs): + return [Update(17, **{message_type: message}).to_dict()] + + try: + monkeypatch.setattr(BaseRequest, "post", post) + updates = await bot.get_updates(timeout=1) + + assert isinstance(updates, tuple) + assert len(updates) == 1 + + message = updates[0][message_type] + if self_sender: + assert message.reply_markup.inline_keyboard[0][0].callback_data == "callback_data" + else: + assert ( + message.reply_markup.inline_keyboard[0][0].callback_data + == reply_markup.inline_keyboard[0][0].callback_data + ) + finally: + bot.callback_data_cache.clear_callback_data() + bot.callback_data_cache.clear_callback_queries() + + @pytest.mark.parametrize("bot_class", [Bot, ExtBot]) + async def test_http2_runtime_error(self, recwarn, bot_class): + bot_class("12345:ABCDE", base_url="http://", request=HTTPXRequest(http_version="2")) + bot_class( + "12345:ABCDE", + base_url="http://", + get_updates_request=HTTPXRequest(http_version="2"), + ) + bot_class( + "12345:ABCDE", + base_url="http://", + request=HTTPXRequest(http_version="2"), + get_updates_request=HTTPXRequest(http_version="2"), + ) + assert len(recwarn) == 3 + assert "You set the HTTP version for the request HTTPXRequest instance" in str( + recwarn[0].message + ) + assert "You set the HTTP version for the get_updates_request HTTPXRequest instance" in str( + recwarn[1].message + ) + assert ( + "You set the HTTP version for the get_updates_request and request HTTPXRequest " + "instance" in str(recwarn[2].message) + ) + for warning in recwarn: + assert warning.filename == __file__, "wrong stacklevel!" + assert warning.category is PTBUserWarning + + async def test_set_get_my_name(self, bot, monkeypatch): + """We only test that we pass the correct values to TG since this endpoint is heavily + rate limited which makes automated tests rather infeasible.""" + default_name = "default_bot_name" + en_name = "en_bot_name" + de_name = "de_bot_name" + + # We predefine the responses that we would TG expect to send us + set_stack = asyncio.Queue() + get_stack = asyncio.Queue() + await set_stack.put({"name": default_name}) + await set_stack.put({"name": en_name, "language_code": "en"}) + await set_stack.put({"name": de_name, "language_code": "de"}) + await get_stack.put({"name": default_name, "language_code": None}) + await get_stack.put({"name": en_name, "language_code": "en"}) + await get_stack.put({"name": de_name, "language_code": "de"}) + + await set_stack.put({"name": default_name}) + await set_stack.put({"language_code": "en"}) + await set_stack.put({"language_code": "de"}) + await get_stack.put({"name": default_name, "language_code": None}) + await get_stack.put({"name": default_name, "language_code": "en"}) + await get_stack.put({"name": default_name, "language_code": "de"}) + + async def post(url, request_data: RequestData, *args, **kwargs): + # The mock-post now just fetches the predefined responses from the queues + if "setMyName" in url: + expected = await set_stack.get() + assert request_data.json_parameters == expected + set_stack.task_done() + return True + + bot_name = await get_stack.get() + if "language_code" in request_data.json_parameters: + assert request_data.json_parameters == {"language_code": bot_name["language_code"]} + else: + assert request_data.json_parameters == {} + get_stack.task_done() + return bot_name + + monkeypatch.setattr(bot.request, "post", post) + + # Set the names + assert all( + await asyncio.gather( + bot.set_my_name(default_name), + bot.set_my_name(en_name, language_code="en"), + bot.set_my_name(de_name, language_code="de"), + ) + ) + + # Check that they were set correctly + assert await asyncio.gather( + bot.get_my_name(), bot.get_my_name("en"), bot.get_my_name("de") + ) == [ + BotName(default_name), + BotName(en_name), + BotName(de_name), + ] + + # Delete the names + assert all( + await asyncio.gather( + bot.set_my_name(default_name), + bot.set_my_name(None, language_code="en"), + bot.set_my_name(None, language_code="de"), + ) + ) + + # Check that they were deleted correctly + assert await asyncio.gather( + bot.get_my_name(), bot.get_my_name("en"), bot.get_my_name("de") + ) == 3 * [BotName(default_name)] + + async def test_set_message_reaction(self, bot, monkeypatch): + """This is here so we can test the convenient conversion we do in the function without + having to do multiple requests to Telegram""" + + expected_param = [ + [{"emoji": ReactionEmoji.THUMBS_UP, "type": "emoji"}], + [{"emoji": ReactionEmoji.RED_HEART, "type": "emoji"}], + [{"custom_emoji_id": "custom_emoji_1", "type": "custom_emoji"}], + [{"custom_emoji_id": "custom_emoji_2", "type": "custom_emoji"}], + [{"emoji": ReactionEmoji.THUMBS_DOWN, "type": "emoji"}], + [{"custom_emoji_id": "custom_emoji_3", "type": "custom_emoji"}], + [ + {"emoji": ReactionEmoji.RED_HEART, "type": "emoji"}, + {"custom_emoji_id": "custom_emoji_4", "type": "custom_emoji"}, + {"emoji": ReactionEmoji.THUMBS_DOWN, "type": "emoji"}, + {"custom_emoji_id": "custom_emoji_5", "type": "custom_emoji"}, + ], + [], + ] + + amount = 0 + + async def post(url, request_data: RequestData, *args, **kwargs): + # The mock-post now just fetches the predefined responses from the queues + assert request_data.json_parameters["chat_id"] == "1" + assert request_data.json_parameters["message_id"] == "2" + assert request_data.json_parameters["is_big"] + nonlocal amount + assert request_data.parameters["reaction"] == expected_param[amount] + amount += 1 + + monkeypatch.setattr(bot.request, "post", post) + await bot.set_message_reaction(1, 2, [ReactionTypeEmoji(ReactionEmoji.THUMBS_UP)], True) + await bot.set_message_reaction(1, 2, ReactionTypeEmoji(ReactionEmoji.RED_HEART), True) + await bot.set_message_reaction(1, 2, [ReactionTypeCustomEmoji("custom_emoji_1")], True) + await bot.set_message_reaction(1, 2, ReactionTypeCustomEmoji("custom_emoji_2"), True) + await bot.set_message_reaction(1, 2, ReactionEmoji.THUMBS_DOWN, True) + await bot.set_message_reaction(1, 2, "custom_emoji_3", True) + await bot.set_message_reaction( + 1, + 2, + [ + ReactionTypeEmoji(ReactionEmoji.RED_HEART), + ReactionTypeCustomEmoji("custom_emoji_4"), + ReactionEmoji.THUMBS_DOWN, + ReactionTypeCustomEmoji("custom_emoji_5"), + ], + True, + ) + + @pytest.mark.parametrize( + ("default_bot", "custom"), + [ + ({"parse_mode": ParseMode.HTML}, None), + ({"parse_mode": ParseMode.HTML}, ParseMode.MARKDOWN_V2), + ({"parse_mode": None}, ParseMode.MARKDOWN_V2), + ], + indirect=["default_bot"], + ) + async def test_send_message_default_quote_parse_mode( + self, default_bot, chat_id, message, custom, monkeypatch + ): + async def make_assertion(url, request_data: RequestData, *args, **kwargs): + assert request_data.parameters["reply_parameters"].get("quote_parse_mode") == ( + custom or default_bot.defaults.quote_parse_mode + ) + return make_message("dummy reply").to_dict() + + kwargs = {"message_id": 1} + if custom is not None: + kwargs["quote_parse_mode"] = custom + + monkeypatch.setattr(default_bot.request, "post", make_assertion) + await default_bot.send_message( + chat_id, message, reply_parameters=ReplyParameters(**kwargs) + ) + + @pytest.mark.parametrize( + ("default_bot", "custom"), + [ + ({"parse_mode": ParseMode.HTML}, "NOTHING"), + ({"parse_mode": ParseMode.HTML}, None), + ({"parse_mode": ParseMode.HTML}, ParseMode.MARKDOWN_V2), + ({"parse_mode": None}, ParseMode.MARKDOWN_V2), + ], + indirect=["default_bot"], + ) + async def test_send_poll_default_text_question_parse_mode( + self, default_bot, raw_bot, chat_id, custom, monkeypatch + ): + async def make_assertion(url, request_data: RequestData, *args, **kwargs): + expected = default_bot.defaults.text_parse_mode if custom == "NOTHING" else custom + + option_1 = request_data.parameters["options"][0] + option_2 = request_data.parameters["options"][1] + assert option_1.get("text_parse_mode") == (default_bot.defaults.text_parse_mode) + assert option_2.get("text_parse_mode") == expected + assert request_data.parameters.get("question_parse_mode") == expected + + return make_message("dummy reply").to_dict() + + async def make_raw_assertion(url, request_data: RequestData, *args, **kwargs): + expected = None if custom == "NOTHING" else custom + + option_1 = request_data.parameters["options"][0] + option_2 = request_data.parameters["options"][1] + assert option_1.get("text_parse_mode") is None + assert option_2.get("text_parse_mode") == expected + + assert request_data.parameters.get("question_parse_mode") == expected + + return make_message("dummy reply").to_dict() + + if custom == "NOTHING": + option_2 = InputPollOption("option2") + kwargs = {} + else: + option_2 = InputPollOption("option2", text_parse_mode=custom) + kwargs = {"question_parse_mode": custom} + + monkeypatch.setattr(default_bot.request, "post", make_assertion) + await default_bot.send_poll( + chat_id, question="question", options=["option1", option_2], **kwargs + ) + + monkeypatch.setattr(raw_bot.request, "post", make_raw_assertion) + await raw_bot.send_poll( + chat_id, question="question", options=["option1", option_2], **kwargs + ) + + @pytest.mark.parametrize( + ("default_bot", "custom"), + [ + ({"parse_mode": ParseMode.HTML}, None), + ({"parse_mode": ParseMode.HTML}, ParseMode.MARKDOWN_V2), + ({"parse_mode": None}, ParseMode.MARKDOWN_V2), + ], + indirect=["default_bot"], + ) + async def test_send_poll_default_quote_parse_mode( + self, default_bot, chat_id, custom, monkeypatch + ): + async def make_assertion(url, request_data: RequestData, *args, **kwargs): + assert request_data.parameters["reply_parameters"].get("quote_parse_mode") == ( + custom or default_bot.defaults.quote_parse_mode + ) + return make_message("dummy reply").to_dict() + + kwargs = {"message_id": 1} + if custom is not None: + kwargs["quote_parse_mode"] = custom + + monkeypatch.setattr(default_bot.request, "post", make_assertion) + await default_bot.send_poll( + chat_id, + question="question", + options=["option1", "option2"], + reply_parameters=ReplyParameters(**kwargs), + ) + + async def test_send_poll_question_parse_mode_entities(self, bot, monkeypatch): + # Currently only custom emoji are supported as entities which we can't test + # We just test that the correct data is passed for now + + async def make_assertion(url, request_data: RequestData, *args, **kwargs): + assert request_data.parameters["question_entities"] == [ + {"type": "custom_emoji", "offset": 0, "length": 1}, + {"type": "custom_emoji", "offset": 2, "length": 1}, + ] + assert request_data.parameters["question_parse_mode"] == ParseMode.MARKDOWN_V2 + return make_message("dummy reply").to_dict() + + monkeypatch.setattr(bot.request, "post", make_assertion) + await bot.send_poll( + 1, + question="😀😃", + options=["option1", "option2"], + question_entities=[ + MessageEntity(MessageEntity.CUSTOM_EMOJI, 0, 1), + MessageEntity(MessageEntity.CUSTOM_EMOJI, 2, 1), + ], + question_parse_mode=ParseMode.MARKDOWN_V2, + ) + + @pytest.mark.parametrize( + ("default_bot", "custom"), + [ + ({"parse_mode": ParseMode.HTML}, None), + ({"parse_mode": ParseMode.HTML}, ParseMode.MARKDOWN_V2), + ({"parse_mode": None}, ParseMode.MARKDOWN_V2), + ], + indirect=["default_bot"], + ) + async def test_send_game_default_quote_parse_mode( + self, default_bot, chat_id, custom, monkeypatch + ): + async def make_assertion(url, request_data: RequestData, *args, **kwargs): + assert request_data.parameters["reply_parameters"].get("quote_parse_mode") == ( + custom or default_bot.defaults.quote_parse_mode + ) + return make_message("dummy reply").to_dict() + + kwargs = {"message_id": 1} + if custom is not None: + kwargs["quote_parse_mode"] = custom + + monkeypatch.setattr(default_bot.request, "post", make_assertion) + await default_bot.send_game( + chat_id, "game_short_name", reply_parameters=ReplyParameters(**kwargs) + ) + + @pytest.mark.parametrize( + ("default_bot", "custom"), + [ + ({"parse_mode": ParseMode.HTML}, None), + ({"parse_mode": ParseMode.HTML}, ParseMode.MARKDOWN_V2), + ({"parse_mode": None}, ParseMode.MARKDOWN_V2), + ], + indirect=["default_bot"], + ) + async def test_copy_message_default_quote_parse_mode( + self, default_bot, chat_id, custom, monkeypatch + ): + async def make_assertion(url, request_data: RequestData, *args, **kwargs): + assert request_data.parameters["reply_parameters"].get("quote_parse_mode") == ( + custom or default_bot.defaults.quote_parse_mode + ) + return make_message("dummy reply").to_dict() + + kwargs = {"message_id": 1} + if custom is not None: + kwargs["quote_parse_mode"] = custom + + monkeypatch.setattr(default_bot.request, "post", make_assertion) + await default_bot.copy_message(chat_id, 1, 1, reply_parameters=ReplyParameters(**kwargs)) + + async def test_do_api_request_camel_case_conversion(self, bot, monkeypatch): + async def make_assertion(url, request_data: RequestData, *args, **kwargs): + return url.endswith("camelCase") + + monkeypatch.setattr(bot.request, "post", make_assertion) + assert await bot.do_api_request("camel_case") + + @pytest.mark.filterwarnings("ignore::telegram.warnings.PTBUserWarning") + async def test_do_api_request_media_write_timeout(self, bot, chat_id, monkeypatch): + test_flag = None + + class CustomRequest(BaseRequest): + async def initialize(self_) -> None: + pass + + async def shutdown(self_) -> None: + pass + + async def do_request(self_, *args, **kwargs) -> Tuple[int, bytes]: + nonlocal test_flag + test_flag = ( + kwargs.get("read_timeout"), + kwargs.get("connect_timeout"), + kwargs.get("write_timeout"), + kwargs.get("pool_timeout"), + ) + return HTTPStatus.OK, b'{"ok": "True", "result": {}}' + + custom_request = CustomRequest() + + bot = Bot(bot.token, request=custom_request) + await bot.do_api_request( + "send_document", + api_kwargs={ + "chat_id": chat_id, + "caption": "test_caption", + "document": InputFile(data_file("telegram.png").open("rb")), + }, + ) + assert test_flag == ( + DEFAULT_NONE, + DEFAULT_NONE, + 20, + DEFAULT_NONE, + ) + + @pytest.mark.filterwarnings("ignore::telegram.warnings.PTBUserWarning") + async def test_do_api_request_default_timezone(self, tz_bot, monkeypatch): + until = dtm.datetime(2020, 1, 11, 16, 13) + until_timestamp = to_timestamp(until, tzinfo=tz_bot.defaults.tzinfo) + + async def make_assertion(url, request_data: RequestData, *args, **kwargs): + data = request_data.parameters + chat_id = data["chat_id"] == 2 + user_id = data["user_id"] == 32 + until_date = data.get("until_date", until_timestamp) == until_timestamp + return chat_id and user_id and until_date + + monkeypatch.setattr(tz_bot.request, "post", make_assertion) + + assert await tz_bot.do_api_request( + "banChatMember", api_kwargs={"chat_id": 2, "user_id": 32} + ) + assert await tz_bot.do_api_request( + "banChatMember", api_kwargs={"chat_id": 2, "user_id": 32, "until_date": until} + ) + assert await tz_bot.do_api_request( + "banChatMember", + api_kwargs={"chat_id": 2, "user_id": 32, "until_date": until_timestamp}, + ) + + async def test_business_connection_id_argument(self, bot, monkeypatch): + """We can't connect to a business acc, so we just test that the correct data is passed. + We also can't test every single method easily, so we just test a few. Our linting will + catch any unused args with the others.""" + + async def make_assertion(url, request_data: RequestData, *args, **kwargs): + assert request_data.parameters.get("business_connection_id") == 42 + return {} + + monkeypatch.setattr(bot.request, "post", make_assertion) + + await bot.send_message(2, "text", business_connection_id=42) + await bot.stop_poll(chat_id=1, message_id=2, business_connection_id=42) + await bot.pin_chat_message(chat_id=1, message_id=2, business_connection_id=42) + await bot.unpin_chat_message(chat_id=1, business_connection_id=42) + + async def test_message_effect_id_argument(self, bot, monkeypatch): + """We can't test every single method easily, so we just test one. Our linting will catch + any unused args with the others.""" + + async def make_assertion(url, request_data: RequestData, *args, **kwargs): + return request_data.parameters.get("message_effect_id") == 42 + + monkeypatch.setattr(bot.request, "post", make_assertion) + assert await bot.send_message(2, "text", message_effect_id=42) + + async def test_get_business_connection(self, bot, monkeypatch): + bci = "42" + user = User(1, "first", False) + user_chat_id = 1 + date = dtm.datetime.utcnow() + can_reply = True + is_enabled = True + bc = BusinessConnection(bci, user, user_chat_id, date, can_reply, is_enabled).to_json() + + async def do_request(*args, **kwargs): + data = kwargs.get("request_data") + obj = data.parameters.get("business_connection_id") + if obj == bci: + return 200, f'{{"ok": true, "result": {bc}}}'.encode() + return 400, b'{"ok": false, "result": []}' + + monkeypatch.setattr(bot.request, "do_request", do_request) + obj = await bot.get_business_connection(business_connection_id=bci) + assert isinstance(obj, BusinessConnection) + + async def test_refund_star_payment(self, bot, monkeypatch): + # can't make actual request so we just test that the correct data is passed + async def make_assertion(url, request_data: RequestData, *args, **kwargs): + return ( + request_data.parameters.get("user_id") == 42 + and request_data.parameters.get("telegram_payment_charge_id") == "37" + ) + + monkeypatch.setattr(bot.request, "post", make_assertion) + assert await bot.refund_star_payment(42, "37") + + async def test_get_star_transactions(self, bot, monkeypatch): + # we just want to test the offset parameter + st = StarTransactions([StarTransaction("1", 1, dtm.datetime.now())]).to_json() + + async def do_request(url, request_data: RequestData, *args, **kwargs): + offset = request_data.parameters.get("offset") == 3 + if offset: + return 200, f'{{"ok": true, "result": {st}}}'.encode() + return 400, b'{"ok": false, "result": []}' + + monkeypatch.setattr(bot.request, "do_request", do_request) + obj = await bot.get_star_transactions(offset=3) + assert isinstance(obj, StarTransactions) + + async def test_create_chat_subscription_invite_link( + self, + monkeypatch, + bot, + ): + # Since the chat invite link object does not say if the sub args are passed we can + # only check here + async def make_assertion(url, request_data: RequestData, *args, **kwargs): + assert request_data.parameters.get("subscription_period") == 2592000 + assert request_data.parameters.get("subscription_price") == 6 + + monkeypatch.setattr(bot.request, "post", make_assertion) + + await bot.create_chat_subscription_invite_link(1234, 2592000, 6) + + +class TestBotWithRequest: + """ + Most are executed on tg.ext.ExtBot, as that class only extends the functionality of tg.bot + + Behavior for init of ExtBot with missing optional dependency cachetools (for CallbackDataCache) + is tested in `test_callbackdatacache` + """ + + async def test_invalid_token_server_response(self): + with pytest.raises(InvalidToken, match="The token `12` was rejected by the server."): + async with ExtBot(token="12"): + pass + + async def test_multiple_init_cycles(self, bot): + # nothing really to assert - this should just not fail + test_bot = Bot(bot.token) + async with test_bot: + await test_bot.get_me() + async with test_bot: + await test_bot.get_me() + + async def test_forward_message(self, bot, chat_id, message): + forward_message = await bot.forward_message( + chat_id, from_chat_id=chat_id, message_id=message.message_id + ) + + assert forward_message.text == message.text + assert forward_message.forward_origin.sender_user == message.from_user + assert isinstance(forward_message.forward_origin.date, dtm.datetime) + + async def test_forward_protected_message(self, bot, chat_id): + tasks = asyncio.gather( + bot.send_message(chat_id, "cant forward me", protect_content=True), + bot.send_message(chat_id, "forward me", protect_content=False), + ) + to_forward_protected, to_forward_unprotected = await tasks + + assert to_forward_protected.has_protected_content + assert not to_forward_unprotected.has_protected_content + + forwarded_but_now_protected = await to_forward_unprotected.forward( + chat_id, protect_content=True + ) + assert forwarded_but_now_protected.has_protected_content + + tasks = asyncio.gather( + to_forward_protected.forward(chat_id), + forwarded_but_now_protected.forward(chat_id), + return_exceptions=True, + ) + result = await tasks + assert all("can't be forwarded" in str(exc) for exc in result) + + async def test_forward_messages(self, bot, chat_id): + tasks = asyncio.gather( + bot.send_message(chat_id, text="will be forwarded"), + bot.send_message(chat_id, text="will be forwarded"), + ) + + msg1, msg2 = await tasks + + forward_messages = await bot.forward_messages( + chat_id, from_chat_id=chat_id, message_ids=sorted((msg1.message_id, msg2.message_id)) + ) + + assert isinstance(forward_messages, tuple) + + tasks = asyncio.gather( + bot.send_message( + chat_id, "temp 1", reply_to_message_id=forward_messages[0].message_id + ), + bot.send_message( + chat_id, "temp 2", reply_to_message_id=forward_messages[1].message_id + ), + ) + + temp_msg1, temp_msg2 = await tasks + forward_msg1 = temp_msg1.reply_to_message + forward_msg2 = temp_msg2.reply_to_message + + assert forward_msg1.text == msg1.text + assert forward_msg1.forward_origin.sender_user == msg1.from_user + assert isinstance(forward_msg1.forward_origin.date, dtm.datetime) + + assert forward_msg2.text == msg2.text + assert forward_msg2.forward_origin.sender_user == msg2.from_user + assert isinstance(forward_msg2.forward_origin.date, dtm.datetime) + + async def test_delete_message(self, bot, chat_id): + message = await bot.send_message(chat_id, text="will be deleted") + await asyncio.sleep(2) + + assert await bot.delete_message(chat_id=chat_id, message_id=message.message_id) is True + + async def test_delete_message_old_message(self, bot, chat_id): + with pytest.raises(BadRequest): + # Considering that the first message is old enough + await bot.delete_message(chat_id=chat_id, message_id=1) + + # send_photo, send_audio, send_document, send_sticker, send_video, send_voice, send_video_note, + # send_media_group, send_animation, get_user_chat_boosts are tested in their respective + # test modules. No need to duplicate here. + + async def test_delete_messages(self, bot, chat_id): + msg1, msg2 = await asyncio.gather( + bot.send_message(chat_id, text="will be deleted"), + bot.send_message(chat_id, text="will be deleted"), + ) + + assert ( + await bot.delete_messages(chat_id=chat_id, message_ids=sorted((msg1.id, msg2.id))) + is True + ) + + async def test_send_venue(self, bot, chat_id): + longitude = -46.788279 + latitude = -23.691288 + title = "title" + address = "address" + foursquare_id = "foursquare id" + foursquare_type = "foursquare type" + google_place_id = "google_place id" + google_place_type = "google_place type" + + tasks = asyncio.gather( + *( + bot.send_venue( + chat_id=chat_id, + title=title, + address=address, + latitude=latitude, + longitude=longitude, + protect_content=True, + **i, + ) + for i in ( + {"foursquare_id": foursquare_id, "foursquare_type": foursquare_type}, + {"google_place_id": google_place_id, "google_place_type": google_place_type}, + ) + ), + ) + + message, message2 = await tasks + assert message.venue + assert message.venue.title == title + assert message.venue.address == address + assert message.venue.location.latitude == latitude + assert message.venue.location.longitude == longitude + assert message.venue.foursquare_id == foursquare_id + assert message.venue.foursquare_type == foursquare_type + assert message.venue.google_place_id is None + assert message.venue.google_place_type is None + assert message.has_protected_content + + assert message2.venue + assert message2.venue.title == title + assert message2.venue.address == address + assert message2.venue.location.latitude == latitude + assert message2.venue.location.longitude == longitude + assert message2.venue.google_place_id == google_place_id + assert message2.venue.google_place_type == google_place_type + assert message2.venue.foursquare_id is None + assert message2.venue.foursquare_type is None + assert message2.has_protected_content + + async def test_send_contact(self, bot, chat_id): + phone_number = "+11234567890" + first_name = "Leandro" + last_name = "Toledo" + message = await bot.send_contact( + chat_id=chat_id, + phone_number=phone_number, + first_name=first_name, + last_name=last_name, + protect_content=True, + ) + + assert message.contact + assert message.contact.phone_number == phone_number + assert message.contact.first_name == first_name + assert message.contact.last_name == last_name + assert message.has_protected_content + + async def test_send_chat_action_all_args(self, bot, chat_id, monkeypatch): + async def make_assertion(*args, **_): + kwargs = args[1] + return ( + kwargs["chat_id"] == chat_id + and kwargs["action"] == "action" + and kwargs["message_thread_id"] == 1 + ) + + monkeypatch.setattr(bot, "_post", make_assertion) + assert await bot.send_chat_action(chat_id, "action", 1) + + # TODO: Add bot to group to test polls too + @pytest.mark.parametrize( + "reply_markup", + [ + None, + InlineKeyboardMarkup.from_button( + InlineKeyboardButton(text="text", callback_data="data") + ), + InlineKeyboardMarkup.from_button( + InlineKeyboardButton(text="text", callback_data="data") + ).to_dict(), + ], + ) + async def test_send_and_stop_poll(self, bot, super_group_id, reply_markup): + question = "Is this a test?" + answers = ["Yes", InputPollOption("No"), "Maybe"] + explanation = "[Here is a link](https://google.com)" + explanation_entities = [ + MessageEntity(MessageEntity.TEXT_LINK, 0, 14, url="https://google.com") + ] + + poll_task = asyncio.create_task( + bot.send_poll( + chat_id=super_group_id, + question=question, + options=answers, + is_anonymous=False, + allows_multiple_answers=True, + read_timeout=60, + protect_content=True, + ) + ) + quiz_task = asyncio.create_task( + bot.send_poll( + chat_id=super_group_id, + question=question, + options=answers, + type=Poll.QUIZ, + correct_option_id=2, + is_closed=True, + explanation=explanation, + explanation_parse_mode=ParseMode.MARKDOWN_V2, + ) + ) + + message = await poll_task + assert message.poll + assert message.poll.question == question + assert message.poll.options[0].text == answers[0] + assert message.poll.options[1].text == answers[1].text + assert message.poll.options[2].text == answers[2] + assert not message.poll.is_anonymous + assert message.poll.allows_multiple_answers + assert not message.poll.is_closed + assert message.poll.type == Poll.REGULAR + assert message.has_protected_content + + # Since only the poll and not the complete message is returned, we can't check that the + # reply_markup is correct. So we just test that sending doesn't give an error. + poll = await bot.stop_poll( + chat_id=super_group_id, + message_id=message.message_id, + reply_markup=reply_markup, + read_timeout=60, + ) + assert isinstance(poll, Poll) + assert poll.is_closed + assert poll.options[0].text == answers[0] + assert poll.options[0].voter_count == 0 + assert poll.options[1].text == answers[1].text + assert poll.options[1].voter_count == 0 + assert poll.options[2].text == answers[2] + assert poll.options[2].voter_count == 0 + assert poll.question == question + assert poll.total_voter_count == 0 + + message_quiz = await quiz_task + assert message_quiz.poll.correct_option_id == 2 + assert message_quiz.poll.type == Poll.QUIZ + assert message_quiz.poll.is_closed + assert message_quiz.poll.explanation == "Here is a link" + assert message_quiz.poll.explanation_entities == tuple(explanation_entities) + assert poll_task.done() + assert quiz_task.done() + + @pytest.mark.parametrize( + ("open_period", "close_date"), [(5, None), (None, True)], ids=["open_period", "close_date"] + ) + async def test_send_open_period(self, bot, super_group_id, open_period, close_date): + question = "Is this a test?" + answers = ["Yes", "No", "Maybe"] + reply_markup = InlineKeyboardMarkup.from_button( + InlineKeyboardButton(text="text", callback_data="data") + ) + + if close_date: + close_date = dtm.datetime.utcnow() + dtm.timedelta(seconds=5.05) + + message = await bot.send_poll( + chat_id=super_group_id, + question=question, + options=answers, + is_anonymous=False, + allows_multiple_answers=True, + read_timeout=60, + open_period=open_period, + close_date=close_date, + ) + await asyncio.sleep(5.1) + new_message = await bot.edit_message_reply_markup( + chat_id=super_group_id, + message_id=message.message_id, + reply_markup=reply_markup, + read_timeout=60, + ) + assert new_message.poll.id == message.poll.id + assert new_message.poll.is_closed + + async def test_send_close_date_default_tz(self, tz_bot, super_group_id): + question = "Is this a test?" + answers = ["Yes", "No", "Maybe"] + reply_markup = InlineKeyboardMarkup.from_button( + InlineKeyboardButton(text="text", callback_data="data") + ) + + aware_close_date = dtm.datetime.now(tz=tz_bot.defaults.tzinfo) + dtm.timedelta(seconds=5) + close_date = aware_close_date.replace(tzinfo=None) + + msg = await tz_bot.send_poll( # The timezone returned from this is always converted to UTC + chat_id=super_group_id, + question=question, + options=answers, + close_date=close_date, + read_timeout=60, + ) + msg.poll._unfreeze() + # Sometimes there can be a few seconds delay, so don't let the test fail due to that- + msg.poll.close_date = msg.poll.close_date.astimezone(aware_close_date.tzinfo) + assert abs(msg.poll.close_date - aware_close_date) <= dtm.timedelta(seconds=5) + + await asyncio.sleep(5.1) + + new_message = await tz_bot.edit_message_reply_markup( + chat_id=super_group_id, + message_id=msg.message_id, + reply_markup=reply_markup, + read_timeout=60, + ) + assert new_message.poll.id == msg.poll.id + assert new_message.poll.is_closed + + async def test_send_poll_explanation_entities(self, bot, chat_id): + test_string = "Italic Bold Code" + entities = [ + MessageEntity(MessageEntity.ITALIC, 0, 6), + MessageEntity(MessageEntity.ITALIC, 7, 4), + MessageEntity(MessageEntity.ITALIC, 12, 4), + ] + message = await bot.send_poll( + chat_id, + "question", + options=["a", "b"], + correct_option_id=0, + type=Poll.QUIZ, + explanation=test_string, + explanation_entities=entities, + ) + + assert message.poll.explanation == test_string + assert message.poll.explanation_entities == tuple(entities) + + @pytest.mark.parametrize("default_bot", [{"parse_mode": "Markdown"}], indirect=True) + async def test_send_poll_default_parse_mode(self, default_bot, super_group_id): + explanation = "Italic Bold Code" + explanation_markdown = "_Italic_ *Bold* `Code`" + question = "Is this a test?" + answers = ["Yes", "No", "Maybe"] + + tasks = asyncio.gather( + *( + default_bot.send_poll( + chat_id=super_group_id, + question=question, + options=answers, + type=Poll.QUIZ, + correct_option_id=2, + is_closed=True, + explanation=explanation_markdown, + **i, + ) + for i in ({}, {"explanation_parse_mode": None}, {"explanation_parse_mode": "HTML"}) + ), + ) + message1, message2, message3 = await tasks + assert message1.poll.explanation == explanation + assert message1.poll.explanation_entities == ( + MessageEntity(MessageEntity.ITALIC, 0, 6), + MessageEntity(MessageEntity.BOLD, 7, 4), + MessageEntity(MessageEntity.CODE, 12, 4), + ) + + assert message2.poll.explanation == explanation_markdown + assert message2.poll.explanation_entities == () + + assert message3.poll.explanation == explanation_markdown + assert message3.poll.explanation_entities == () + + @pytest.mark.parametrize( + ("default_bot", "custom"), + [ + ({"allow_sending_without_reply": True}, None), + ({"allow_sending_without_reply": False}, None), + ({"allow_sending_without_reply": False}, True), + ], + indirect=["default_bot"], + ) + async def test_send_poll_default_allow_sending_without_reply( + self, default_bot, chat_id, custom + ): + question = "Is this a test?" + answers = ["Yes", "No", "Maybe"] + reply_to_message = await default_bot.send_message(chat_id, "test") + await reply_to_message.delete() + if custom is not None: + message = await default_bot.send_poll( + chat_id, + question=question, + options=answers, + allow_sending_without_reply=custom, + reply_to_message_id=reply_to_message.message_id, + ) + assert message.reply_to_message is None + elif default_bot.defaults.allow_sending_without_reply: + message = await default_bot.send_poll( + chat_id, + question=question, + options=answers, + reply_to_message_id=reply_to_message.message_id, + ) + assert message.reply_to_message is None + else: + with pytest.raises(BadRequest, match="Message to be replied not found"): + await default_bot.send_poll( + chat_id, + question=question, + options=answers, + reply_to_message_id=reply_to_message.message_id, + ) + + @pytest.mark.parametrize("default_bot", [{"protect_content": True}], indirect=True) + async def test_send_poll_default_protect_content(self, chat_id, default_bot): + tasks = asyncio.gather( + default_bot.send_poll(chat_id, "Test", ["1", "2"]), + default_bot.send_poll(chat_id, "test", ["1", "2"], protect_content=False), + ) + protected_poll, unprotect_poll = await tasks + assert protected_poll.has_protected_content + assert not unprotect_poll.has_protected_content + + @pytest.mark.parametrize("emoji", [*Dice.ALL_EMOJI, None]) + async def test_send_dice(self, bot, chat_id, emoji): + message = await bot.send_dice(chat_id, emoji=emoji, protect_content=True) + + assert message.dice + assert message.has_protected_content + if emoji is None: + assert message.dice.emoji == Dice.DICE + else: + assert message.dice.emoji == emoji + + @pytest.mark.parametrize( + ("default_bot", "custom"), + [ + ({"allow_sending_without_reply": True}, None), + ({"allow_sending_without_reply": False}, None), + ({"allow_sending_without_reply": False}, True), + ], + indirect=["default_bot"], + ) + async def test_send_dice_default_allow_sending_without_reply( + self, default_bot, chat_id, custom + ): + reply_to_message = await default_bot.send_message(chat_id, "test") + await reply_to_message.delete() + if custom is not None: + message = await default_bot.send_dice( + chat_id, + allow_sending_without_reply=custom, + reply_to_message_id=reply_to_message.message_id, + ) + assert message.reply_to_message is None + elif default_bot.defaults.allow_sending_without_reply: + message = await default_bot.send_dice( + chat_id, + reply_to_message_id=reply_to_message.message_id, + ) + assert message.reply_to_message is None + else: + with pytest.raises(BadRequest, match="Message to be replied not found"): + await default_bot.send_dice( + chat_id, reply_to_message_id=reply_to_message.message_id + ) + + @pytest.mark.parametrize("default_bot", [{"protect_content": True}], indirect=True) + async def test_send_dice_default_protect_content(self, chat_id, default_bot): + tasks = asyncio.gather( + default_bot.send_dice(chat_id), default_bot.send_dice(chat_id, protect_content=False) + ) + protected_dice, unprotected_dice = await tasks + assert protected_dice.has_protected_content + assert not unprotected_dice.has_protected_content + + @pytest.mark.parametrize("chat_action", list(ChatAction)) + async def test_send_chat_action(self, bot, chat_id, chat_action): + assert await bot.send_chat_action(chat_id, chat_action) + + async def test_wrong_chat_action(self, bot, chat_id): + with pytest.raises(BadRequest, match="Wrong parameter action"): + await bot.send_chat_action(chat_id, "unknown action") + + async def test_answer_inline_query_current_offset_error(self, bot, inline_results): + with pytest.raises(ValueError, match="`current_offset` and `next_offset`"): + await bot.answer_inline_query( + 1234, results=inline_results, next_offset=42, current_offset=51 + ) + + async def test_get_user_profile_photos(self, bot, chat_id): + user_profile_photos = await bot.get_user_profile_photos(chat_id) + assert user_profile_photos.photos[0][0].file_size == 5403 + + async def test_get_one_user_profile_photo(self, bot, chat_id): + user_profile_photos = await bot.get_user_profile_photos(chat_id, offset=0, limit=1) + assert user_profile_photos.total_count == 1 + assert user_profile_photos.photos[0][0].file_size == 5403 + + async def test_edit_message_text(self, bot, message): + message = await bot.edit_message_text( + text="new_text", + chat_id=message.chat_id, + message_id=message.message_id, + parse_mode="HTML", + disable_web_page_preview=True, + ) + + assert message.text == "new_text" + + async def test_edit_message_text_entities(self, bot, message): + test_string = "Italic Bold Code" + entities = [ + MessageEntity(MessageEntity.ITALIC, 0, 6), + MessageEntity(MessageEntity.ITALIC, 7, 4), + MessageEntity(MessageEntity.ITALIC, 12, 4), + ] + message = await bot.edit_message_text( + text=test_string, + chat_id=message.chat_id, + message_id=message.message_id, + entities=entities, + ) + + assert message.text == test_string + assert message.entities == tuple(entities) + + @pytest.mark.parametrize("default_bot", [{"parse_mode": "Markdown"}], indirect=True) + async def test_edit_message_text_default_parse_mode(self, default_bot, message): + test_string = "Italic Bold Code" + test_markdown_string = "_Italic_ *Bold* `Code`" + + message = await default_bot.edit_message_text( + text=test_markdown_string, + chat_id=message.chat_id, + message_id=message.message_id, + disable_web_page_preview=True, + ) + assert message.text_markdown == test_markdown_string + assert message.text == test_string + + message = await default_bot.edit_message_text( + text=test_markdown_string, + chat_id=message.chat_id, + message_id=message.message_id, + parse_mode=None, + disable_web_page_preview=True, + ) + assert message.text == test_markdown_string + assert message.text_markdown == escape_markdown(test_markdown_string) + + message = await default_bot.edit_message_text( + text=test_markdown_string, + chat_id=message.chat_id, + message_id=message.message_id, + disable_web_page_preview=True, + ) + message = await default_bot.edit_message_text( + text=test_markdown_string, + chat_id=message.chat_id, + message_id=message.message_id, + parse_mode="HTML", + disable_web_page_preview=True, + ) + assert message.text == test_markdown_string + assert message.text_markdown == escape_markdown(test_markdown_string) + + @pytest.mark.skip(reason="need reference to an inline message") + async def test_edit_message_text_inline(self): + pass + + async def test_edit_message_caption(self, bot, media_message): + message = await bot.edit_message_caption( + caption="new_caption", + chat_id=media_message.chat_id, + message_id=media_message.message_id, + show_caption_above_media=False, + ) + + assert message.caption == "new_caption" + assert not message.show_caption_above_media + + async def test_edit_message_caption_entities(self, bot, media_message): + test_string = "Italic Bold Code" + entities = [ + MessageEntity(MessageEntity.ITALIC, 0, 6), + MessageEntity(MessageEntity.ITALIC, 7, 4), + MessageEntity(MessageEntity.ITALIC, 12, 4), + ] + message = await bot.edit_message_caption( + caption=test_string, + chat_id=media_message.chat_id, + message_id=media_message.message_id, + caption_entities=entities, + ) + + assert message.caption == test_string + assert message.caption_entities == tuple(entities) + + # edit_message_media is tested in test_inputmedia + + @pytest.mark.parametrize("default_bot", [{"parse_mode": "Markdown"}], indirect=True) + async def test_edit_message_caption_default_parse_mode(self, default_bot, media_message): + test_string = "Italic Bold Code" + test_markdown_string = "_Italic_ *Bold* `Code`" + + message = await default_bot.edit_message_caption( + caption=test_markdown_string, + chat_id=media_message.chat_id, + message_id=media_message.message_id, + ) + assert message.caption_markdown == test_markdown_string + assert message.caption == test_string + + message = await default_bot.edit_message_caption( + caption=test_markdown_string, + chat_id=media_message.chat_id, + message_id=media_message.message_id, + parse_mode=None, + ) + assert message.caption == test_markdown_string + assert message.caption_markdown == escape_markdown(test_markdown_string) + + message = await default_bot.edit_message_caption( + caption=test_markdown_string, + chat_id=media_message.chat_id, + message_id=media_message.message_id, + ) + message = await default_bot.edit_message_caption( + caption=test_markdown_string, + chat_id=media_message.chat_id, + message_id=media_message.message_id, + parse_mode="HTML", + ) + assert message.caption == test_markdown_string + assert message.caption_markdown == escape_markdown(test_markdown_string) + + async def test_edit_message_caption_with_parse_mode(self, bot, media_message): + message = await bot.edit_message_caption( + caption="new *caption*", + parse_mode="Markdown", + chat_id=media_message.chat_id, + message_id=media_message.message_id, + ) + + assert message.caption == "new caption" + + @pytest.mark.skip(reason="need reference to an inline message") + async def test_edit_message_caption_inline(self): + pass + + async def test_edit_reply_markup(self, bot, message): + new_markup = InlineKeyboardMarkup([[InlineKeyboardButton(text="test", callback_data="1")]]) + message = await bot.edit_message_reply_markup( + chat_id=message.chat_id, message_id=message.message_id, reply_markup=new_markup + ) + + assert message is not True + + @pytest.mark.skip(reason="need reference to an inline message") + async def test_edit_reply_markup_inline(self): + pass + + @pytest.mark.xdist_group("getUpdates_and_webhook") + # TODO: Actually send updates to the test bot so this can be tested properly + async def test_get_updates(self, bot): + await bot.delete_webhook() # make sure there is no webhook set if webhook tests failed + updates = await bot.get_updates(timeout=1) + + assert isinstance(updates, tuple) + if updates: + assert isinstance(updates[0], Update) + + @pytest.mark.parametrize("bot_class", [Bot, ExtBot]) + async def test_get_updates_read_timeout_deprecation_warning( + self, bot, recwarn, monkeypatch, bot_class + ): + # Using the normal HTTPXRequest should not issue any warnings + await bot.get_updates() + assert len(recwarn) == 0 + + # Now let's test deprecation warning when using get_updates for other BaseRequest + # subclasses (we just monkeypatch the existing HTTPXRequest for this) + read_timeout = None + + async def catch_timeouts(*args, **kwargs): + nonlocal read_timeout + read_timeout = kwargs.get("read_timeout") + return HTTPStatus.OK, b'{"ok": "True", "result": {}}' + + monkeypatch.setattr(HTTPXRequest, "read_timeout", BaseRequest.read_timeout) + monkeypatch.setattr(HTTPXRequest, "do_request", catch_timeouts) + + bot = bot_class(get_updates_request=HTTPXRequest(), token=bot.token) + await bot.get_updates() + + assert len(recwarn) == 1 + assert "does not override the property `read_timeout`" in str(recwarn[0].message) + assert recwarn[0].category is PTBDeprecationWarning + assert recwarn[0].filename == __file__, "wrong stacklevel" + + assert read_timeout == 2 + + @pytest.mark.parametrize( + ("read_timeout", "timeout", "expected"), + [ + (None, None, 0), + (1, None, 1), + (None, 1, 1), + (DEFAULT_NONE, None, 10), + (DEFAULT_NONE, 1, 11), + (1, 2, 3), + ], + ) + async def test_get_updates_read_timeout_value_passing( + self, bot, read_timeout, timeout, expected, monkeypatch + ): + caught_read_timeout = None + + async def catch_timeouts(*args, **kwargs): + nonlocal caught_read_timeout + caught_read_timeout = kwargs.get("read_timeout") + return HTTPStatus.OK, b'{"ok": "True", "result": {}}' + + monkeypatch.setattr(HTTPXRequest, "do_request", catch_timeouts) + + bot = Bot(get_updates_request=HTTPXRequest(read_timeout=10), token=bot.token) + await bot.get_updates(read_timeout=read_timeout, timeout=timeout) + assert caught_read_timeout == expected + + @pytest.mark.xdist_group("getUpdates_and_webhook") + @pytest.mark.parametrize("use_ip", [True, False]) + # local file path as file_input is tested below in test_set_webhook_params + @pytest.mark.parametrize("file_input", ["bytes", "file_handle"]) + async def test_set_webhook_get_webhook_info_and_delete_webhook(self, bot, use_ip, file_input): + url = "https://python-telegram-bot.org/test/webhook" + # Get the ip address of the website - dynamically just in case it ever changes + ip = socket.gethostbyname("python-telegram-bot.org") + max_connections = 7 + allowed_updates = ["message"] + file_input = ( + data_file("sslcert.pem").read_bytes() + if file_input == "bytes" + else data_file("sslcert.pem").open("rb") + ) + await bot.set_webhook( + url, + max_connections=max_connections, + allowed_updates=allowed_updates, + ip_address=ip if use_ip else None, + certificate=file_input if use_ip else None, + ) + + await asyncio.sleep(1) + live_info = await bot.get_webhook_info() + assert live_info.url == url + assert live_info.max_connections == max_connections + assert live_info.allowed_updates == tuple(allowed_updates) + assert live_info.ip_address == ip + assert live_info.has_custom_certificate == use_ip + + await bot.delete_webhook() + await asyncio.sleep(1) + info = await bot.get_webhook_info() + assert not info.url + assert info.ip_address is None + assert info.has_custom_certificate is False + + async def test_leave_chat(self, bot): + with pytest.raises(BadRequest, match="Chat not found"): + await bot.leave_chat(-123456) + + with pytest.raises(NetworkError, match="Chat not found"): + await bot.leave_chat(-123456) + + async def test_get_chat(self, bot, super_group_id): + cfi = await bot.get_chat(super_group_id) + assert cfi.type == "supergroup" + assert cfi.title == f">>> telegram.Bot(test) @{bot.username}" + assert cfi.id == int(super_group_id) + + async def test_get_chat_administrators(self, bot, channel_id): + admins = await bot.get_chat_administrators(channel_id) + assert isinstance(admins, tuple) + + for a in admins: + assert a.status in ("administrator", "creator") + + async def test_get_chat_member_count(self, bot, channel_id): + count = await bot.get_chat_member_count(channel_id) + assert isinstance(count, int) + assert count > 3 + + async def test_get_chat_member(self, bot, channel_id, chat_id): + chat_member = await bot.get_chat_member(channel_id, chat_id) + + assert chat_member.status == "creator" + assert chat_member.user.first_name == "PTB" + assert chat_member.user.last_name == "Test user" + + @pytest.mark.skip(reason="Not implemented since we need a supergroup with many members") + async def test_set_chat_sticker_set(self): + pass + + @pytest.mark.skip(reason="Not implemented since we need a supergroup with many members") + async def test_delete_chat_sticker_set(self): + pass + + async def test_send_game(self, bot, chat_id): + game_short_name = "test_game" + message = await bot.send_game(chat_id, game_short_name, protect_content=True) + + assert message.game + assert ( + message.game.description + == "A no-op test game, for python-telegram-bot bot framework testing." + ) + assert message.game.animation.file_id + # We added some test bots later and for some reason the file size is not the same for them + # so we accept three different sizes here. Shouldn't be too much of + assert message.game.photo[0].file_size in [851, 4928, 850] + assert message.has_protected_content + + @pytest.mark.parametrize( + ("default_bot", "custom"), + [ + ({"allow_sending_without_reply": True}, None), + ({"allow_sending_without_reply": False}, None), + ({"allow_sending_without_reply": False}, True), + ], + indirect=["default_bot"], + ) + async def test_send_game_default_allow_sending_without_reply( + self, default_bot, chat_id, custom + ): + game_short_name = "test_game" + reply_to_message = await default_bot.send_message(chat_id, "test") + await reply_to_message.delete() + if custom is not None: + message = await default_bot.send_game( + chat_id, + game_short_name, + allow_sending_without_reply=custom, + reply_to_message_id=reply_to_message.message_id, + ) + assert message.reply_to_message is None + elif default_bot.defaults.allow_sending_without_reply: + message = await default_bot.send_game( + chat_id, + game_short_name, + reply_to_message_id=reply_to_message.message_id, + ) + assert message.reply_to_message is None + else: + with pytest.raises(BadRequest, match="Message to be replied not found"): + await default_bot.send_game( + chat_id, game_short_name, reply_to_message_id=reply_to_message.message_id + ) + + @pytest.mark.parametrize( + ("default_bot", "val"), + [({"protect_content": True}, True), ({"protect_content": False}, None)], + indirect=["default_bot"], + ) + async def test_send_game_default_protect_content(self, default_bot, chat_id, val): + protected = await default_bot.send_game(chat_id, "test_game", protect_content=val) + assert protected.has_protected_content is val + + @pytest.mark.xdist_group("game") + @xfail + async def test_set_game_score_1(self, bot, chat_id): + # NOTE: numbering of methods assures proper order between test_set_game_scoreX methods + # First, test setting a score. + game_short_name = "test_game" + game = await bot.send_game(chat_id, game_short_name) + + message = await bot.set_game_score( + user_id=chat_id, + score=BASE_GAME_SCORE, # Score value is relevant for other set_game_score_* tests! + chat_id=game.chat_id, + message_id=game.message_id, + ) + + assert message.game.description == game.game.description + assert message.game.photo[0].file_size == game.game.photo[0].file_size + assert message.game.animation.file_unique_id == game.game.animation.file_unique_id + assert message.game.text != game.game.text + + @pytest.mark.xdist_group("game") + @xfail + async def test_set_game_score_2(self, bot, chat_id): + # NOTE: numbering of methods assures proper order between test_set_game_scoreX methods + # Test setting a score higher than previous + game_short_name = "test_game" + game = await bot.send_game(chat_id, game_short_name) + + score = BASE_GAME_SCORE + 1 + + message = await bot.set_game_score( + user_id=chat_id, + score=score, + chat_id=game.chat_id, + message_id=game.message_id, + disable_edit_message=True, + ) + + assert message.game.description == game.game.description + assert message.game.photo[0].file_size == game.game.photo[0].file_size + assert message.game.animation.file_unique_id == game.game.animation.file_unique_id + assert message.game.text == game.game.text + + @pytest.mark.xdist_group("game") + @xfail + async def test_set_game_score_3(self, bot, chat_id): + # NOTE: numbering of methods assures proper order between test_set_game_scoreX methods + # Test setting a score lower than previous (should raise error) + game_short_name = "test_game" + game = await bot.send_game(chat_id, game_short_name) + + score = BASE_GAME_SCORE # Even a score equal to previous raises an error. + + with pytest.raises(BadRequest, match="Bot_score_not_modified"): + await bot.set_game_score( + user_id=chat_id, score=score, chat_id=game.chat_id, message_id=game.message_id + ) + + @pytest.mark.xdist_group("game") + @xfail + async def test_set_game_score_4(self, bot, chat_id): + # NOTE: numbering of methods assures proper order between test_set_game_scoreX methods + # Test force setting a lower score + game_short_name = "test_game" + game = await bot.send_game(chat_id, game_short_name) + await asyncio.sleep(1.5) + + score = BASE_GAME_SCORE - 10 + + message = await bot.set_game_score( + user_id=chat_id, + score=score, + chat_id=game.chat_id, + message_id=game.message_id, + force=True, + ) + + assert message.game.description == game.game.description + assert message.game.photo[0].file_size == game.game.photo[0].file_size + assert message.game.animation.file_unique_id == game.game.animation.file_unique_id + + # For some reason the returned message doesn't contain the updated score. need to fetch + # the game again... (the service message is also absent when running the test suite) + game2 = await bot.send_game(chat_id, game_short_name) + assert str(score) in game2.game.text + + @pytest.mark.xdist_group("game") + @xfail + async def test_get_game_high_scores(self, bot, chat_id): + # We need a game to get the scores for + game_short_name = "test_game" + game = await bot.send_game(chat_id, game_short_name) + high_scores = await bot.get_game_high_scores(chat_id, game.chat_id, game.message_id) + # We assume that the other game score tests ran within 20 sec + assert high_scores[0].score == BASE_GAME_SCORE - 10 + + # send_invoice and create_invoice_link is tested in test_invoice + async def test_promote_chat_member(self, bot, channel_id, monkeypatch): + # TODO: Add bot to supergroup so this can be tested properly / give bot perms + with pytest.raises(BadRequest, match="Not enough rights"): + assert await bot.promote_chat_member( + channel_id, + 1325859552, + is_anonymous=True, + can_change_info=True, + can_post_messages=True, + can_edit_messages=True, + can_delete_messages=True, + can_invite_users=True, + can_restrict_members=True, + can_pin_messages=True, + can_promote_members=True, + can_manage_chat=True, + can_manage_video_chats=True, + can_manage_topics=True, + can_post_stories=True, + can_edit_stories=True, + can_delete_stories=True, + ) + + # Test that we pass the correct params to TG + async def make_assertion(*args, **_): + data = args[1] + return ( + data.get("chat_id") == channel_id + and data.get("user_id") == 1325859552 + and data.get("is_anonymous") == 1 + and data.get("can_change_info") == 2 + and data.get("can_post_messages") == 3 + and data.get("can_edit_messages") == 4 + and data.get("can_delete_messages") == 5 + and data.get("can_invite_users") == 6 + and data.get("can_restrict_members") == 7 + and data.get("can_pin_messages") == 8 + and data.get("can_promote_members") == 9 + and data.get("can_manage_chat") == 10 + and data.get("can_manage_video_chats") == 11 + and data.get("can_manage_topics") == 12 + and data.get("can_post_stories") == 13 + and data.get("can_edit_stories") == 14 + and data.get("can_delete_stories") == 15 + ) + + monkeypatch.setattr(bot, "_post", make_assertion) + assert await bot.promote_chat_member( + channel_id, + 1325859552, + is_anonymous=1, + can_change_info=2, + can_post_messages=3, + can_edit_messages=4, + can_delete_messages=5, + can_invite_users=6, + can_restrict_members=7, + can_pin_messages=8, + can_promote_members=9, + can_manage_chat=10, + can_manage_video_chats=11, + can_manage_topics=12, + can_post_stories=13, + can_edit_stories=14, + can_delete_stories=15, + ) + + async def test_export_chat_invite_link(self, bot, channel_id): + # Each link is unique apparently + invite_link = await bot.export_chat_invite_link(channel_id) + assert isinstance(invite_link, str) + assert invite_link + + async def test_edit_revoke_chat_invite_link_passing_link_objects(self, bot, channel_id): + invite_link = await bot.create_chat_invite_link(chat_id=channel_id) + assert invite_link.name is None + + edited_link = await bot.edit_chat_invite_link( + chat_id=channel_id, invite_link=invite_link, name="some_name" + ) + assert edited_link == invite_link + assert edited_link.name == "some_name" + + revoked_link = await bot.revoke_chat_invite_link( + chat_id=channel_id, invite_link=edited_link + ) + assert revoked_link.invite_link == edited_link.invite_link + assert revoked_link.is_revoked is True + assert revoked_link.name == "some_name" + + @pytest.mark.parametrize("creates_join_request", [True, False]) + @pytest.mark.parametrize("name", [None, "name"]) + async def test_create_chat_invite_link_basics( + self, bot, creates_join_request, name, channel_id + ): + data = {} + if creates_join_request: + data["creates_join_request"] = True + if name: + data["name"] = name + invite_link = await bot.create_chat_invite_link(chat_id=channel_id, **data) + + assert invite_link.member_limit is None + assert invite_link.expire_date is None + assert invite_link.creates_join_request == creates_join_request + assert invite_link.name == name + + revoked_link = await bot.revoke_chat_invite_link( + chat_id=channel_id, invite_link=invite_link.invite_link + ) + assert revoked_link.is_revoked + + @pytest.mark.skipif(not TEST_WITH_OPT_DEPS, reason="This test's implementation requires pytz") + @pytest.mark.parametrize("datetime", argvalues=[True, False], ids=["datetime", "integer"]) + async def test_advanced_chat_invite_links(self, bot, channel_id, datetime): + # we are testing this all in one function in order to save api calls + timestamp = dtm.datetime.utcnow() + add_seconds = dtm.timedelta(0, 70) + time_in_future = timestamp + add_seconds + expire_time = time_in_future if datetime else to_timestamp(time_in_future) + aware_time_in_future = UTC.localize(time_in_future) + + invite_link = await bot.create_chat_invite_link( + channel_id, expire_date=expire_time, member_limit=10 + ) + assert invite_link.invite_link + assert not invite_link.invite_link.endswith("...") + assert abs(invite_link.expire_date - aware_time_in_future) < dtm.timedelta(seconds=1) + assert invite_link.member_limit == 10 + + add_seconds = dtm.timedelta(0, 80) + time_in_future = timestamp + add_seconds + expire_time = time_in_future if datetime else to_timestamp(time_in_future) + aware_time_in_future = UTC.localize(time_in_future) + + edited_invite_link = await bot.edit_chat_invite_link( + channel_id, + invite_link.invite_link, + expire_date=expire_time, + member_limit=20, + name="NewName", + ) + assert edited_invite_link.invite_link == invite_link.invite_link + assert abs(edited_invite_link.expire_date - aware_time_in_future) < dtm.timedelta( + seconds=1 + ) + assert edited_invite_link.name == "NewName" + assert edited_invite_link.member_limit == 20 + + edited_invite_link = await bot.edit_chat_invite_link( + channel_id, + invite_link.invite_link, + name="EvenNewerName", + creates_join_request=True, + ) + assert edited_invite_link.invite_link == invite_link.invite_link + assert not edited_invite_link.expire_date + assert edited_invite_link.name == "EvenNewerName" + assert edited_invite_link.creates_join_request + assert edited_invite_link.member_limit is None + + revoked_invite_link = await bot.revoke_chat_invite_link( + channel_id, invite_link.invite_link + ) + assert revoked_invite_link.invite_link == invite_link.invite_link + assert revoked_invite_link.is_revoked + + async def test_advanced_chat_invite_links_default_tzinfo(self, tz_bot, channel_id): + # we are testing this all in one function in order to save api calls + add_seconds = dtm.timedelta(0, 70) + aware_expire_date = dtm.datetime.now(tz=tz_bot.defaults.tzinfo) + add_seconds + time_in_future = aware_expire_date.replace(tzinfo=None) + + invite_link = await tz_bot.create_chat_invite_link( + channel_id, expire_date=time_in_future, member_limit=10 + ) + assert invite_link.invite_link + assert not invite_link.invite_link.endswith("...") + assert abs(invite_link.expire_date - aware_expire_date) < dtm.timedelta(seconds=1) + assert invite_link.member_limit == 10 + + add_seconds = dtm.timedelta(0, 80) + aware_expire_date += add_seconds + time_in_future = aware_expire_date.replace(tzinfo=None) + + edited_invite_link = await tz_bot.edit_chat_invite_link( + channel_id, + invite_link.invite_link, + expire_date=time_in_future, + member_limit=20, + name="NewName", + ) + assert edited_invite_link.invite_link == invite_link.invite_link + assert abs(edited_invite_link.expire_date - aware_expire_date) < dtm.timedelta(seconds=1) + assert edited_invite_link.name == "NewName" + assert edited_invite_link.member_limit == 20 + + edited_invite_link = await tz_bot.edit_chat_invite_link( + channel_id, + invite_link.invite_link, + name="EvenNewerName", + creates_join_request=True, + ) + assert edited_invite_link.invite_link == invite_link.invite_link + assert not edited_invite_link.expire_date + assert edited_invite_link.name == "EvenNewerName" + assert edited_invite_link.creates_join_request + assert edited_invite_link.member_limit is None + + revoked_invite_link = await tz_bot.revoke_chat_invite_link( + channel_id, invite_link.invite_link + ) + assert revoked_invite_link.invite_link == invite_link.invite_link + assert revoked_invite_link.is_revoked + + async def test_approve_chat_join_request(self, bot, chat_id, channel_id): + # TODO: Need incoming join request to properly test + # Since we can't create join requests on the fly, we just tests the call to TG + # by checking that it complains about approving a user who is already in the chat + with pytest.raises(BadRequest, match="User_already_participant"): + await bot.approve_chat_join_request(chat_id=channel_id, user_id=chat_id) + + async def test_decline_chat_join_request(self, bot, chat_id, channel_id): + # TODO: Need incoming join request to properly test + # Since we can't create join requests on the fly, we just tests the call to TG + # by checking that it complains about declining a user who is already in the chat + # + # The error message Hide_requester_missing started showing up instead of + # User_already_participant. Don't know why … + with pytest.raises(BadRequest, match="User_already_participant|Hide_requester_missing"): + await bot.decline_chat_join_request(chat_id=channel_id, user_id=chat_id) + + async def test_set_chat_photo(self, bot, channel_id): + async def func(): + assert await bot.set_chat_photo(channel_id, f) + + with data_file("telegram_test_channel.jpg").open("rb") as f: + await expect_bad_request( + func, "Type of file mismatch", "Telegram did not accept the file." + ) + + async def test_delete_chat_photo(self, bot, channel_id): + async def func(): + assert await bot.delete_chat_photo(channel_id) + + await expect_bad_request(func, "Chat_not_modified", "Chat photo was not set.") + + async def test_set_chat_title(self, bot, channel_id): + assert await bot.set_chat_title(channel_id, ">>> telegram.Bot() - Tests") + + async def test_set_chat_description(self, bot, channel_id): + assert await bot.set_chat_description(channel_id, "Time: " + str(time.time())) + + async def test_pin_and_unpin_message(self, bot, super_group_id): + messages = [] # contains the Messages we sent + pinned_messages_tasks = set() # contains the asyncio.Tasks that pin the messages + + # Let's send 3 messages so we can pin them + awaitables = {bot.send_message(super_group_id, f"test_pin_message_{i}") for i in range(3)} + + # We will pin the messages immediately after sending them + for sending_msg in asyncio.as_completed(awaitables): # as_completed sends the messages + msg = await sending_msg + coro = bot.pin_chat_message(super_group_id, msg.message_id, True, read_timeout=10) + pinned_messages_tasks.add(asyncio.create_task(coro)) # start pinning the message + messages.append(msg) + + assert len(messages) == 3 # Check if we sent 3 messages + + # Check if we pinned 3 messages + assert all([await i for i in pinned_messages_tasks]) + assert all(i.done() for i in pinned_messages_tasks) # Check if all tasks are done + + chat = await bot.get_chat(super_group_id) # get the chat to check the pinned message + assert chat.pinned_message in messages + + # Determine which message is not the most recently pinned + for old_pin_msg in messages: + if chat.pinned_message != old_pin_msg: + break + + # Test unpinning our messages + tasks = asyncio.gather( + bot.unpin_chat_message( # unpins any message except the most recent + chat_id=super_group_id, # because we don't want to accidentally unpin the same msg + message_id=old_pin_msg.message_id, # twice + read_timeout=10, + ), + bot.unpin_chat_message(chat_id=super_group_id, read_timeout=10), # unpins most recent + ) + assert all(await tasks) + assert all(i.done() for i in tasks) + assert await bot.unpin_all_chat_messages(super_group_id, read_timeout=10) + + # get_sticker_set, upload_sticker_file, create_new_sticker_set, add_sticker_to_set, + # set_sticker_position_in_set, delete_sticker_from_set and get_custom_emoji_stickers, + # replace_sticker_in_set are tested in the test_sticker module. + + # get_forum_topic_icon_stickers, edit_forum_topic, general_forum etc... + # are tested in the test_forum module. + async def test_send_message_disable_web_page_preview(self, bot, chat_id): + """Test that disable_web_page_preview is substituted for link_preview_options and that + it still works as expected for backward compatability.""" + msg = await bot.send_message( + chat_id, + "https://github.com/python-telegram-bot/python-telegram-bot", + disable_web_page_preview=True, + ) + assert msg.link_preview_options + assert msg.link_preview_options.is_disabled + + async def test_send_message_link_preview_options(self, bot, chat_id): + """Test whether link_preview_options is correctly passed to the API.""" + # btw it is possible to have no url in the text, but set a url for the preview. + msg = await bot.send_message( + chat_id, + "https://github.com/python-telegram-bot/python-telegram-bot", + link_preview_options=LinkPreviewOptions(prefer_small_media=True, show_above_text=True), + ) + assert msg.link_preview_options + assert not msg.link_preview_options.is_disabled + # The prefer_* options aren't very consistent on the client side (big pic shown) + + # they are not returned by the API. + # assert msg.link_preview_options.prefer_small_media + assert msg.link_preview_options.show_above_text + + @pytest.mark.parametrize( + "default_bot", + [{"link_preview_options": LinkPreviewOptions(show_above_text=True)}], + indirect=True, + ) + async def test_send_message_default_link_preview_options(self, default_bot, chat_id): + """Test whether Defaults.link_preview_options is correctly fused with the passed LPO.""" + github_url = "https://github.com/python-telegram-bot/python-telegram-bot" + website = "https://python-telegram-bot.org/" + + # First test just the default passing: + coro1 = default_bot.send_message(chat_id, github_url) + # Next test fusion of both LPOs: + coro2 = default_bot.send_message( + chat_id, + github_url, + link_preview_options=LinkPreviewOptions(url=website, prefer_large_media=True), + ) + # Now test fusion + overriding of passed LPO: + coro3 = default_bot.send_message( + chat_id, + github_url, + link_preview_options=LinkPreviewOptions(show_above_text=False, url=website), + ) + # finally test explicitly setting to None + coro4 = default_bot.send_message(chat_id, github_url, link_preview_options=None) + + msgs = asyncio.gather(coro1, coro2, coro3, coro4) + msg1, msg2, msg3, msg4 = await msgs + assert msg1.link_preview_options + assert msg1.link_preview_options.show_above_text + + assert msg2.link_preview_options + assert msg2.link_preview_options.show_above_text + assert msg2.link_preview_options.url == website + assert msg2.link_preview_options.prefer_large_media # Now works correctly using new url.. + + assert msg3.link_preview_options + assert not msg3.link_preview_options.show_above_text + assert msg3.link_preview_options.url == website + + assert msg4.link_preview_options == LinkPreviewOptions(url=github_url) + + @pytest.mark.parametrize( + "default_bot", + [{"link_preview_options": LinkPreviewOptions(show_above_text=True)}], + indirect=True, + ) + async def test_edit_message_text_default_link_preview_options(self, default_bot, chat_id): + """Test whether Defaults.link_preview_options is correctly fused with the passed LPO.""" + github_url = "https://github.com/python-telegram-bot/python-telegram-bot" + website = "https://python-telegram-bot.org/" + telegram_url = "https://telegram.org" + base_1, base_2, base_3, base_4 = await asyncio.gather( + *(default_bot.send_message(chat_id, telegram_url) for _ in range(4)) + ) + + # First test just the default passing: + coro1 = base_1.edit_text(github_url) + # Next test fusion of both LPOs: + coro2 = base_2.edit_text( + github_url, + link_preview_options=LinkPreviewOptions(url=website, prefer_large_media=True), + ) + # Now test fusion + overriding of passed LPO: + coro3 = base_3.edit_text( + github_url, + link_preview_options=LinkPreviewOptions(show_above_text=False, url=website), + ) + # finally test explicitly setting to None + coro4 = base_4.edit_text(github_url, link_preview_options=None) + + msgs = asyncio.gather(coro1, coro2, coro3, coro4) + msg1, msg2, msg3, msg4 = await msgs + assert msg1.link_preview_options + assert msg1.link_preview_options.show_above_text + + assert msg2.link_preview_options + assert msg2.link_preview_options.show_above_text + assert msg2.link_preview_options.url == website + assert msg2.link_preview_options.prefer_large_media # Now works correctly using new url.. + + assert msg3.link_preview_options + assert not msg3.link_preview_options.show_above_text + assert msg3.link_preview_options.url == website + + assert msg4.link_preview_options == LinkPreviewOptions(url=github_url) + + async def test_send_message_entities(self, bot, chat_id): + test_string = "Italic Bold Code Spoiler" + entities = [ + MessageEntity(MessageEntity.ITALIC, 0, 6), + MessageEntity(MessageEntity.ITALIC, 7, 4), + MessageEntity(MessageEntity.ITALIC, 12, 4), + MessageEntity(MessageEntity.SPOILER, 17, 7), + ] + message = await bot.send_message(chat_id=chat_id, text=test_string, entities=entities) + assert message.text == test_string + assert message.entities == tuple(entities) + + @pytest.mark.parametrize("default_bot", [{"parse_mode": "Markdown"}], indirect=True) + async def test_send_message_default_parse_mode(self, default_bot, chat_id): + test_string = "Italic Bold Code" + test_markdown_string = "_Italic_ *Bold* `Code`" + + tasks = asyncio.gather( + *( + default_bot.send_message(chat_id, test_markdown_string, **i) + for i in ({}, {"parse_mode": None}, {"parse_mode": "HTML"}) + ) + ) + msg1, msg2, msg3 = await tasks + assert msg1.text_markdown == test_markdown_string + assert msg1.text == test_string + + assert msg2.text == test_markdown_string + assert msg2.text_markdown == escape_markdown(test_markdown_string) + + assert msg3.text == test_markdown_string + assert msg3.text_markdown == escape_markdown(test_markdown_string) + + @pytest.mark.parametrize("default_bot", [{"protect_content": True}], indirect=True) + async def test_send_message_default_protect_content(self, default_bot, chat_id): + tasks = asyncio.gather( + default_bot.send_message(chat_id, "test"), + default_bot.send_message(chat_id, "test", protect_content=False), + ) + to_check, no_protect = await tasks + assert to_check.has_protected_content + assert not no_protect.has_protected_content + + @pytest.mark.parametrize( + ("default_bot", "custom"), + [ + ({"allow_sending_without_reply": True}, None), + ({"allow_sending_without_reply": False}, None), + ({"allow_sending_without_reply": False}, True), + ], + indirect=["default_bot"], + ) + async def test_send_message_default_allow_sending_without_reply( + self, default_bot, chat_id, custom + ): + reply_to_message = await default_bot.send_message(chat_id, "test") + await reply_to_message.delete() + if custom is not None: + message = await default_bot.send_message( + chat_id, + "test", + allow_sending_without_reply=custom, + reply_to_message_id=reply_to_message.message_id, + ) + assert message.reply_to_message is None + elif default_bot.defaults.allow_sending_without_reply: + message = await default_bot.send_message( + chat_id, "test", reply_to_message_id=reply_to_message.message_id + ) + assert message.reply_to_message is None + else: + with pytest.raises(BadRequest, match="Message to be replied not found"): + await default_bot.send_message( + chat_id, "test", reply_to_message_id=reply_to_message.message_id + ) + + async def test_get_set_my_default_administrator_rights(self, bot): + # Test that my default administrator rights for group are as all False + assert await bot.set_my_default_administrator_rights() # clear any set rights + my_admin_rights_grp = await bot.get_my_default_administrator_rights() + assert isinstance(my_admin_rights_grp, ChatAdministratorRights) + assert all(not getattr(my_admin_rights_grp, at) for at in my_admin_rights_grp.__slots__) + + # Test setting my default admin rights for channel + my_rights = ChatAdministratorRights.all_rights() + assert await bot.set_my_default_administrator_rights(my_rights, for_channels=True) + my_admin_rights_ch = await bot.get_my_default_administrator_rights(for_channels=True) + assert my_admin_rights_ch.can_invite_users is my_rights.can_invite_users + # tg bug? is_anonymous is False despite setting it True for channels: + assert my_admin_rights_ch.is_anonymous is not my_rights.is_anonymous + + assert my_admin_rights_ch.can_manage_chat is my_rights.can_manage_chat + assert my_admin_rights_ch.can_delete_messages is my_rights.can_delete_messages + assert my_admin_rights_ch.can_edit_messages is my_rights.can_edit_messages + assert my_admin_rights_ch.can_post_messages is my_rights.can_post_messages + assert my_admin_rights_ch.can_change_info is my_rights.can_change_info + assert my_admin_rights_ch.can_promote_members is my_rights.can_promote_members + assert my_admin_rights_ch.can_restrict_members is my_rights.can_restrict_members + assert my_admin_rights_ch.can_pin_messages is None # Not returned for channels + assert my_admin_rights_ch.can_manage_topics is None # Not returned for channels + + async def test_get_set_chat_menu_button(self, bot, chat_id): + # Test our chat menu button is commands- + menu_button = await bot.get_chat_menu_button() + assert isinstance(menu_button, MenuButton) + assert isinstance(menu_button, MenuButtonCommands) + assert menu_button.type == MenuButtonType.COMMANDS + + # Test setting our chat menu button to Webapp. + my_menu = MenuButtonWebApp("click me!", WebAppInfo("https://telegram.org/")) + assert await bot.set_chat_menu_button(chat_id=chat_id, menu_button=my_menu) + menu_button = await bot.get_chat_menu_button(chat_id) + assert isinstance(menu_button, MenuButtonWebApp) + assert menu_button.type == MenuButtonType.WEB_APP + assert menu_button.text == my_menu.text + assert menu_button.web_app.url == my_menu.web_app.url + + assert await bot.set_chat_menu_button(chat_id=chat_id, menu_button=MenuButtonDefault()) + menu_button = await bot.get_chat_menu_button(chat_id=chat_id) + assert isinstance(menu_button, MenuButtonDefault) + + async def test_set_and_get_my_commands(self, bot): + commands = [BotCommand("cmd1", "descr1"), ["cmd2", "descr2"]] + assert await bot.set_my_commands([]) + assert await bot.get_my_commands() == () + assert await bot.set_my_commands(commands) + + for i, bc in enumerate(await bot.get_my_commands()): + assert bc.command == f"cmd{i + 1}" + assert bc.description == f"descr{i + 1}" + + async def test_get_set_delete_my_commands_with_scope(self, bot, super_group_id, chat_id): + group_cmds = [BotCommand("group_cmd", "visible to this supergroup only")] + private_cmds = [BotCommand("private_cmd", "visible to this private chat only")] + group_scope = BotCommandScopeChat(super_group_id) + private_scope = BotCommandScopeChat(chat_id) + + # Set supergroup command list with lang code and check if the same can be returned from api + assert await bot.set_my_commands(group_cmds, scope=group_scope, language_code="en") + gotten_group_cmds = await bot.get_my_commands(scope=group_scope, language_code="en") + + assert len(gotten_group_cmds) == len(group_cmds) + assert gotten_group_cmds[0].command == group_cmds[0].command + + # Set private command list and check if same can be returned from the api + assert await bot.set_my_commands(private_cmds, scope=private_scope) + gotten_private_cmd = await bot.get_my_commands(scope=private_scope) + + assert len(gotten_private_cmd) == len(private_cmds) + assert gotten_private_cmd[0].command == private_cmds[0].command + + # Delete command list from that supergroup and private chat- + tasks = asyncio.gather( + bot.delete_my_commands(private_scope), + bot.delete_my_commands(group_scope, "en"), + ) + assert all(await tasks) + + # Check if its been deleted- + tasks = asyncio.gather( + bot.get_my_commands(private_scope), + bot.get_my_commands(group_scope, "en"), + ) + deleted_priv_cmds, deleted_grp_cmds = await tasks + + assert len(deleted_grp_cmds) == 0 == len(group_cmds) - 1 + assert len(deleted_priv_cmds) == 0 == len(private_cmds) - 1 + + await bot.delete_my_commands() # Delete commands from default scope + assert len(await bot.get_my_commands()) == 0 + + async def test_copy_message_without_reply(self, bot, chat_id, media_message): + keyboard = InlineKeyboardMarkup( + [[InlineKeyboardButton(text="test", callback_data="test2")]] + ) + + returned = await bot.copy_message( + chat_id, + from_chat_id=chat_id, + message_id=media_message.message_id, + caption="Test", + parse_mode=ParseMode.HTML, + reply_to_message_id=media_message.message_id, + reply_markup=keyboard, + show_caption_above_media=False, + ) + # we send a temp message which replies to the returned message id in order to get a + # message object + temp_message = await bot.send_message( + chat_id, "test", reply_to_message_id=returned.message_id + ) + message = temp_message.reply_to_message + assert message.chat_id == int(chat_id) + assert message.caption == "Test" + assert len(message.caption_entities) == 1 + assert message.reply_markup == keyboard + + @pytest.mark.parametrize( + "default_bot", + [ + ({"parse_mode": ParseMode.HTML, "allow_sending_without_reply": True}), + ({"parse_mode": None, "allow_sending_without_reply": True}), + ({"parse_mode": None, "allow_sending_without_reply": False}), + ], + indirect=["default_bot"], + ) + async def test_copy_message_with_default(self, default_bot, chat_id, media_message): + reply_to_message = await default_bot.send_message(chat_id, "test") + await reply_to_message.delete() + if not default_bot.defaults.allow_sending_without_reply: + with pytest.raises(BadRequest, match="not found"): + await default_bot.copy_message( + chat_id, + from_chat_id=chat_id, + message_id=media_message.message_id, + caption="Test", + reply_to_message_id=reply_to_message.message_id, + ) + return + returned = await default_bot.copy_message( + chat_id, + from_chat_id=chat_id, + message_id=media_message.message_id, + caption="Test", + reply_to_message_id=reply_to_message.message_id, + ) + # we send a temp message which replies to the returned message id in order to get a + # message object + temp_message = await default_bot.send_message( + chat_id, "test", reply_to_message_id=returned.message_id + ) + message = temp_message.reply_to_message + if default_bot.defaults.parse_mode: + assert len(message.caption_entities) == 1 + else: + assert len(message.caption_entities) == 0 + + async def test_copy_messages(self, bot, chat_id): + tasks = asyncio.gather( + bot.send_message(chat_id, text="will be copied 1"), + bot.send_message(chat_id, text="will be copied 2"), + ) + msg1, msg2 = await tasks + + copy_messages = await bot.copy_messages( + chat_id, from_chat_id=chat_id, message_ids=sorted((msg1.message_id, msg2.message_id)) + ) + assert isinstance(copy_messages, tuple) + + tasks = asyncio.gather( + bot.send_message(chat_id, "temp 1", reply_to_message_id=copy_messages[0].message_id), + bot.send_message(chat_id, "temp 2", reply_to_message_id=copy_messages[1].message_id), + ) + temp_msg1, temp_msg2 = await tasks + + forward_msg1 = temp_msg1.reply_to_message + forward_msg2 = temp_msg2.reply_to_message + + assert forward_msg1.text == msg1.text + assert forward_msg2.text == msg2.text + + # Continue testing arbitrary callback data here with actual requests: + async def test_replace_callback_data_send_message(self, cdc_bot, chat_id): + bot = cdc_bot + + try: + replace_button = InlineKeyboardButton(text="replace", callback_data="replace_test") + no_replace_button = InlineKeyboardButton( + text="no_replace", url="http://python-telegram-bot.org/" + ) + reply_markup = InlineKeyboardMarkup.from_row( + [ + replace_button, + no_replace_button, + ] + ) + message = await bot.send_message( + chat_id=chat_id, text="test", reply_markup=reply_markup + ) + inline_keyboard = message.reply_markup.inline_keyboard + + assert inline_keyboard[0][1] == no_replace_button + assert inline_keyboard[0][0] == replace_button + keyboard = next(iter(bot.callback_data_cache._keyboard_data)) + data = next( + iter(bot.callback_data_cache._keyboard_data[keyboard].button_data.values()) + ) + assert data == "replace_test" + finally: + bot.callback_data_cache.clear_callback_data() + bot.callback_data_cache.clear_callback_queries() + + async def test_replace_callback_data_stop_poll_and_repl_to_message(self, cdc_bot, chat_id): + bot = cdc_bot + + poll_message = await bot.send_poll(chat_id=chat_id, question="test", options=["1", "2"]) + try: + replace_button = InlineKeyboardButton(text="replace", callback_data="replace_test") + no_replace_button = InlineKeyboardButton( + text="no_replace", url="http://python-telegram-bot.org/" + ) + reply_markup = InlineKeyboardMarkup.from_row( + [ + replace_button, + no_replace_button, + ] + ) + await poll_message.stop_poll(reply_markup=reply_markup) + helper_message = await poll_message.reply_text("temp", quote=True) + message = helper_message.reply_to_message + inline_keyboard = message.reply_markup.inline_keyboard + + assert inline_keyboard[0][1] == no_replace_button + assert inline_keyboard[0][0] == replace_button + keyboard = next(iter(bot.callback_data_cache._keyboard_data)) + data = next( + iter(bot.callback_data_cache._keyboard_data[keyboard].button_data.values()) + ) + assert data == "replace_test" + finally: + bot.callback_data_cache.clear_callback_data() + bot.callback_data_cache.clear_callback_queries() + + async def test_replace_callback_data_copy_message(self, cdc_bot, chat_id): + """This also tests that data is inserted into the buttons of message.reply_to_message + where message is the return value of a bot method""" + bot = cdc_bot + + original_message = await bot.send_message(chat_id=chat_id, text="original") + try: + replace_button = InlineKeyboardButton(text="replace", callback_data="replace_test") + no_replace_button = InlineKeyboardButton( + text="no_replace", url="http://python-telegram-bot.org/" + ) + reply_markup = InlineKeyboardMarkup.from_row( + [ + replace_button, + no_replace_button, + ] + ) + message_id = await original_message.copy(chat_id=chat_id, reply_markup=reply_markup) + helper_message = await bot.send_message( + chat_id=chat_id, reply_to_message_id=message_id.message_id, text="temp" + ) + message = helper_message.reply_to_message + inline_keyboard = message.reply_markup.inline_keyboard + + assert inline_keyboard[0][1] == no_replace_button + assert inline_keyboard[0][0] == replace_button + keyboard = next(iter(bot.callback_data_cache._keyboard_data)) + data = next( + iter(bot.callback_data_cache._keyboard_data[keyboard].button_data.values()) + ) + assert data == "replace_test" + finally: + bot.callback_data_cache.clear_callback_data() + bot.callback_data_cache.clear_callback_queries() + + async def test_get_chat_arbitrary_callback_data(self, channel_id, cdc_bot): + bot = cdc_bot + + try: + reply_markup = InlineKeyboardMarkup.from_button( + InlineKeyboardButton(text="text", callback_data="callback_data") + ) + + message = await bot.send_message( + channel_id, text="get_chat_arbitrary_callback_data", reply_markup=reply_markup + ) + await message.pin() + + keyboard = next(iter(bot.callback_data_cache._keyboard_data)) + data = next( + iter(bot.callback_data_cache._keyboard_data[keyboard].button_data.values()) + ) + assert data == "callback_data" + + cfi = await bot.get_chat(channel_id) + assert cfi.pinned_message == message + assert cfi.pinned_message.reply_markup == reply_markup + assert await message.unpin() # (not placed in finally block since msg can be unbound) + finally: + bot.callback_data_cache.clear_callback_data() + bot.callback_data_cache.clear_callback_queries() + + async def test_arbitrary_callback_data_get_chat_no_pinned_message( + self, super_group_id, cdc_bot + ): + bot = cdc_bot + await bot.unpin_all_chat_messages(super_group_id) + + try: + cfi = await bot.get_chat(super_group_id) + + assert isinstance(cfi, ChatFullInfo) + assert int(cfi.id) == int(super_group_id) + assert cfi.pinned_message is None + finally: + bot.callback_data_cache.clear_callback_data() + bot.callback_data_cache.clear_callback_queries() + + async def test_set_get_my_description(self, bot): + default_description = f"{bot.username} - default - {dtm.datetime.utcnow().isoformat()}" + en_description = f"{bot.username} - en - {dtm.datetime.utcnow().isoformat()}" + de_description = f"{bot.username} - de - {dtm.datetime.utcnow().isoformat()}" + + # Set the descriptions + assert all( + await asyncio.gather( + bot.set_my_description(default_description), + bot.set_my_description(en_description, language_code="en"), + bot.set_my_description(de_description, language_code="de"), + ) + ) + + # Check that they were set correctly + assert await asyncio.gather( + bot.get_my_description(), bot.get_my_description("en"), bot.get_my_description("de") + ) == [ + BotDescription(default_description), + BotDescription(en_description), + BotDescription(de_description), + ] + + # Delete the descriptions + assert all( + await asyncio.gather( + bot.set_my_description(None), + bot.set_my_description(None, language_code="en"), + bot.set_my_description(None, language_code="de"), + ) + ) + + # Check that they were deleted correctly + assert await asyncio.gather( + bot.get_my_description(), bot.get_my_description("en"), bot.get_my_description("de") + ) == 3 * [BotDescription("")] + + async def test_set_get_my_short_description(self, bot): + default_short_description = ( + f"{bot.username} - default - {dtm.datetime.utcnow().isoformat()}" + ) + en_short_description = f"{bot.username} - en - {dtm.datetime.utcnow().isoformat()}" + de_short_description = f"{bot.username} - de - {dtm.datetime.utcnow().isoformat()}" + + # Set the short_descriptions + assert all( + await asyncio.gather( + bot.set_my_short_description(default_short_description), + bot.set_my_short_description(en_short_description, language_code="en"), + bot.set_my_short_description(de_short_description, language_code="de"), + ) + ) + + # Check that they were set correctly + assert await asyncio.gather( + bot.get_my_short_description(), + bot.get_my_short_description("en"), + bot.get_my_short_description("de"), + ) == [ + BotShortDescription(default_short_description), + BotShortDescription(en_short_description), + BotShortDescription(de_short_description), + ] + + # Delete the short_descriptions + assert all( + await asyncio.gather( + bot.set_my_short_description(None), + bot.set_my_short_description(None, language_code="en"), + bot.set_my_short_description(None, language_code="de"), + ) + ) + + # Check that they were deleted correctly + assert await asyncio.gather( + bot.get_my_short_description(), + bot.get_my_short_description("en"), + bot.get_my_short_description("de"), + ) == 3 * [BotShortDescription("")] + + async def test_set_message_reaction(self, bot, chat_id, message): + assert await bot.set_message_reaction( + chat_id, message.message_id, ReactionEmoji.THUMBS_DOWN, True + ) + + @pytest.mark.parametrize("bot_class", [Bot, ExtBot]) + async def test_do_api_request_warning_known_method(self, bot, bot_class): + with pytest.warns(PTBUserWarning, match="Please use 'Bot.get_me'") as record: + await bot_class(bot.token).do_api_request("get_me") + + assert record[0].filename == __file__, "Wrong stack level!" + + async def test_do_api_request_unknown_method(self, bot): + with pytest.raises(EndPointNotFound, match="'unknownEndpoint' not found"): + await bot.do_api_request("unknown_endpoint") + + @pytest.mark.filterwarnings("ignore::telegram.warnings.PTBUserWarning") + async def test_do_api_request_invalid_token(self, bot): + # we do not initialize the bot here on purpose b/c that's the case were we actually + # do not know for sure if the token is invalid or the method was not found + with pytest.raises( + InvalidToken, match="token was rejected by Telegram or the endpoint 'getMe'" + ): + await Bot("invalid_token").do_api_request("get_me") + + # same test, but with a valid token bot and unknown endpoint + with pytest.raises( + InvalidToken, match="token was rejected by Telegram or the endpoint 'unknownEndpoint'" + ): + await Bot(bot.token).do_api_request("unknown_endpoint") + + @pytest.mark.filterwarnings("ignore::telegram.warnings.PTBUserWarning") + @pytest.mark.parametrize("return_type", [Message, None]) + async def test_do_api_request_basic_and_files(self, bot, chat_id, return_type): + result = await bot.do_api_request( + "send_document", + api_kwargs={ + "chat_id": chat_id, + "caption": "test_caption", + "document": InputFile(data_file("telegram.png").open("rb")), + }, + return_type=return_type, + ) + if return_type is None: + assert isinstance(result, dict) + result = Message.de_json(result, bot) + + assert isinstance(result, Message) + assert result.chat_id == int(chat_id) + assert result.caption == "test_caption" + out = BytesIO() + await (await result.document.get_file()).download_to_memory(out) + out.seek(0) + assert out.read() == data_file("telegram.png").open("rb").read() + assert result.document.file_name == "telegram.png" + + @pytest.mark.filterwarnings("ignore::telegram.warnings.PTBUserWarning") + @pytest.mark.parametrize("return_type", [Message, None]) + async def test_do_api_request_list_return_type(self, bot, chat_id, return_type): + result = await bot.do_api_request( + "send_media_group", + api_kwargs={ + "chat_id": chat_id, + "media": [ + InputMediaDocument( + InputFile( + data_file("text_file.txt").open("rb"), + attach=True, + ) + ), + InputMediaDocument( + InputFile( + data_file("local_file.txt").open("rb"), + attach=True, + ) + ), + ], + }, + return_type=return_type, + ) + if return_type is None: + assert isinstance(result, list) + for entry in result: + assert isinstance(entry, dict) + result = Message.de_list(result, bot) + + for message, file_name in zip(result, ("text_file.txt", "local_file.txt")): + assert isinstance(message, Message) + assert message.chat_id == int(chat_id) + out = BytesIO() + await (await message.document.get_file()).download_to_memory(out) + out.seek(0) + assert out.read() == data_file(file_name).open("rb").read() + assert message.document.file_name == file_name + + @pytest.mark.filterwarnings("ignore::telegram.warnings.PTBUserWarning") + @pytest.mark.parametrize("return_type", [Message, None]) + async def test_do_api_request_bool_return_type(self, bot, chat_id, return_type): + assert await bot.do_api_request("delete_my_commands", return_type=return_type) is True + + async def test_get_star_transactions(self, bot): + transactions = await bot.get_star_transactions(limit=1) + assert isinstance(transactions, StarTransactions) + assert len(transactions.transactions) == 0 + + async def test_create_edit_chat_subscription_link( + self, bot, subscription_channel_id, channel_id + ): + sub_link = await bot.create_chat_subscription_invite_link( + subscription_channel_id, + name="sub_name", + subscription_period=2592000, + subscription_price=13, + ) + assert sub_link.name == "sub_name" + assert sub_link.subscription_period == 2592000 + assert sub_link.subscription_price == 13 + + edited_link = await bot.edit_chat_subscription_invite_link( + chat_id=subscription_channel_id, invite_link=sub_link, name="sub_name_2" + ) + assert edited_link.name == "sub_name_2" + assert sub_link.subscription_period == 2592000 + assert sub_link.subscription_price == 13 diff --git a/test_botcommand.py b/test_botcommand.py new file mode 100644 index 0000000000000000000000000000000000000000..1e4a360e0651f7f651f164551626d245448b522b --- /dev/null +++ b/test_botcommand.py @@ -0,0 +1,74 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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/]. + +import pytest + +from telegram import BotCommand, Dice +from tests.auxil.slots import mro_slots + + +@pytest.fixture(scope="module") +def bot_command(): + return BotCommand(command="start", description="A command") + + +class TestBotCommandWithoutRequest: + command = "start" + description = "A command" + + def test_slot_behaviour(self, bot_command): + for attr in bot_command.__slots__: + assert getattr(bot_command, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(bot_command)) == len(set(mro_slots(bot_command))), "duplicate slot" + + def test_de_json(self, bot): + json_dict = {"command": self.command, "description": self.description} + bot_command = BotCommand.de_json(json_dict, bot) + assert bot_command.api_kwargs == {} + + assert bot_command.command == self.command + assert bot_command.description == self.description + + assert BotCommand.de_json(None, bot) is None + + def test_to_dict(self, bot_command): + bot_command_dict = bot_command.to_dict() + + assert isinstance(bot_command_dict, dict) + assert bot_command_dict["command"] == bot_command.command + assert bot_command_dict["description"] == bot_command.description + + def test_equality(self): + a = BotCommand("start", "some description") + b = BotCommand("start", "some description") + c = BotCommand("start", "some other description") + d = BotCommand("hepl", "some description") + e = Dice(4, "emoji") + + assert a == b + assert hash(a) == hash(b) + + assert a != c + assert hash(a) != hash(c) + + assert a != d + assert hash(a) != hash(d) + + assert a != e + assert hash(a) != hash(e) diff --git a/test_botcommandscope.py b/test_botcommandscope.py new file mode 100644 index 0000000000000000000000000000000000000000..63766b95e170da2050f3dffc8058087a1c6c0984 --- /dev/null +++ b/test_botcommandscope.py @@ -0,0 +1,214 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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/]. +from copy import deepcopy + +import pytest + +from telegram import ( + BotCommandScope, + BotCommandScopeAllChatAdministrators, + BotCommandScopeAllGroupChats, + BotCommandScopeAllPrivateChats, + BotCommandScopeChat, + BotCommandScopeChatAdministrators, + BotCommandScopeChatMember, + BotCommandScopeDefault, + Dice, +) +from telegram.constants import BotCommandScopeType +from tests.auxil.slots import mro_slots + + +@pytest.fixture(scope="module", params=["str", "int"]) +def chat_id(request): + if request.param == "str": + return "@supergroupusername" + return 43 + + +@pytest.fixture( + scope="class", + params=[ + BotCommandScope.DEFAULT, + BotCommandScope.ALL_PRIVATE_CHATS, + BotCommandScope.ALL_GROUP_CHATS, + BotCommandScope.ALL_CHAT_ADMINISTRATORS, + BotCommandScope.CHAT, + BotCommandScope.CHAT_ADMINISTRATORS, + BotCommandScope.CHAT_MEMBER, + ], +) +def scope_type(request): + return request.param + + +@pytest.fixture( + scope="module", + params=[ + BotCommandScopeDefault, + BotCommandScopeAllPrivateChats, + BotCommandScopeAllGroupChats, + BotCommandScopeAllChatAdministrators, + BotCommandScopeChat, + BotCommandScopeChatAdministrators, + BotCommandScopeChatMember, + ], + ids=[ + BotCommandScope.DEFAULT, + BotCommandScope.ALL_PRIVATE_CHATS, + BotCommandScope.ALL_GROUP_CHATS, + BotCommandScope.ALL_CHAT_ADMINISTRATORS, + BotCommandScope.CHAT, + BotCommandScope.CHAT_ADMINISTRATORS, + BotCommandScope.CHAT_MEMBER, + ], +) +def scope_class(request): + return request.param + + +@pytest.fixture( + scope="module", + params=[ + (BotCommandScopeDefault, BotCommandScope.DEFAULT), + (BotCommandScopeAllPrivateChats, BotCommandScope.ALL_PRIVATE_CHATS), + (BotCommandScopeAllGroupChats, BotCommandScope.ALL_GROUP_CHATS), + (BotCommandScopeAllChatAdministrators, BotCommandScope.ALL_CHAT_ADMINISTRATORS), + (BotCommandScopeChat, BotCommandScope.CHAT), + (BotCommandScopeChatAdministrators, BotCommandScope.CHAT_ADMINISTRATORS), + (BotCommandScopeChatMember, BotCommandScope.CHAT_MEMBER), + ], + ids=[ + BotCommandScope.DEFAULT, + BotCommandScope.ALL_PRIVATE_CHATS, + BotCommandScope.ALL_GROUP_CHATS, + BotCommandScope.ALL_CHAT_ADMINISTRATORS, + BotCommandScope.CHAT, + BotCommandScope.CHAT_ADMINISTRATORS, + BotCommandScope.CHAT_MEMBER, + ], +) +def scope_class_and_type(request): + return request.param + + +@pytest.fixture(scope="module") +def bot_command_scope(scope_class_and_type, chat_id): + # we use de_json here so that we don't have to worry about which class needs which arguments + return scope_class_and_type[0].de_json( + {"type": scope_class_and_type[1], "chat_id": chat_id, "user_id": 42}, bot=None + ) + + +# All the scope types are very similar, so we test everything via parametrization +class TestBotCommandScopeWithoutRequest: + def test_slot_behaviour(self, bot_command_scope): + for attr in bot_command_scope.__slots__: + assert getattr(bot_command_scope, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(bot_command_scope)) == len( + set(mro_slots(bot_command_scope)) + ), "duplicate slot" + + def test_de_json(self, bot, scope_class_and_type, chat_id): + cls = scope_class_and_type[0] + type_ = scope_class_and_type[1] + + assert cls.de_json({}, bot) is None + + json_dict = {"type": type_, "chat_id": chat_id, "user_id": 42} + bot_command_scope = BotCommandScope.de_json(json_dict, bot) + assert set(bot_command_scope.api_kwargs.keys()) == {"chat_id", "user_id"} - set( + cls.__slots__ + ) + + assert isinstance(bot_command_scope, BotCommandScope) + assert isinstance(bot_command_scope, cls) + assert bot_command_scope.type == type_ + if "chat_id" in cls.__slots__: + assert bot_command_scope.chat_id == chat_id + if "user_id" in cls.__slots__: + assert bot_command_scope.user_id == 42 + + def test_de_json_invalid_type(self, bot): + json_dict = {"type": "invalid", "chat_id": chat_id, "user_id": 42} + bot_command_scope = BotCommandScope.de_json(json_dict, bot) + + assert type(bot_command_scope) is BotCommandScope + assert bot_command_scope.type == "invalid" + + def test_de_json_subclass(self, scope_class, bot, chat_id): + """This makes sure that e.g. BotCommandScopeDefault(data) never returns a + BotCommandScopeChat instance.""" + json_dict = {"type": "invalid", "chat_id": chat_id, "user_id": 42} + assert type(scope_class.de_json(json_dict, bot)) is scope_class + + def test_to_dict(self, bot_command_scope): + bot_command_scope_dict = bot_command_scope.to_dict() + + assert isinstance(bot_command_scope_dict, dict) + assert bot_command_scope["type"] == bot_command_scope.type + if hasattr(bot_command_scope, "chat_id"): + assert bot_command_scope["chat_id"] == bot_command_scope.chat_id + if hasattr(bot_command_scope, "user_id"): + assert bot_command_scope["user_id"] == bot_command_scope.user_id + + def test_type_enum_conversion(self): + assert type(BotCommandScope("default").type) is BotCommandScopeType + assert BotCommandScope("unknown").type == "unknown" + + def test_equality(self, bot_command_scope, bot): + a = BotCommandScope("base_type") + b = BotCommandScope("base_type") + c = bot_command_scope + d = deepcopy(bot_command_scope) + e = Dice(4, "emoji") + + assert a == b + assert hash(a) == hash(b) + + assert a != c + assert hash(a) != hash(c) + + assert a != d + assert hash(a) != hash(d) + + assert a != e + assert hash(a) != hash(e) + + assert c == d + assert hash(c) == hash(d) + + assert c != e + assert hash(c) != hash(e) + + if hasattr(c, "chat_id"): + json_dict = c.to_dict() + json_dict["chat_id"] = 0 + f = c.__class__.de_json(json_dict, bot) + + assert c != f + assert hash(c) != hash(f) + + if hasattr(c, "user_id"): + json_dict = c.to_dict() + json_dict["user_id"] = 0 + g = c.__class__.de_json(json_dict, bot) + + assert c != g + assert hash(c) != hash(g) diff --git a/test_botdescription.py b/test_botdescription.py new file mode 100644 index 0000000000000000000000000000000000000000..a75a88cb1a78dd3bad14a39cb915e58bdab6aeb2 --- /dev/null +++ b/test_botdescription.py @@ -0,0 +1,91 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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/]. +import pytest + +from telegram import BotDescription, BotShortDescription +from tests.auxil.slots import mro_slots + + +@pytest.fixture(scope="module") +def bot_description(bot): + return BotDescription(BotDescriptionTestBase.description) + + +@pytest.fixture(scope="module") +def bot_short_description(bot): + return BotShortDescription(BotDescriptionTestBase.short_description) + + +class BotDescriptionTestBase: + description = "This is a test description" + short_description = "This is a test short description" + + +class TestBotDescriptionWithoutRequest(BotDescriptionTestBase): + def test_slot_behaviour(self, bot_description): + for attr in bot_description.__slots__: + assert getattr(bot_description, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(bot_description)) == len( + set(mro_slots(bot_description)) + ), "duplicate slot" + + def test_to_dict(self, bot_description): + bot_description_dict = bot_description.to_dict() + + assert isinstance(bot_description_dict, dict) + assert bot_description_dict["description"] == self.description + + def test_equality(self): + a = BotDescription(self.description) + b = BotDescription(self.description) + c = BotDescription("text.com") + + assert a == b + assert hash(a) == hash(b) + assert a is not b + + assert a != c + assert hash(a) != hash(c) + + +class TestBotShortDescriptionWithoutRequest(BotDescriptionTestBase): + def test_slot_behaviour(self, bot_short_description): + for attr in bot_short_description.__slots__: + assert getattr(bot_short_description, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(bot_short_description)) == len( + set(mro_slots(bot_short_description)) + ), "duplicate slot" + + def test_to_dict(self, bot_short_description): + bot_short_description_dict = bot_short_description.to_dict() + + assert isinstance(bot_short_description_dict, dict) + assert bot_short_description_dict["short_description"] == self.short_description + + def test_equality(self): + a = BotShortDescription(self.short_description) + b = BotShortDescription(self.short_description) + c = BotShortDescription("text.com") + + assert a == b + assert hash(a) == hash(b) + assert a is not b + + assert a != c + assert hash(a) != hash(c) diff --git a/test_botname.py b/test_botname.py new file mode 100644 index 0000000000000000000000000000000000000000..4edefae394337a7497359533c376a971e3848506 --- /dev/null +++ b/test_botname.py @@ -0,0 +1,56 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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/]. +import pytest + +from telegram import BotName +from tests.auxil.slots import mro_slots + + +@pytest.fixture(scope="module") +def bot_name(bot): + return BotName(BotNameTestBase.name) + + +class BotNameTestBase: + name = "This is a test name" + + +class TestBotNameWithoutRequest(BotNameTestBase): + def test_slot_behaviour(self, bot_name): + for attr in bot_name.__slots__: + assert getattr(bot_name, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(bot_name)) == len(set(mro_slots(bot_name))), "duplicate slot" + + def test_to_dict(self, bot_name): + bot_name_dict = bot_name.to_dict() + + assert isinstance(bot_name_dict, dict) + assert bot_name_dict["name"] == self.name + + def test_equality(self): + a = BotName(self.name) + b = BotName(self.name) + c = BotName("text.com") + + assert a == b + assert hash(a) == hash(b) + assert a is not b + + assert a != c + assert hash(a) != hash(c) diff --git a/test_business.py b/test_business.py new file mode 100644 index 0000000000000000000000000000000000000000..735f2e7177aa636aa7c005d09c08ba10e78fb789 --- /dev/null +++ b/test_business.py @@ -0,0 +1,412 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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/]. +from datetime import datetime + +import pytest + +from telegram import ( + BusinessConnection, + BusinessIntro, + BusinessLocation, + BusinessMessagesDeleted, + BusinessOpeningHours, + BusinessOpeningHoursInterval, + Chat, + Location, + Sticker, + User, +) +from telegram._utils.datetime import UTC, to_timestamp +from tests.auxil.slots import mro_slots + + +class BusinessTestBase: + id_ = "123" + user = User(123, "test_user", False) + user_chat_id = 123 + date = datetime.now(tz=UTC).replace(microsecond=0) + can_reply = True + is_enabled = True + message_ids = (123, 321) + business_connection_id = "123" + chat = Chat(123, "test_chat") + title = "Business Title" + message = "Business description" + sticker = Sticker("sticker_id", "unique_id", 50, 50, True, False, Sticker.REGULAR) + address = "address" + location = Location(-23.691288, 46.788279) + opening_minute = 0 + closing_minute = 60 + time_zone_name = "Country/City" + opening_hours = [ + BusinessOpeningHoursInterval(opening, opening + 60) for opening in (0, 24 * 60) + ] + + +@pytest.fixture(scope="module") +def business_connection(): + return BusinessConnection( + BusinessTestBase.id_, + BusinessTestBase.user, + BusinessTestBase.user_chat_id, + BusinessTestBase.date, + BusinessTestBase.can_reply, + BusinessTestBase.is_enabled, + ) + + +@pytest.fixture(scope="module") +def business_messages_deleted(): + return BusinessMessagesDeleted( + BusinessTestBase.business_connection_id, + BusinessTestBase.chat, + BusinessTestBase.message_ids, + ) + + +@pytest.fixture(scope="module") +def business_intro(): + return BusinessIntro( + BusinessTestBase.title, + BusinessTestBase.message, + BusinessTestBase.sticker, + ) + + +@pytest.fixture(scope="module") +def business_location(): + return BusinessLocation( + BusinessTestBase.address, + BusinessTestBase.location, + ) + + +@pytest.fixture(scope="module") +def business_opening_hours_interval(): + return BusinessOpeningHoursInterval( + BusinessTestBase.opening_minute, + BusinessTestBase.closing_minute, + ) + + +@pytest.fixture(scope="module") +def business_opening_hours(): + return BusinessOpeningHours( + BusinessTestBase.time_zone_name, + BusinessTestBase.opening_hours, + ) + + +class TestBusinessConnectionWithoutRequest(BusinessTestBase): + def test_slots(self, business_connection): + bc = business_connection + for attr in bc.__slots__: + assert getattr(bc, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(bc)) == len(set(mro_slots(bc))), "duplicate slot" + + def test_de_json(self): + json_dict = { + "id": self.id_, + "user": self.user.to_dict(), + "user_chat_id": self.user_chat_id, + "date": to_timestamp(self.date), + "can_reply": self.can_reply, + "is_enabled": self.is_enabled, + } + bc = BusinessConnection.de_json(json_dict, None) + assert bc.id == self.id_ + assert bc.user == self.user + assert bc.user_chat_id == self.user_chat_id + assert bc.date == self.date + assert bc.can_reply == self.can_reply + assert bc.is_enabled == self.is_enabled + assert bc.api_kwargs == {} + assert isinstance(bc, BusinessConnection) + + def test_de_json_localization(self, bot, raw_bot, tz_bot): + json_dict = { + "id": self.id_, + "user": self.user.to_dict(), + "user_chat_id": self.user_chat_id, + "date": to_timestamp(self.date), + "can_reply": self.can_reply, + "is_enabled": self.is_enabled, + } + chat_bot = BusinessConnection.de_json(json_dict, bot) + chat_bot_raw = BusinessConnection.de_json(json_dict, raw_bot) + chat_bot_tz = BusinessConnection.de_json(json_dict, tz_bot) + + # comparing utcoffsets because comparing tzinfo objects is not reliable + date_offset = chat_bot_tz.date.utcoffset() + date_offset_tz = tz_bot.defaults.tzinfo.utcoffset(chat_bot_tz.date.replace(tzinfo=None)) + + assert chat_bot.date.tzinfo == UTC + assert chat_bot_raw.date.tzinfo == UTC + assert date_offset_tz == date_offset + + def test_to_dict(self, business_connection): + bc_dict = business_connection.to_dict() + assert isinstance(bc_dict, dict) + assert bc_dict["id"] == self.id_ + assert bc_dict["user"] == self.user.to_dict() + assert bc_dict["user_chat_id"] == self.user_chat_id + assert bc_dict["date"] == to_timestamp(self.date) + assert bc_dict["can_reply"] == self.can_reply + assert bc_dict["is_enabled"] == self.is_enabled + + def test_equality(self): + bc1 = BusinessConnection( + self.id_, self.user, self.user_chat_id, self.date, self.can_reply, self.is_enabled + ) + bc2 = BusinessConnection( + self.id_, self.user, self.user_chat_id, self.date, self.can_reply, self.is_enabled + ) + bc3 = BusinessConnection( + "321", self.user, self.user_chat_id, self.date, self.can_reply, self.is_enabled + ) + + assert bc1 == bc2 + assert hash(bc1) == hash(bc2) + + assert bc1 != bc3 + assert hash(bc1) != hash(bc3) + + +class TestBusinessMessagesDeleted(BusinessTestBase): + def test_slots(self, business_messages_deleted): + bmd = business_messages_deleted + for attr in bmd.__slots__: + assert getattr(bmd, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(bmd)) == len(set(mro_slots(bmd))), "duplicate slot" + + def test_to_dict(self, business_messages_deleted): + bmd_dict = business_messages_deleted.to_dict() + assert isinstance(bmd_dict, dict) + assert bmd_dict["message_ids"] == list(self.message_ids) + assert bmd_dict["business_connection_id"] == self.business_connection_id + assert bmd_dict["chat"] == self.chat.to_dict() + + def test_de_json(self): + json_dict = { + "business_connection_id": self.business_connection_id, + "chat": self.chat.to_dict(), + "message_ids": self.message_ids, + } + bmd = BusinessMessagesDeleted.de_json(json_dict, None) + assert bmd.business_connection_id == self.business_connection_id + assert bmd.chat == self.chat + assert bmd.message_ids == self.message_ids + assert bmd.api_kwargs == {} + assert isinstance(bmd, BusinessMessagesDeleted) + + def test_equality(self): + bmd1 = BusinessMessagesDeleted(self.business_connection_id, self.chat, self.message_ids) + bmd2 = BusinessMessagesDeleted(self.business_connection_id, self.chat, self.message_ids) + bmd3 = BusinessMessagesDeleted("1", Chat(4, "random"), [321, 123]) + + assert bmd1 == bmd2 + assert hash(bmd1) == hash(bmd2) + + assert bmd1 != bmd3 + assert hash(bmd1) != hash(bmd3) + + +class TestBusinessIntroWithoutRequest(BusinessTestBase): + def test_slot_behaviour(self, business_intro): + intro = business_intro + for attr in intro.__slots__: + assert getattr(intro, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(intro)) == len(set(mro_slots(intro))), "duplicate slot" + + def test_to_dict(self, business_intro): + intro_dict = business_intro.to_dict() + assert isinstance(intro_dict, dict) + assert intro_dict["title"] == self.title + assert intro_dict["message"] == self.message + assert intro_dict["sticker"] == self.sticker.to_dict() + + def test_de_json(self): + json_dict = { + "title": self.title, + "message": self.message, + "sticker": self.sticker.to_dict(), + } + intro = BusinessIntro.de_json(json_dict, None) + assert intro.title == self.title + assert intro.message == self.message + assert intro.sticker == self.sticker + assert intro.api_kwargs == {} + assert isinstance(intro, BusinessIntro) + + def test_equality(self): + intro1 = BusinessIntro(self.title, self.message, self.sticker) + intro2 = BusinessIntro(self.title, self.message, self.sticker) + intro3 = BusinessIntro("Other Business", self.message, self.sticker) + + assert intro1 == intro2 + assert hash(intro1) == hash(intro2) + assert intro1 is not intro2 + + assert intro1 != intro3 + assert hash(intro1) != hash(intro3) + + +class TestBusinessLocationWithoutRequest(BusinessTestBase): + def test_slot_behaviour(self, business_location): + inst = business_location + for attr in inst.__slots__: + assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" + + def test_to_dict(self, business_location): + blc_dict = business_location.to_dict() + assert isinstance(blc_dict, dict) + assert blc_dict["address"] == self.address + assert blc_dict["location"] == self.location.to_dict() + + def test_de_json(self): + json_dict = { + "address": self.address, + "location": self.location.to_dict(), + } + blc = BusinessLocation.de_json(json_dict, None) + assert blc.address == self.address + assert blc.location == self.location + assert blc.api_kwargs == {} + assert isinstance(blc, BusinessLocation) + + def test_equality(self): + blc1 = BusinessLocation(self.address, self.location) + blc2 = BusinessLocation(self.address, self.location) + blc3 = BusinessLocation("Other Address", self.location) + + assert blc1 == blc2 + assert hash(blc1) == hash(blc2) + assert blc1 is not blc2 + + assert blc1 != blc3 + assert hash(blc1) != hash(blc3) + + +class TestBusinessOpeningHoursIntervalWithoutRequest(BusinessTestBase): + def test_slot_behaviour(self, business_opening_hours_interval): + inst = business_opening_hours_interval + for attr in inst.__slots__: + assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" + + def test_to_dict(self, business_opening_hours_interval): + bohi_dict = business_opening_hours_interval.to_dict() + assert isinstance(bohi_dict, dict) + assert bohi_dict["opening_minute"] == self.opening_minute + assert bohi_dict["closing_minute"] == self.closing_minute + + def test_de_json(self): + json_dict = { + "opening_minute": self.opening_minute, + "closing_minute": self.closing_minute, + } + bohi = BusinessOpeningHoursInterval.de_json(json_dict, None) + assert bohi.opening_minute == self.opening_minute + assert bohi.closing_minute == self.closing_minute + assert bohi.api_kwargs == {} + assert isinstance(bohi, BusinessOpeningHoursInterval) + + def test_equality(self): + bohi1 = BusinessOpeningHoursInterval(self.opening_minute, self.closing_minute) + bohi2 = BusinessOpeningHoursInterval(self.opening_minute, self.closing_minute) + bohi3 = BusinessOpeningHoursInterval(61, 100) + + assert bohi1 == bohi2 + assert hash(bohi1) == hash(bohi2) + assert bohi1 is not bohi2 + + assert bohi1 != bohi3 + assert hash(bohi1) != hash(bohi3) + + @pytest.mark.parametrize( + ("opening_minute", "expected"), + [ # openings per docstring + (8 * 60, (0, 8, 0)), + (24 * 60, (1, 0, 0)), + (6 * 24 * 60, (6, 0, 0)), + ], + ) + def test_opening_time(self, opening_minute, expected): + bohi = BusinessOpeningHoursInterval(opening_minute, -0) + + opening_time = bohi.opening_time + assert opening_time == expected + + cached = bohi.opening_time + assert cached is opening_time + + @pytest.mark.parametrize( + ("closing_minute", "expected"), + [ # closings per docstring + (20 * 60 + 30, (0, 20, 30)), + (2 * 24 * 60 - 1, (1, 23, 59)), + (7 * 24 * 60 - 2, (6, 23, 58)), + ], + ) + def test_closing_time(self, closing_minute, expected): + bohi = BusinessOpeningHoursInterval(-0, closing_minute) + + closing_time = bohi.closing_time + assert closing_time == expected + + cached = bohi.closing_time + assert cached is closing_time + + +class TestBusinessOpeningHoursWithoutRequest(BusinessTestBase): + def test_slot_behaviour(self, business_opening_hours): + inst = business_opening_hours + for attr in inst.__slots__: + assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" + + def test_to_dict(self, business_opening_hours): + boh_dict = business_opening_hours.to_dict() + assert isinstance(boh_dict, dict) + assert boh_dict["time_zone_name"] == self.time_zone_name + assert boh_dict["opening_hours"] == [opening.to_dict() for opening in self.opening_hours] + + def test_de_json(self): + json_dict = { + "time_zone_name": self.time_zone_name, + "opening_hours": [opening.to_dict() for opening in self.opening_hours], + } + boh = BusinessOpeningHours.de_json(json_dict, None) + assert boh.time_zone_name == self.time_zone_name + assert boh.opening_hours == tuple(self.opening_hours) + assert boh.api_kwargs == {} + assert isinstance(boh, BusinessOpeningHours) + + def test_equality(self): + boh1 = BusinessOpeningHours(self.time_zone_name, self.opening_hours) + boh2 = BusinessOpeningHours(self.time_zone_name, self.opening_hours) + boh3 = BusinessOpeningHours("Other/Timezone", self.opening_hours) + + assert boh1 == boh2 + assert hash(boh1) == hash(boh2) + assert boh1 is not boh2 + + assert boh1 != boh3 + assert hash(boh1) != hash(boh3) diff --git a/test_callbackquery.py b/test_callbackquery.py new file mode 100644 index 0000000000000000000000000000000000000000..3131b34f249e463c37399f285297ac03decb9449 --- /dev/null +++ b/test_callbackquery.py @@ -0,0 +1,539 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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/]. + +from datetime import datetime + +import pytest + +from telegram import Audio, Bot, CallbackQuery, Chat, InaccessibleMessage, Message, User +from tests.auxil.bot_method_checks import ( + check_defaults_handling, + check_shortcut_call, + check_shortcut_signature, +) +from tests.auxil.slots import mro_slots + + +@pytest.fixture(params=["message", "inline", "inaccessible_message"]) +def callback_query(bot, request): + cbq = CallbackQuery( + CallbackQueryTestBase.id_, + CallbackQueryTestBase.from_user, + CallbackQueryTestBase.chat_instance, + data=CallbackQueryTestBase.data, + game_short_name=CallbackQueryTestBase.game_short_name, + ) + cbq.set_bot(bot) + cbq._unfreeze() + if request.param == "message": + cbq.message = CallbackQueryTestBase.message + cbq.message.set_bot(bot) + elif request.param == "inline": + cbq.inline_message_id = CallbackQueryTestBase.inline_message_id + elif request.param == "inaccessible_message": + cbq.message = InaccessibleMessage( + chat=CallbackQueryTestBase.message.chat, + message_id=CallbackQueryTestBase.message.message_id, + ) + return cbq + + +class CallbackQueryTestBase: + id_ = "id" + from_user = User(1, "test_user", False) + chat_instance = "chat_instance" + message = Message(3, datetime.utcnow(), Chat(4, "private"), from_user=User(5, "bot", False)) + data = "data" + inline_message_id = "inline_message_id" + game_short_name = "the_game" + + +class TestCallbackQueryWithoutRequest(CallbackQueryTestBase): + @staticmethod + def skip_params(callback_query: CallbackQuery): + if callback_query.inline_message_id: + return {"message_id", "chat_id", "business_connection_id"} + return {"inline_message_id", "business_connection_id"} + + @staticmethod + def shortcut_kwargs(callback_query: CallbackQuery): + if not callback_query.inline_message_id: + return {"message_id", "chat_id"} + return {"inline_message_id"} + + @staticmethod + def check_passed_ids(callback_query: CallbackQuery, kwargs): + if callback_query.inline_message_id: + id_ = kwargs["inline_message_id"] == callback_query.inline_message_id + chat_id = kwargs["chat_id"] is None + message_id = kwargs["message_id"] is None + else: + id_ = kwargs["inline_message_id"] is None + chat_id = kwargs["chat_id"] == callback_query.message.chat_id + message_id = kwargs["message_id"] == callback_query.message.message_id + return id_ and chat_id and message_id + + def test_slot_behaviour(self, callback_query): + for attr in callback_query.__slots__: + assert getattr(callback_query, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(callback_query)) == len(set(mro_slots(callback_query))), "same slot" + + def test_de_json(self, bot): + json_dict = { + "id": self.id_, + "from": self.from_user.to_dict(), + "chat_instance": self.chat_instance, + "message": self.message.to_dict(), + "data": self.data, + "inline_message_id": self.inline_message_id, + "game_short_name": self.game_short_name, + } + callback_query = CallbackQuery.de_json(json_dict, bot) + assert callback_query.api_kwargs == {} + + assert callback_query.id == self.id_ + assert callback_query.from_user == self.from_user + assert callback_query.chat_instance == self.chat_instance + assert callback_query.message == self.message + assert callback_query.data == self.data + assert callback_query.inline_message_id == self.inline_message_id + assert callback_query.game_short_name == self.game_short_name + + def test_to_dict(self, callback_query): + callback_query_dict = callback_query.to_dict() + + assert isinstance(callback_query_dict, dict) + assert callback_query_dict["id"] == callback_query.id + assert callback_query_dict["from"] == callback_query.from_user.to_dict() + assert callback_query_dict["chat_instance"] == callback_query.chat_instance + if callback_query.message is not None: + assert callback_query_dict["message"] == callback_query.message.to_dict() + elif callback_query.inline_message_id: + assert callback_query_dict["inline_message_id"] == callback_query.inline_message_id + assert callback_query_dict["data"] == callback_query.data + assert callback_query_dict["game_short_name"] == callback_query.game_short_name + + def test_equality(self): + a = CallbackQuery(self.id_, self.from_user, "chat") + b = CallbackQuery(self.id_, self.from_user, "chat") + c = CallbackQuery(self.id_, None, "") + d = CallbackQuery("", None, "chat") + e = Audio(self.id_, "unique_id", 1) + + assert a == b + assert hash(a) == hash(b) + assert a is not b + + assert a == c + assert hash(a) == hash(c) + + assert a != d + assert hash(a) != hash(d) + + assert a != e + assert hash(a) != hash(e) + + async def test_answer(self, monkeypatch, callback_query): + async def make_assertion(*_, **kwargs): + return kwargs["callback_query_id"] == callback_query.id + + assert check_shortcut_signature( + CallbackQuery.answer, Bot.answer_callback_query, ["callback_query_id"], [] + ) + assert await check_shortcut_call( + callback_query.answer, callback_query.get_bot(), "answer_callback_query" + ) + assert await check_defaults_handling(callback_query.answer, callback_query.get_bot()) + + monkeypatch.setattr(callback_query.get_bot(), "answer_callback_query", make_assertion) + assert await callback_query.answer() + + async def test_edit_message_text(self, monkeypatch, callback_query): + if isinstance(callback_query.message, InaccessibleMessage): + with pytest.raises(TypeError, match="inaccessible message"): + await callback_query.edit_message_text("test") + return + + async def make_assertion(*_, **kwargs): + text = kwargs["text"] == "test" + ids = self.check_passed_ids(callback_query, kwargs) + return ids and text + + assert check_shortcut_signature( + CallbackQuery.edit_message_text, + Bot.edit_message_text, + ["inline_message_id", "message_id", "chat_id", "business_connection_id"], + [], + ) + assert await check_shortcut_call( + callback_query.edit_message_text, + callback_query.get_bot(), + "edit_message_text", + skip_params=self.skip_params(callback_query), + shortcut_kwargs=self.shortcut_kwargs(callback_query), + ) + assert await check_defaults_handling( + callback_query.edit_message_text, callback_query.get_bot() + ) + + monkeypatch.setattr(callback_query.get_bot(), "edit_message_text", make_assertion) + assert await callback_query.edit_message_text(text="test") + assert await callback_query.edit_message_text("test") + + async def test_edit_message_caption(self, monkeypatch, callback_query): + if isinstance(callback_query.message, InaccessibleMessage): + with pytest.raises(TypeError, match="inaccessible message"): + await callback_query.edit_message_caption("test") + return + + async def make_assertion(*_, **kwargs): + caption = kwargs["caption"] == "new caption" + ids = self.check_passed_ids(callback_query, kwargs) + return ids and caption + + assert check_shortcut_signature( + CallbackQuery.edit_message_caption, + Bot.edit_message_caption, + ["inline_message_id", "message_id", "chat_id", "business_connection_id"], + [], + ) + assert await check_shortcut_call( + callback_query.edit_message_caption, + callback_query.get_bot(), + "edit_message_caption", + skip_params=self.skip_params(callback_query), + shortcut_kwargs=self.shortcut_kwargs(callback_query), + ) + assert await check_defaults_handling( + callback_query.edit_message_caption, callback_query.get_bot() + ) + + monkeypatch.setattr(callback_query.get_bot(), "edit_message_caption", make_assertion) + assert await callback_query.edit_message_caption(caption="new caption") + assert await callback_query.edit_message_caption("new caption") + + async def test_edit_message_reply_markup(self, monkeypatch, callback_query): + if isinstance(callback_query.message, InaccessibleMessage): + with pytest.raises(TypeError, match="inaccessible message"): + await callback_query.edit_message_reply_markup("test") + return + + async def make_assertion(*_, **kwargs): + reply_markup = kwargs["reply_markup"] == [["1", "2"]] + ids = self.check_passed_ids(callback_query, kwargs) + return ids and reply_markup + + assert check_shortcut_signature( + CallbackQuery.edit_message_reply_markup, + Bot.edit_message_reply_markup, + ["inline_message_id", "message_id", "chat_id", "business_connection_id"], + [], + ) + assert await check_shortcut_call( + callback_query.edit_message_reply_markup, + callback_query.get_bot(), + "edit_message_reply_markup", + skip_params=self.skip_params(callback_query), + shortcut_kwargs=self.shortcut_kwargs(callback_query), + ) + assert await check_defaults_handling( + callback_query.edit_message_reply_markup, callback_query.get_bot() + ) + + monkeypatch.setattr(callback_query.get_bot(), "edit_message_reply_markup", make_assertion) + assert await callback_query.edit_message_reply_markup(reply_markup=[["1", "2"]]) + assert await callback_query.edit_message_reply_markup([["1", "2"]]) + + async def test_edit_message_media(self, monkeypatch, callback_query): + if isinstance(callback_query.message, InaccessibleMessage): + with pytest.raises(TypeError, match="inaccessible message"): + await callback_query.edit_message_media("test") + return + + async def make_assertion(*_, **kwargs): + message_media = kwargs.get("media") == [["1", "2"]] + ids = self.check_passed_ids(callback_query, kwargs) + return ids and message_media + + assert check_shortcut_signature( + CallbackQuery.edit_message_media, + Bot.edit_message_media, + ["inline_message_id", "message_id", "chat_id", "business_connection_id"], + [], + ) + assert await check_shortcut_call( + callback_query.edit_message_media, + callback_query.get_bot(), + "edit_message_media", + skip_params=self.skip_params(callback_query), + shortcut_kwargs=self.shortcut_kwargs(callback_query), + ) + assert await check_defaults_handling( + callback_query.edit_message_media, callback_query.get_bot() + ) + + monkeypatch.setattr(callback_query.get_bot(), "edit_message_media", make_assertion) + assert await callback_query.edit_message_media(media=[["1", "2"]]) + assert await callback_query.edit_message_media([["1", "2"]]) + + async def test_edit_message_live_location(self, monkeypatch, callback_query): + if isinstance(callback_query.message, InaccessibleMessage): + with pytest.raises(TypeError, match="inaccessible message"): + await callback_query.edit_message_live_location("test") + return + + async def make_assertion(*_, **kwargs): + latitude = kwargs.get("latitude") == 1 + longitude = kwargs.get("longitude") == 2 + live = kwargs.get("live_period") == 900 + ids = self.check_passed_ids(callback_query, kwargs) + return ids and latitude and longitude and live + + assert check_shortcut_signature( + CallbackQuery.edit_message_live_location, + Bot.edit_message_live_location, + ["inline_message_id", "message_id", "chat_id", "business_connection_id"], + [], + ) + assert await check_shortcut_call( + callback_query.edit_message_live_location, + callback_query.get_bot(), + "edit_message_live_location", + skip_params=self.skip_params(callback_query), + shortcut_kwargs=self.shortcut_kwargs(callback_query), + ) + assert await check_defaults_handling( + callback_query.edit_message_live_location, callback_query.get_bot() + ) + + monkeypatch.setattr(callback_query.get_bot(), "edit_message_live_location", make_assertion) + assert await callback_query.edit_message_live_location( + latitude=1, longitude=2, live_period=900 + ) + assert await callback_query.edit_message_live_location(1, 2, live_period=900) + + async def test_stop_message_live_location(self, monkeypatch, callback_query): + if isinstance(callback_query.message, InaccessibleMessage): + with pytest.raises(TypeError, match="inaccessible message"): + await callback_query.stop_message_live_location("test") + return + + async def make_assertion(*_, **kwargs): + return self.check_passed_ids(callback_query, kwargs) + + assert check_shortcut_signature( + CallbackQuery.stop_message_live_location, + Bot.stop_message_live_location, + ["inline_message_id", "message_id", "chat_id", "business_connection_id"], + [], + ) + assert await check_shortcut_call( + callback_query.stop_message_live_location, + callback_query.get_bot(), + "stop_message_live_location", + skip_params=self.skip_params(callback_query), + shortcut_kwargs=self.shortcut_kwargs(callback_query), + ) + assert await check_defaults_handling( + callback_query.stop_message_live_location, callback_query.get_bot() + ) + + monkeypatch.setattr(callback_query.get_bot(), "stop_message_live_location", make_assertion) + assert await callback_query.stop_message_live_location() + + async def test_set_game_score(self, monkeypatch, callback_query): + if isinstance(callback_query.message, InaccessibleMessage): + with pytest.raises(TypeError, match="inaccessible message"): + await callback_query.set_game_score(user_id=1, score=2) + return + + async def make_assertion(*_, **kwargs): + user_id = kwargs.get("user_id") == 1 + score = kwargs.get("score") == 2 + ids = self.check_passed_ids(callback_query, kwargs) + return ids and user_id and score + + assert check_shortcut_signature( + CallbackQuery.set_game_score, + Bot.set_game_score, + ["inline_message_id", "message_id", "chat_id"], + [], + ) + assert await check_shortcut_call( + callback_query.set_game_score, + callback_query.get_bot(), + "set_game_score", + skip_params=self.skip_params(callback_query), + shortcut_kwargs=self.shortcut_kwargs(callback_query), + ) + assert await check_defaults_handling( + callback_query.set_game_score, callback_query.get_bot() + ) + + monkeypatch.setattr(callback_query.get_bot(), "set_game_score", make_assertion) + assert await callback_query.set_game_score(user_id=1, score=2) + assert await callback_query.set_game_score(1, 2) + + async def test_get_game_high_scores(self, monkeypatch, callback_query): + if isinstance(callback_query.message, InaccessibleMessage): + with pytest.raises(TypeError, match="inaccessible message"): + await callback_query.get_game_high_scores("test") + return + + async def make_assertion(*_, **kwargs): + user_id = kwargs.get("user_id") == 1 + ids = self.check_passed_ids(callback_query, kwargs) + return ids and user_id + + assert check_shortcut_signature( + CallbackQuery.get_game_high_scores, + Bot.get_game_high_scores, + ["inline_message_id", "message_id", "chat_id"], + [], + ) + assert await check_shortcut_call( + callback_query.get_game_high_scores, + callback_query.get_bot(), + "get_game_high_scores", + skip_params=self.skip_params(callback_query), + shortcut_kwargs=self.shortcut_kwargs(callback_query), + ) + assert await check_defaults_handling( + callback_query.get_game_high_scores, callback_query.get_bot() + ) + + monkeypatch.setattr(callback_query.get_bot(), "get_game_high_scores", make_assertion) + assert await callback_query.get_game_high_scores(user_id=1) + assert await callback_query.get_game_high_scores(1) + + async def test_delete_message(self, monkeypatch, callback_query): + if isinstance(callback_query.message, InaccessibleMessage): + with pytest.raises(TypeError, match="inaccessible message"): + await callback_query.delete_message() + return + if callback_query.inline_message_id: + pytest.skip("Can't delete inline messages") + + async def make_assertion(*args, **kwargs): + id_ = kwargs["chat_id"] == callback_query.message.chat_id + message = kwargs["message_id"] == callback_query.message.message_id + return id_ and message + + assert check_shortcut_signature( + CallbackQuery.delete_message, + Bot.delete_message, + ["message_id", "chat_id"], + [], + ) + assert await check_shortcut_call( + callback_query.delete_message, callback_query.get_bot(), "delete_message" + ) + assert await check_defaults_handling( + callback_query.delete_message, callback_query.get_bot() + ) + + monkeypatch.setattr(callback_query.get_bot(), "delete_message", make_assertion) + assert await callback_query.delete_message() + + async def test_pin_message(self, monkeypatch, callback_query): + if isinstance(callback_query.message, InaccessibleMessage): + with pytest.raises(TypeError, match="inaccessible message"): + await callback_query.pin_message() + return + if callback_query.inline_message_id: + pytest.skip("Can't pin inline messages") + + async def make_assertion(*args, **kwargs): + return kwargs["chat_id"] == callback_query.message.chat_id + + assert check_shortcut_signature( + CallbackQuery.pin_message, + Bot.pin_chat_message, + ["message_id", "chat_id", "business_connection_id"], + [], + ) + assert await check_shortcut_call( + callback_query.pin_message, + callback_query.get_bot(), + "pin_chat_message", + ["business_connection_id"], + ) + assert await check_defaults_handling(callback_query.pin_message, callback_query.get_bot()) + + monkeypatch.setattr(callback_query.get_bot(), "pin_chat_message", make_assertion) + assert await callback_query.pin_message() + + async def test_unpin_message(self, monkeypatch, callback_query): + if isinstance(callback_query.message, InaccessibleMessage): + with pytest.raises(TypeError, match="inaccessible message"): + await callback_query.unpin_message() + return + if callback_query.inline_message_id: + pytest.skip("Can't unpin inline messages") + + async def make_assertion(*args, **kwargs): + return kwargs["chat_id"] == callback_query.message.chat_id + + assert check_shortcut_signature( + CallbackQuery.unpin_message, + Bot.unpin_chat_message, + ["message_id", "chat_id", "business_connection_id"], + [], + ) + assert await check_shortcut_call( + callback_query.unpin_message, + callback_query.get_bot(), + "unpin_chat_message", + shortcut_kwargs=["message_id"], + skip_params=["business_connection_id"], + ) + assert await check_defaults_handling( + callback_query.unpin_message, callback_query.get_bot() + ) + + monkeypatch.setattr(callback_query.get_bot(), "unpin_chat_message", make_assertion) + assert await callback_query.unpin_message() + + async def test_copy_message(self, monkeypatch, callback_query): + if isinstance(callback_query.message, InaccessibleMessage): + with pytest.raises(TypeError, match="inaccessible message"): + await callback_query.copy_message(1) + return + if callback_query.inline_message_id: + pytest.skip("Can't copy inline messages") + + async def make_assertion(*args, **kwargs): + id_ = kwargs["from_chat_id"] == callback_query.message.chat_id + chat_id = kwargs["chat_id"] == 1 + message = kwargs["message_id"] == callback_query.message.message_id + return id_ and message and chat_id + + assert check_shortcut_signature( + CallbackQuery.copy_message, + Bot.copy_message, + ["message_id", "from_chat_id"], + [], + ) + assert await check_shortcut_call( + callback_query.copy_message, callback_query.get_bot(), "copy_message" + ) + assert await check_defaults_handling(callback_query.copy_message, callback_query.get_bot()) + + monkeypatch.setattr(callback_query.get_bot(), "copy_message", make_assertion) + assert await callback_query.copy_message(1) diff --git a/test_chat.py b/test_chat.py new file mode 100644 index 0000000000000000000000000000000000000000..a3dcd6aa17f57ac9c4717d9ee4d25e33430cbe05 --- /dev/null +++ b/test_chat.py @@ -0,0 +1,1402 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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/]. + + +import pytest + +from telegram import Bot, Chat, ChatPermissions, ReactionTypeEmoji, User +from telegram.constants import ChatAction, ChatType, ReactionEmoji +from telegram.helpers import escape_markdown +from tests.auxil.bot_method_checks import ( + check_defaults_handling, + check_shortcut_call, + check_shortcut_signature, +) +from tests.auxil.slots import mro_slots + + +@pytest.fixture(scope="module") +def chat(bot): + chat = Chat( + ChatTestBase.id_, + title=ChatTestBase.title, + type=ChatTestBase.type_, + username=ChatTestBase.username, + is_forum=True, + first_name=ChatTestBase.first_name, + last_name=ChatTestBase.last_name, + ) + chat.set_bot(bot) + chat._unfreeze() + return chat + + +class ChatTestBase: + id_ = -28767330 + title = "ToledosPalaceBot - Group" + type_ = "group" + username = "username" + is_forum = True + first_name = "first" + last_name = "last" + + +class TestChatWithoutRequest(ChatTestBase): + def test_slot_behaviour(self, chat): + for attr in chat.__slots__: + assert getattr(chat, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(chat)) == len(set(mro_slots(chat))), "duplicate slot" + + def test_de_json(self, bot): + json_dict = { + "id": self.id_, + "title": self.title, + "type": self.type_, + "username": self.username, + "is_forum": self.is_forum, + "first_name": self.first_name, + "last_name": self.last_name, + } + chat = Chat.de_json(json_dict, bot) + + assert chat.id == self.id_ + assert chat.title == self.title + assert chat.type == self.type_ + assert chat.username == self.username + assert chat.is_forum == self.is_forum + assert chat.first_name == self.first_name + assert chat.last_name == self.last_name + + def test_to_dict(self, chat): + chat_dict = chat.to_dict() + + assert isinstance(chat_dict, dict) + assert chat_dict["id"] == chat.id + assert chat_dict["title"] == chat.title + assert chat_dict["type"] == chat.type + assert chat_dict["username"] == chat.username + assert chat_dict["is_forum"] == chat.is_forum + assert chat_dict["first_name"] == chat.first_name + assert chat_dict["last_name"] == chat.last_name + + def test_enum_init(self): + chat = Chat(id=1, type="foo") + assert chat.type == "foo" + chat = Chat(id=1, type="private") + assert chat.type is ChatType.PRIVATE + + def test_equality(self): + a = Chat(self.id_, self.title, self.type_) + b = Chat(self.id_, self.title, self.type_) + c = Chat(self.id_, "", "") + d = Chat(0, self.title, self.type_) + e = User(self.id_, "", False) + + assert a == b + assert hash(a) == hash(b) + assert a is not b + + assert a == c + assert hash(a) == hash(c) + + assert a != d + assert hash(a) != hash(d) + + assert a != e + assert hash(a) != hash(e) + + def test_link(self, chat): + assert chat.link == f"https://t.me/{chat.username}" + chat.username = None + assert chat.link is None + + def test_full_name(self): + chat = Chat( + id=1, type=Chat.PRIVATE, first_name="first\u2022name", last_name="last\u2022name" + ) + assert chat.full_name == "first\u2022name last\u2022name" + chat = Chat(id=1, type=Chat.PRIVATE, first_name="first\u2022name") + assert chat.full_name == "first\u2022name" + chat = Chat(id=1, type=Chat.PRIVATE) + assert chat.full_name is None + + def test_effective_name(self): + chat = Chat(id=1, type=Chat.PRIVATE, first_name="first\u2022name") + assert chat.effective_name == "first\u2022name" + chat = Chat(id=1, type=Chat.GROUP, title="group") + assert chat.effective_name == "group" + chat = Chat(id=1, type=Chat.GROUP, first_name="first\u2022name", title="group") + assert chat.effective_name == "group" + chat = Chat(id=1, type=Chat.GROUP) + assert chat.effective_name is None + + async def test_delete_message(self, monkeypatch, chat): + async def make_assertion(*_, **kwargs): + return kwargs["chat_id"] == chat.id and kwargs["message_id"] == 42 + + assert check_shortcut_signature(Chat.delete_message, Bot.delete_message, ["chat_id"], []) + assert await check_shortcut_call(chat.delete_message, chat.get_bot(), "delete_message") + assert await check_defaults_handling(chat.delete_message, chat.get_bot()) + + monkeypatch.setattr(chat.get_bot(), "delete_message", make_assertion) + assert await chat.delete_message(message_id=42) + + async def test_delete_messages(self, monkeypatch, chat): + async def make_assertion(*_, **kwargs): + return kwargs["chat_id"] == chat.id and kwargs["message_ids"] == (42, 43) + + assert check_shortcut_signature(Chat.delete_messages, Bot.delete_messages, ["chat_id"], []) + assert await check_shortcut_call(chat.delete_messages, chat.get_bot(), "delete_messages") + assert await check_defaults_handling(chat.delete_messages, chat.get_bot()) + + monkeypatch.setattr(chat.get_bot(), "delete_messages", make_assertion) + assert await chat.delete_messages(message_ids=(42, 43)) + + async def test_send_action(self, monkeypatch, chat): + async def make_assertion(*_, **kwargs): + id_ = kwargs["chat_id"] == chat.id + action = kwargs["action"] == ChatAction.TYPING + return id_ and action + + assert check_shortcut_signature(chat.send_action, Bot.send_chat_action, ["chat_id"], []) + assert await check_shortcut_call(chat.send_action, chat.get_bot(), "send_chat_action") + assert await check_defaults_handling(chat.send_action, chat.get_bot()) + + monkeypatch.setattr(chat.get_bot(), "send_chat_action", make_assertion) + assert await chat.send_action(action=ChatAction.TYPING) + + async def test_leave(self, monkeypatch, chat): + async def make_assertion(*_, **kwargs): + return kwargs["chat_id"] == chat.id + + assert check_shortcut_signature(Chat.leave, Bot.leave_chat, ["chat_id"], []) + assert await check_shortcut_call(chat.leave, chat.get_bot(), "leave_chat") + assert await check_defaults_handling(chat.leave, chat.get_bot()) + + monkeypatch.setattr(chat.get_bot(), "leave_chat", make_assertion) + assert await chat.leave() + + async def test_get_administrators(self, monkeypatch, chat): + async def make_assertion(*_, **kwargs): + return kwargs["chat_id"] == chat.id + + assert check_shortcut_signature( + Chat.get_administrators, Bot.get_chat_administrators, ["chat_id"], [] + ) + assert await check_shortcut_call( + chat.get_administrators, chat.get_bot(), "get_chat_administrators" + ) + assert await check_defaults_handling(chat.get_administrators, chat.get_bot()) + + monkeypatch.setattr(chat.get_bot(), "get_chat_administrators", make_assertion) + assert await chat.get_administrators() + + async def test_get_members_count(self, monkeypatch, chat): + async def make_assertion(*_, **kwargs): + return kwargs["chat_id"] == chat.id + + assert check_shortcut_signature( + Chat.get_member_count, Bot.get_chat_member_count, ["chat_id"], [] + ) + assert await check_shortcut_call( + chat.get_member_count, chat.get_bot(), "get_chat_member_count" + ) + assert await check_defaults_handling(chat.get_member_count, chat.get_bot()) + + monkeypatch.setattr(chat.get_bot(), "get_chat_member_count", make_assertion) + assert await chat.get_member_count() + + async def test_get_member(self, monkeypatch, chat): + async def make_assertion(*_, **kwargs): + chat_id = kwargs["chat_id"] == chat.id + user_id = kwargs["user_id"] == 42 + return chat_id and user_id + + assert check_shortcut_signature(Chat.get_member, Bot.get_chat_member, ["chat_id"], []) + assert await check_shortcut_call(chat.get_member, chat.get_bot(), "get_chat_member") + assert await check_defaults_handling(chat.get_member, chat.get_bot()) + + monkeypatch.setattr(chat.get_bot(), "get_chat_member", make_assertion) + assert await chat.get_member(user_id=42) + + async def test_ban_member(self, monkeypatch, chat): + async def make_assertion(*_, **kwargs): + chat_id = kwargs["chat_id"] == chat.id + user_id = kwargs["user_id"] == 42 + until = kwargs["until_date"] == 43 + return chat_id and user_id and until + + assert check_shortcut_signature(Chat.ban_member, Bot.ban_chat_member, ["chat_id"], []) + assert await check_shortcut_call(chat.ban_member, chat.get_bot(), "ban_chat_member") + assert await check_defaults_handling(chat.ban_member, chat.get_bot()) + + monkeypatch.setattr(chat.get_bot(), "ban_chat_member", make_assertion) + assert await chat.ban_member(user_id=42, until_date=43) + + async def test_ban_sender_chat(self, monkeypatch, chat): + async def make_assertion(*_, **kwargs): + chat_id = kwargs["chat_id"] == chat.id + sender_chat_id = kwargs["sender_chat_id"] == 42 + return chat_id and sender_chat_id + + assert check_shortcut_signature( + Chat.ban_sender_chat, Bot.ban_chat_sender_chat, ["chat_id"], [] + ) + assert await check_shortcut_call( + chat.ban_sender_chat, chat.get_bot(), "ban_chat_sender_chat" + ) + assert await check_defaults_handling(chat.ban_sender_chat, chat.get_bot()) + + monkeypatch.setattr(chat.get_bot(), "ban_chat_sender_chat", make_assertion) + assert await chat.ban_sender_chat(42) + + async def test_ban_chat(self, monkeypatch, chat): + async def make_assertion(*_, **kwargs): + chat_id = kwargs["chat_id"] == 42 + sender_chat_id = kwargs["sender_chat_id"] == chat.id + return chat_id and sender_chat_id + + assert check_shortcut_signature( + Chat.ban_chat, Bot.ban_chat_sender_chat, ["sender_chat_id"], [] + ) + assert await check_shortcut_call(chat.ban_chat, chat.get_bot(), "ban_chat_sender_chat") + assert await check_defaults_handling(chat.ban_chat, chat.get_bot()) + + monkeypatch.setattr(chat.get_bot(), "ban_chat_sender_chat", make_assertion) + assert await chat.ban_chat(42) + + @pytest.mark.parametrize("only_if_banned", [True, False, None]) + async def test_unban_member(self, monkeypatch, chat, only_if_banned): + async def make_assertion(*_, **kwargs): + chat_id = kwargs["chat_id"] == chat.id + user_id = kwargs["user_id"] == 42 + o_i_b = kwargs.get("only_if_banned", None) == only_if_banned + return chat_id and user_id and o_i_b + + assert check_shortcut_signature(Chat.unban_member, Bot.unban_chat_member, ["chat_id"], []) + assert await check_shortcut_call(chat.unban_member, chat.get_bot(), "unban_chat_member") + assert await check_defaults_handling(chat.unban_member, chat.get_bot()) + + monkeypatch.setattr(chat.get_bot(), "unban_chat_member", make_assertion) + assert await chat.unban_member(user_id=42, only_if_banned=only_if_banned) + + async def test_unban_sender_chat(self, monkeypatch, chat): + async def make_assertion(*_, **kwargs): + chat_id = kwargs["chat_id"] == chat.id + sender_chat_id = kwargs["sender_chat_id"] == 42 + return chat_id and sender_chat_id + + assert check_shortcut_signature( + Chat.unban_sender_chat, Bot.unban_chat_sender_chat, ["chat_id"], [] + ) + assert await check_shortcut_call( + chat.unban_sender_chat, chat.get_bot(), "unban_chat_sender_chat" + ) + assert await check_defaults_handling(chat.unban_sender_chat, chat.get_bot()) + + monkeypatch.setattr(chat.get_bot(), "unban_chat_sender_chat", make_assertion) + assert await chat.unban_sender_chat(42) + + async def test_unban_chat(self, monkeypatch, chat): + async def make_assertion(*_, **kwargs): + chat_id = kwargs["chat_id"] == 42 + sender_chat_id = kwargs["sender_chat_id"] == chat.id + return chat_id and sender_chat_id + + assert check_shortcut_signature( + Chat.unban_chat, Bot.ban_chat_sender_chat, ["sender_chat_id"], [] + ) + assert await check_shortcut_call(chat.unban_chat, chat.get_bot(), "unban_chat_sender_chat") + assert await check_defaults_handling(chat.unban_chat, chat.get_bot()) + + monkeypatch.setattr(chat.get_bot(), "unban_chat_sender_chat", make_assertion) + assert await chat.unban_chat(42) + + @pytest.mark.parametrize("is_anonymous", [True, False, None]) + async def test_promote_member(self, monkeypatch, chat, is_anonymous): + async def make_assertion(*_, **kwargs): + chat_id = kwargs["chat_id"] == chat.id + user_id = kwargs["user_id"] == 42 + o_i_b = kwargs.get("is_anonymous", None) == is_anonymous + return chat_id and user_id and o_i_b + + assert check_shortcut_signature( + Chat.promote_member, Bot.promote_chat_member, ["chat_id"], [] + ) + assert await check_shortcut_call( + chat.promote_member, chat.get_bot(), "promote_chat_member" + ) + assert await check_defaults_handling(chat.promote_member, chat.get_bot()) + + monkeypatch.setattr(chat.get_bot(), "promote_chat_member", make_assertion) + assert await chat.promote_member(user_id=42, is_anonymous=is_anonymous) + + async def test_restrict_member(self, monkeypatch, chat): + permissions = ChatPermissions(True, False, True, False, True, False, True, False) + + async def make_assertion(*_, **kwargs): + chat_id = kwargs["chat_id"] == chat.id + user_id = kwargs["user_id"] == 42 + o_i_b = kwargs.get("permissions", None) == permissions + return chat_id and user_id and o_i_b + + assert check_shortcut_signature( + Chat.restrict_member, Bot.restrict_chat_member, ["chat_id"], [] + ) + assert await check_shortcut_call( + chat.restrict_member, chat.get_bot(), "restrict_chat_member" + ) + assert await check_defaults_handling(chat.restrict_member, chat.get_bot()) + + monkeypatch.setattr(chat.get_bot(), "restrict_chat_member", make_assertion) + assert await chat.restrict_member(user_id=42, permissions=permissions) + + async def test_set_permissions(self, monkeypatch, chat): + async def make_assertion(*_, **kwargs): + chat_id = kwargs["chat_id"] == chat.id + permissions = kwargs["permissions"] == ChatPermissions.all_permissions() + return chat_id and permissions + + assert check_shortcut_signature( + Chat.set_permissions, Bot.set_chat_permissions, ["chat_id"], [] + ) + assert await check_shortcut_call( + chat.set_permissions, chat.get_bot(), "set_chat_permissions" + ) + assert await check_defaults_handling(chat.set_permissions, chat.get_bot()) + + monkeypatch.setattr(chat.get_bot(), "set_chat_permissions", make_assertion) + assert await chat.set_permissions(permissions=ChatPermissions.all_permissions()) + + async def test_set_administrator_custom_title(self, monkeypatch, chat): + async def make_assertion(*_, **kwargs): + chat_id = kwargs["chat_id"] == chat.id + user_id = kwargs["user_id"] == 42 + custom_title = kwargs["custom_title"] == "custom_title" + return chat_id and user_id and custom_title + + assert check_shortcut_signature( + Chat.set_administrator_custom_title, + Bot.set_chat_administrator_custom_title, + ["chat_id"], + [], + ) + assert await check_shortcut_call( + chat.set_administrator_custom_title, + chat.get_bot(), + "set_chat_administrator_custom_title", + ) + assert await check_defaults_handling(chat.set_administrator_custom_title, chat.get_bot()) + + monkeypatch.setattr("telegram.Bot.set_chat_administrator_custom_title", make_assertion) + assert await chat.set_administrator_custom_title(user_id=42, custom_title="custom_title") + + async def test_set_photo(self, monkeypatch, chat): + async def make_assertion(*_, **kwargs): + chat_id = kwargs["chat_id"] == chat.id + photo = kwargs["photo"] == "test_photo" + return chat_id, photo + + assert check_shortcut_signature(Chat.set_photo, Bot.set_chat_photo, ["chat_id"], []) + assert await check_shortcut_call(chat.set_photo, chat.get_bot(), "set_chat_photo") + assert await check_defaults_handling(chat.set_photo, chat.get_bot()) + + monkeypatch.setattr(chat.get_bot(), "set_chat_photo", make_assertion) + assert await chat.set_photo(photo="test_photo") + + async def test_delete_photo(self, monkeypatch, chat): + async def make_assertion(*_, **kwargs): + return kwargs["chat_id"] == chat.id + + assert check_shortcut_signature(Chat.delete_photo, Bot.delete_chat_photo, ["chat_id"], []) + assert await check_shortcut_call(chat.delete_photo, chat.get_bot(), "delete_chat_photo") + assert await check_defaults_handling(chat.delete_photo, chat.get_bot()) + + monkeypatch.setattr(chat.get_bot(), "delete_chat_photo", make_assertion) + assert await chat.delete_photo() + + async def test_set_title(self, monkeypatch, chat): + async def make_assertion(*_, **kwargs): + chat_id = kwargs["chat_id"] == chat.id + title = kwargs["title"] == "test_title" + return chat_id, title + + assert check_shortcut_signature(Chat.set_title, Bot.set_chat_title, ["chat_id"], []) + assert await check_shortcut_call(chat.set_title, chat.get_bot(), "set_chat_title") + assert await check_defaults_handling(chat.set_title, chat.get_bot()) + + monkeypatch.setattr(chat.get_bot(), "set_chat_title", make_assertion) + assert await chat.set_title(title="test_title") + + async def test_set_description(self, monkeypatch, chat): + async def make_assertion(*_, **kwargs): + chat_id = kwargs["chat_id"] == chat.id + description = kwargs["description"] == "test_descripton" + return chat_id, description + + assert check_shortcut_signature( + Chat.set_description, Bot.set_chat_description, ["chat_id"], [] + ) + assert await check_shortcut_call( + chat.set_description, chat.get_bot(), "set_chat_description" + ) + assert await check_defaults_handling(chat.set_description, chat.get_bot()) + + monkeypatch.setattr(chat.get_bot(), "set_chat_description", make_assertion) + assert await chat.set_description(description="test_description") + + async def test_pin_message(self, monkeypatch, chat): + async def make_assertion(*_, **kwargs): + return kwargs["chat_id"] == chat.id and kwargs["message_id"] == 42 + + assert check_shortcut_signature(Chat.pin_message, Bot.pin_chat_message, ["chat_id"], []) + assert await check_shortcut_call(chat.pin_message, chat.get_bot(), "pin_chat_message") + assert await check_defaults_handling(chat.pin_message, chat.get_bot()) + + monkeypatch.setattr(chat.get_bot(), "pin_chat_message", make_assertion) + assert await chat.pin_message(message_id=42) + + async def test_unpin_message(self, monkeypatch, chat): + async def make_assertion(*_, **kwargs): + return kwargs["chat_id"] == chat.id + + assert check_shortcut_signature( + Chat.unpin_message, Bot.unpin_chat_message, ["chat_id"], [] + ) + assert await check_shortcut_call(chat.unpin_message, chat.get_bot(), "unpin_chat_message") + assert await check_defaults_handling(chat.unpin_message, chat.get_bot()) + + monkeypatch.setattr(chat.get_bot(), "unpin_chat_message", make_assertion) + assert await chat.unpin_message() + + async def test_unpin_all_messages(self, monkeypatch, chat): + async def make_assertion(*_, **kwargs): + return kwargs["chat_id"] == chat.id + + assert check_shortcut_signature( + Chat.unpin_all_messages, Bot.unpin_all_chat_messages, ["chat_id"], [] + ) + assert await check_shortcut_call( + chat.unpin_all_messages, chat.get_bot(), "unpin_all_chat_messages" + ) + assert await check_defaults_handling(chat.unpin_all_messages, chat.get_bot()) + + monkeypatch.setattr(chat.get_bot(), "unpin_all_chat_messages", make_assertion) + assert await chat.unpin_all_messages() + + async def test_instance_method_send_message(self, monkeypatch, chat): + async def make_assertion(*_, **kwargs): + return kwargs["chat_id"] == chat.id and kwargs["text"] == "test" + + assert check_shortcut_signature(Chat.send_message, Bot.send_message, ["chat_id"], []) + assert await check_shortcut_call(chat.send_message, chat.get_bot(), "send_message") + assert await check_defaults_handling(chat.send_message, chat.get_bot()) + + monkeypatch.setattr(chat.get_bot(), "send_message", make_assertion) + assert await chat.send_message(text="test") + + async def test_instance_method_send_media_group(self, monkeypatch, chat): + async def make_assertion(*_, **kwargs): + return kwargs["chat_id"] == chat.id and kwargs["media"] == "test_media_group" + + assert check_shortcut_signature( + Chat.send_media_group, Bot.send_media_group, ["chat_id"], [] + ) + assert await check_shortcut_call(chat.send_media_group, chat.get_bot(), "send_media_group") + assert await check_defaults_handling(chat.send_media_group, chat.get_bot()) + + monkeypatch.setattr(chat.get_bot(), "send_media_group", make_assertion) + assert await chat.send_media_group(media="test_media_group") + + async def test_instance_method_send_photo(self, monkeypatch, chat): + async def make_assertion(*_, **kwargs): + return kwargs["chat_id"] == chat.id and kwargs["photo"] == "test_photo" + + assert check_shortcut_signature(Chat.send_photo, Bot.send_photo, ["chat_id"], []) + assert await check_shortcut_call(chat.send_photo, chat.get_bot(), "send_photo") + assert await check_defaults_handling(chat.send_photo, chat.get_bot()) + + monkeypatch.setattr(chat.get_bot(), "send_photo", make_assertion) + assert await chat.send_photo(photo="test_photo") + + async def test_instance_method_send_contact(self, monkeypatch, chat): + async def make_assertion(*_, **kwargs): + return kwargs["chat_id"] == chat.id and kwargs["phone_number"] == "test_contact" + + assert check_shortcut_signature(Chat.send_contact, Bot.send_contact, ["chat_id"], []) + assert await check_shortcut_call(chat.send_contact, chat.get_bot(), "send_contact") + assert await check_defaults_handling(chat.send_contact, chat.get_bot()) + + monkeypatch.setattr(chat.get_bot(), "send_contact", make_assertion) + assert await chat.send_contact(phone_number="test_contact") + + async def test_instance_method_send_audio(self, monkeypatch, chat): + async def make_assertion(*_, **kwargs): + return kwargs["chat_id"] == chat.id and kwargs["audio"] == "test_audio" + + assert check_shortcut_signature(Chat.send_audio, Bot.send_audio, ["chat_id"], []) + assert await check_shortcut_call(chat.send_audio, chat.get_bot(), "send_audio") + assert await check_defaults_handling(chat.send_audio, chat.get_bot()) + + monkeypatch.setattr(chat.get_bot(), "send_audio", make_assertion) + assert await chat.send_audio(audio="test_audio") + + async def test_instance_method_send_document(self, monkeypatch, chat): + async def make_assertion(*_, **kwargs): + return kwargs["chat_id"] == chat.id and kwargs["document"] == "test_document" + + assert check_shortcut_signature(Chat.send_document, Bot.send_document, ["chat_id"], []) + assert await check_shortcut_call(chat.send_document, chat.get_bot(), "send_document") + assert await check_defaults_handling(chat.send_document, chat.get_bot()) + + monkeypatch.setattr(chat.get_bot(), "send_document", make_assertion) + assert await chat.send_document(document="test_document") + + async def test_instance_method_send_dice(self, monkeypatch, chat): + async def make_assertion(*_, **kwargs): + return kwargs["chat_id"] == chat.id and kwargs["emoji"] == "test_dice" + + assert check_shortcut_signature(Chat.send_dice, Bot.send_dice, ["chat_id"], []) + assert await check_shortcut_call(chat.send_dice, chat.get_bot(), "send_dice") + assert await check_defaults_handling(chat.send_dice, chat.get_bot()) + + monkeypatch.setattr(chat.get_bot(), "send_dice", make_assertion) + assert await chat.send_dice(emoji="test_dice") + + async def test_instance_method_send_game(self, monkeypatch, chat): + async def make_assertion(*_, **kwargs): + return kwargs["chat_id"] == chat.id and kwargs["game_short_name"] == "test_game" + + assert check_shortcut_signature(Chat.send_game, Bot.send_game, ["chat_id"], []) + assert await check_shortcut_call(chat.send_game, chat.get_bot(), "send_game") + assert await check_defaults_handling(chat.send_game, chat.get_bot()) + + monkeypatch.setattr(chat.get_bot(), "send_game", make_assertion) + assert await chat.send_game(game_short_name="test_game") + + async def test_instance_method_send_invoice(self, monkeypatch, chat): + async def make_assertion(*_, **kwargs): + title = kwargs["title"] == "title" + description = kwargs["description"] == "description" + payload = kwargs["payload"] == "payload" + provider_token = kwargs["provider_token"] == "provider_token" + currency = kwargs["currency"] == "currency" + prices = kwargs["prices"] == "prices" + args = title and description and payload and provider_token and currency and prices + return kwargs["chat_id"] == chat.id and args + + assert check_shortcut_signature(Chat.send_invoice, Bot.send_invoice, ["chat_id"], []) + assert await check_shortcut_call(chat.send_invoice, chat.get_bot(), "send_invoice") + assert await check_defaults_handling(chat.send_invoice, chat.get_bot()) + + monkeypatch.setattr(chat.get_bot(), "send_invoice", make_assertion) + assert await chat.send_invoice( + "title", + "description", + "payload", + "provider_token", + "currency", + "prices", + ) + + async def test_instance_method_send_location(self, monkeypatch, chat): + async def make_assertion(*_, **kwargs): + return kwargs["chat_id"] == chat.id and kwargs["latitude"] == "test_location" + + assert check_shortcut_signature(Chat.send_location, Bot.send_location, ["chat_id"], []) + assert await check_shortcut_call(chat.send_location, chat.get_bot(), "send_location") + assert await check_defaults_handling(chat.send_location, chat.get_bot()) + + monkeypatch.setattr(chat.get_bot(), "send_location", make_assertion) + assert await chat.send_location(latitude="test_location") + + async def test_instance_method_send_sticker(self, monkeypatch, chat): + async def make_assertion(*_, **kwargs): + return kwargs["chat_id"] == chat.id and kwargs["sticker"] == "test_sticker" + + assert check_shortcut_signature(Chat.send_sticker, Bot.send_sticker, ["chat_id"], []) + assert await check_shortcut_call(chat.send_sticker, chat.get_bot(), "send_sticker") + assert await check_defaults_handling(chat.send_sticker, chat.get_bot()) + + monkeypatch.setattr(chat.get_bot(), "send_sticker", make_assertion) + assert await chat.send_sticker(sticker="test_sticker") + + async def test_instance_method_send_venue(self, monkeypatch, chat): + async def make_assertion(*_, **kwargs): + return kwargs["chat_id"] == chat.id and kwargs["title"] == "test_venue" + + assert check_shortcut_signature(Chat.send_venue, Bot.send_venue, ["chat_id"], []) + assert await check_shortcut_call(chat.send_venue, chat.get_bot(), "send_venue") + assert await check_defaults_handling(chat.send_venue, chat.get_bot()) + + monkeypatch.setattr(chat.get_bot(), "send_venue", make_assertion) + assert await chat.send_venue(title="test_venue") + + async def test_instance_method_send_video(self, monkeypatch, chat): + async def make_assertion(*_, **kwargs): + return kwargs["chat_id"] == chat.id and kwargs["video"] == "test_video" + + assert check_shortcut_signature(Chat.send_video, Bot.send_video, ["chat_id"], []) + assert await check_shortcut_call(chat.send_video, chat.get_bot(), "send_video") + assert await check_defaults_handling(chat.send_video, chat.get_bot()) + + monkeypatch.setattr(chat.get_bot(), "send_video", make_assertion) + assert await chat.send_video(video="test_video") + + async def test_instance_method_send_video_note(self, monkeypatch, chat): + async def make_assertion(*_, **kwargs): + return kwargs["chat_id"] == chat.id and kwargs["video_note"] == "test_video_note" + + assert check_shortcut_signature(Chat.send_video_note, Bot.send_video_note, ["chat_id"], []) + assert await check_shortcut_call(chat.send_video_note, chat.get_bot(), "send_video_note") + assert await check_defaults_handling(chat.send_video_note, chat.get_bot()) + + monkeypatch.setattr(chat.get_bot(), "send_video_note", make_assertion) + assert await chat.send_video_note(video_note="test_video_note") + + async def test_instance_method_send_voice(self, monkeypatch, chat): + async def make_assertion(*_, **kwargs): + return kwargs["chat_id"] == chat.id and kwargs["voice"] == "test_voice" + + assert check_shortcut_signature(Chat.send_voice, Bot.send_voice, ["chat_id"], []) + assert await check_shortcut_call(chat.send_voice, chat.get_bot(), "send_voice") + assert await check_defaults_handling(chat.send_voice, chat.get_bot()) + + monkeypatch.setattr(chat.get_bot(), "send_voice", make_assertion) + assert await chat.send_voice(voice="test_voice") + + async def test_instance_method_send_animation(self, monkeypatch, chat): + async def make_assertion(*_, **kwargs): + return kwargs["chat_id"] == chat.id and kwargs["animation"] == "test_animation" + + assert check_shortcut_signature(Chat.send_animation, Bot.send_animation, ["chat_id"], []) + assert await check_shortcut_call(chat.send_animation, chat.get_bot(), "send_animation") + assert await check_defaults_handling(chat.send_animation, chat.get_bot()) + + monkeypatch.setattr(chat.get_bot(), "send_animation", make_assertion) + assert await chat.send_animation(animation="test_animation") + + async def test_instance_method_send_poll(self, monkeypatch, chat): + async def make_assertion(*_, **kwargs): + return kwargs["chat_id"] == chat.id and kwargs["question"] == "test_poll" + + assert check_shortcut_signature(Chat.send_poll, Bot.send_poll, ["chat_id"], []) + assert await check_shortcut_call(chat.send_poll, chat.get_bot(), "send_poll") + assert await check_defaults_handling(chat.send_poll, chat.get_bot()) + + monkeypatch.setattr(chat.get_bot(), "send_poll", make_assertion) + assert await chat.send_poll(question="test_poll", options=[1, 2]) + + async def test_instance_method_send_copy(self, monkeypatch, chat): + async def make_assertion(*_, **kwargs): + from_chat_id = kwargs["from_chat_id"] == "test_copy" + message_id = kwargs["message_id"] == 42 + chat_id = kwargs["chat_id"] == chat.id + return from_chat_id and message_id and chat_id + + assert check_shortcut_signature(Chat.send_copy, Bot.copy_message, ["chat_id"], []) + assert await check_shortcut_call(chat.send_copy, chat.get_bot(), "copy_message") + assert await check_defaults_handling(chat.send_copy, chat.get_bot()) + + monkeypatch.setattr(chat.get_bot(), "copy_message", make_assertion) + assert await chat.send_copy(from_chat_id="test_copy", message_id=42) + + async def test_instance_method_copy_message(self, monkeypatch, chat): + async def make_assertion(*_, **kwargs): + from_chat_id = kwargs["from_chat_id"] == chat.id + message_id = kwargs["message_id"] == 42 + chat_id = kwargs["chat_id"] == "test_copy" + return from_chat_id and message_id and chat_id + + assert check_shortcut_signature(Chat.copy_message, Bot.copy_message, ["from_chat_id"], []) + assert await check_shortcut_call(chat.copy_message, chat.get_bot(), "copy_message") + assert await check_defaults_handling(chat.copy_message, chat.get_bot()) + + monkeypatch.setattr(chat.get_bot(), "copy_message", make_assertion) + assert await chat.copy_message(chat_id="test_copy", message_id=42) + + async def test_instance_method_send_copies(self, monkeypatch, chat): + async def make_assertion(*_, **kwargs): + from_chat_id = kwargs["from_chat_id"] == "test_copies" + message_ids = kwargs["message_ids"] == (42, 43) + chat_id = kwargs["chat_id"] == chat.id + return from_chat_id and message_ids and chat_id + + assert check_shortcut_signature(Chat.send_copies, Bot.copy_messages, ["chat_id"], []) + assert await check_shortcut_call(chat.send_copies, chat.get_bot(), "copy_messages") + assert await check_defaults_handling(chat.send_copies, chat.get_bot()) + + monkeypatch.setattr(chat.get_bot(), "copy_messages", make_assertion) + assert await chat.send_copies(from_chat_id="test_copies", message_ids=(42, 43)) + + async def test_instance_method_copy_messages(self, monkeypatch, chat): + async def make_assertion(*_, **kwargs): + from_chat_id = kwargs["from_chat_id"] == chat.id + message_ids = kwargs["message_ids"] == (42, 43) + chat_id = kwargs["chat_id"] == "test_copies" + return from_chat_id and message_ids and chat_id + + assert check_shortcut_signature( + Chat.copy_messages, Bot.copy_messages, ["from_chat_id"], [] + ) + assert await check_shortcut_call(chat.copy_messages, chat.get_bot(), "copy_messages") + assert await check_defaults_handling(chat.copy_messages, chat.get_bot()) + + monkeypatch.setattr(chat.get_bot(), "copy_messages", make_assertion) + assert await chat.copy_messages(chat_id="test_copies", message_ids=(42, 43)) + + async def test_instance_method_forward_from(self, monkeypatch, chat): + async def make_assertion(*_, **kwargs): + chat_id = kwargs["chat_id"] == chat.id + message_id = kwargs["message_id"] == 42 + from_chat_id = kwargs["from_chat_id"] == "test_forward" + return from_chat_id and message_id and chat_id + + assert check_shortcut_signature(Chat.forward_from, Bot.forward_message, ["chat_id"], []) + assert await check_shortcut_call(chat.forward_from, chat.get_bot(), "forward_message") + assert await check_defaults_handling(chat.forward_from, chat.get_bot()) + + monkeypatch.setattr(chat.get_bot(), "forward_message", make_assertion) + assert await chat.forward_from(from_chat_id="test_forward", message_id=42) + + async def test_instance_method_forward_to(self, monkeypatch, chat): + async def make_assertion(*_, **kwargs): + from_chat_id = kwargs["from_chat_id"] == chat.id + message_id = kwargs["message_id"] == 42 + chat_id = kwargs["chat_id"] == "test_forward" + return from_chat_id and message_id and chat_id + + assert check_shortcut_signature(Chat.forward_to, Bot.forward_message, ["from_chat_id"], []) + assert await check_shortcut_call(chat.forward_to, chat.get_bot(), "forward_message") + assert await check_defaults_handling(chat.forward_to, chat.get_bot()) + + monkeypatch.setattr(chat.get_bot(), "forward_message", make_assertion) + assert await chat.forward_to(chat_id="test_forward", message_id=42) + + async def test_instance_method_forward_messages_from(self, monkeypatch, chat): + async def make_assertion(*_, **kwargs): + chat_id = kwargs["chat_id"] == chat.id + message_ids = kwargs["message_ids"] == (42, 43) + from_chat_id = kwargs["from_chat_id"] == "test_forwards" + return from_chat_id and message_ids and chat_id + + assert check_shortcut_signature( + Chat.forward_messages_from, Bot.forward_messages, ["chat_id"], [] + ) + assert await check_shortcut_call( + chat.forward_messages_from, chat.get_bot(), "forward_messages" + ) + assert await check_defaults_handling(chat.forward_messages_from, chat.get_bot()) + + monkeypatch.setattr(chat.get_bot(), "forward_messages", make_assertion) + assert await chat.forward_messages_from(from_chat_id="test_forwards", message_ids=(42, 43)) + + async def test_instance_method_forward_messages_to(self, monkeypatch, chat): + async def make_assertion(*_, **kwargs): + from_chat_id = kwargs["from_chat_id"] == chat.id + message_ids = kwargs["message_ids"] == (42, 43) + chat_id = kwargs["chat_id"] == "test_forwards" + return from_chat_id and message_ids and chat_id + + assert check_shortcut_signature( + Chat.forward_messages_to, Bot.forward_messages, ["from_chat_id"], [] + ) + assert await check_shortcut_call( + chat.forward_messages_to, chat.get_bot(), "forward_messages" + ) + assert await check_defaults_handling(chat.forward_messages_to, chat.get_bot()) + + monkeypatch.setattr(chat.get_bot(), "forward_messages", make_assertion) + assert await chat.forward_messages_to(chat_id="test_forwards", message_ids=(42, 43)) + + async def test_export_invite_link(self, monkeypatch, chat): + async def make_assertion(*_, **kwargs): + return kwargs["chat_id"] == chat.id + + assert check_shortcut_signature( + Chat.export_invite_link, Bot.export_chat_invite_link, ["chat_id"], [] + ) + assert await check_shortcut_call( + chat.export_invite_link, chat.get_bot(), "export_chat_invite_link" + ) + assert await check_defaults_handling(chat.export_invite_link, chat.get_bot()) + + monkeypatch.setattr(chat.get_bot(), "export_chat_invite_link", make_assertion) + assert await chat.export_invite_link() + + async def test_create_invite_link(self, monkeypatch, chat): + async def make_assertion(*_, **kwargs): + return kwargs["chat_id"] == chat.id + + assert check_shortcut_signature( + Chat.create_invite_link, Bot.create_chat_invite_link, ["chat_id"], [] + ) + assert await check_shortcut_call( + chat.create_invite_link, chat.get_bot(), "create_chat_invite_link" + ) + assert await check_defaults_handling(chat.create_invite_link, chat.get_bot()) + + monkeypatch.setattr(chat.get_bot(), "create_chat_invite_link", make_assertion) + assert await chat.create_invite_link() + + async def test_edit_invite_link(self, monkeypatch, chat): + link = "ThisIsALink" + + async def make_assertion(*_, **kwargs): + return kwargs["chat_id"] == chat.id and kwargs["invite_link"] == link + + assert check_shortcut_signature( + Chat.edit_invite_link, Bot.edit_chat_invite_link, ["chat_id"], [] + ) + assert await check_shortcut_call( + chat.edit_invite_link, chat.get_bot(), "edit_chat_invite_link" + ) + assert await check_defaults_handling(chat.edit_invite_link, chat.get_bot()) + + monkeypatch.setattr(chat.get_bot(), "edit_chat_invite_link", make_assertion) + assert await chat.edit_invite_link(invite_link=link) + + async def test_revoke_invite_link(self, monkeypatch, chat): + link = "ThisIsALink" + + async def make_assertion(*_, **kwargs): + return kwargs["chat_id"] == chat.id and kwargs["invite_link"] == link + + assert check_shortcut_signature( + Chat.revoke_invite_link, Bot.revoke_chat_invite_link, ["chat_id"], [] + ) + assert await check_shortcut_call( + chat.revoke_invite_link, chat.get_bot(), "revoke_chat_invite_link" + ) + assert await check_defaults_handling(chat.revoke_invite_link, chat.get_bot()) + + monkeypatch.setattr(chat.get_bot(), "revoke_chat_invite_link", make_assertion) + assert await chat.revoke_invite_link(invite_link=link) + + async def test_create_subscription_invite_link(self, monkeypatch, chat): + async def make_assertion(*_, **kwargs): + return ( + kwargs["chat_id"] == chat.id + and kwargs["subscription_price"] == 42 + and kwargs["subscription_period"] == 42 + ) + + assert check_shortcut_signature( + Chat.create_subscription_invite_link, + Bot.create_chat_subscription_invite_link, + ["chat_id"], + [], + ) + assert await check_shortcut_call( + chat.create_subscription_invite_link, + chat.get_bot(), + "create_chat_subscription_invite_link", + ) + assert await check_defaults_handling(chat.create_subscription_invite_link, chat.get_bot()) + + monkeypatch.setattr(chat.get_bot(), "create_chat_subscription_invite_link", make_assertion) + assert await chat.create_subscription_invite_link( + subscription_price=42, subscription_period=42 + ) + + async def test_edit_subscription_invite_link(self, monkeypatch, chat): + link = "ThisIsALink" + + async def make_assertion(*_, **kwargs): + return kwargs["chat_id"] == chat.id and kwargs["invite_link"] == link + + assert check_shortcut_signature( + Chat.edit_subscription_invite_link, + Bot.edit_chat_subscription_invite_link, + ["chat_id"], + [], + ) + assert await check_shortcut_call( + chat.edit_subscription_invite_link, + chat.get_bot(), + "edit_chat_subscription_invite_link", + ) + assert await check_defaults_handling(chat.edit_subscription_invite_link, chat.get_bot()) + + monkeypatch.setattr(chat.get_bot(), "edit_chat_subscription_invite_link", make_assertion) + assert await chat.edit_subscription_invite_link(invite_link=link) + + async def test_instance_method_get_menu_button(self, monkeypatch, chat): + async def make_assertion(*_, **kwargs): + return kwargs["chat_id"] == chat.id + + assert check_shortcut_signature( + Chat.get_menu_button, Bot.get_chat_menu_button, ["chat_id"], [] + ) + assert await check_shortcut_call( + chat.get_menu_button, + chat.get_bot(), + "get_chat_menu_button", + shortcut_kwargs=["chat_id"], + ) + assert await check_defaults_handling(chat.get_menu_button, chat.get_bot()) + + monkeypatch.setattr(chat.get_bot(), "get_chat_menu_button", make_assertion) + assert await chat.get_menu_button() + + async def test_instance_method_set_menu_button(self, monkeypatch, chat): + async def make_assertion(*_, **kwargs): + return kwargs["chat_id"] == chat.id and kwargs["menu_button"] == "menu_button" + + assert check_shortcut_signature( + Chat.set_menu_button, Bot.set_chat_menu_button, ["chat_id"], [] + ) + assert await check_shortcut_call( + chat.set_menu_button, + chat.get_bot(), + "set_chat_menu_button", + shortcut_kwargs=["chat_id"], + ) + assert await check_defaults_handling(chat.set_menu_button, chat.get_bot()) + + monkeypatch.setattr(chat.get_bot(), "set_chat_menu_button", make_assertion) + assert await chat.set_menu_button(menu_button="menu_button") + + async def test_approve_join_request(self, monkeypatch, chat): + async def make_assertion(*_, **kwargs): + return kwargs["chat_id"] == chat.id and kwargs["user_id"] == 42 + + assert check_shortcut_signature( + Chat.approve_join_request, Bot.approve_chat_join_request, ["chat_id"], [] + ) + assert await check_shortcut_call( + chat.approve_join_request, chat.get_bot(), "approve_chat_join_request" + ) + assert await check_defaults_handling(chat.approve_join_request, chat.get_bot()) + + monkeypatch.setattr(chat.get_bot(), "approve_chat_join_request", make_assertion) + assert await chat.approve_join_request(user_id=42) + + async def test_decline_join_request(self, monkeypatch, chat): + async def make_assertion(*_, **kwargs): + return kwargs["chat_id"] == chat.id and kwargs["user_id"] == 42 + + assert check_shortcut_signature( + Chat.decline_join_request, Bot.decline_chat_join_request, ["chat_id"], [] + ) + assert await check_shortcut_call( + chat.decline_join_request, chat.get_bot(), "decline_chat_join_request" + ) + assert await check_defaults_handling(chat.decline_join_request, chat.get_bot()) + + monkeypatch.setattr(chat.get_bot(), "decline_chat_join_request", make_assertion) + assert await chat.decline_join_request(user_id=42) + + async def test_create_forum_topic(self, monkeypatch, chat): + async def make_assertion(*_, **kwargs): + return ( + kwargs["chat_id"] == chat.id + and kwargs["name"] == "New Name" + and kwargs["icon_color"] == 0x6FB9F0 + and kwargs["icon_custom_emoji_id"] == "12345" + ) + + assert check_shortcut_signature( + Chat.create_forum_topic, Bot.create_forum_topic, ["chat_id"], [] + ) + assert await check_shortcut_call( + chat.create_forum_topic, + chat.get_bot(), + "create_forum_topic", + shortcut_kwargs=["chat_id"], + ) + assert await check_defaults_handling(chat.create_forum_topic, chat.get_bot()) + + monkeypatch.setattr(chat.get_bot(), "create_forum_topic", make_assertion) + assert await chat.create_forum_topic( + name="New Name", icon_color=0x6FB9F0, icon_custom_emoji_id="12345" + ) + + async def test_edit_forum_topic(self, monkeypatch, chat): + async def make_assertion(*_, **kwargs): + return ( + kwargs["chat_id"] == chat.id + and kwargs["message_thread_id"] == 42 + and kwargs["name"] == "New Name" + and kwargs["icon_custom_emoji_id"] == "12345" + ) + + assert check_shortcut_signature( + Chat.edit_forum_topic, Bot.edit_forum_topic, ["chat_id"], [] + ) + assert await check_shortcut_call( + chat.edit_forum_topic, chat.get_bot(), "edit_forum_topic", shortcut_kwargs=["chat_id"] + ) + assert await check_defaults_handling(chat.edit_forum_topic, chat.get_bot()) + + monkeypatch.setattr(chat.get_bot(), "edit_forum_topic", make_assertion) + assert await chat.edit_forum_topic( + message_thread_id=42, name="New Name", icon_custom_emoji_id="12345" + ) + + async def test_close_forum_topic(self, monkeypatch, chat): + async def make_assertion(*_, **kwargs): + return kwargs["chat_id"] == chat.id and kwargs["message_thread_id"] == 42 + + assert check_shortcut_signature( + Chat.close_forum_topic, Bot.close_forum_topic, ["chat_id"], [] + ) + assert await check_shortcut_call( + chat.close_forum_topic, + chat.get_bot(), + "close_forum_topic", + shortcut_kwargs=["chat_id"], + ) + assert await check_defaults_handling(chat.close_forum_topic, chat.get_bot()) + + monkeypatch.setattr(chat.get_bot(), "close_forum_topic", make_assertion) + assert await chat.close_forum_topic(message_thread_id=42) + + async def test_reopen_forum_topic(self, monkeypatch, chat): + async def make_assertion(*_, **kwargs): + return kwargs["chat_id"] == chat.id and kwargs["message_thread_id"] == 42 + + assert check_shortcut_signature( + Chat.reopen_forum_topic, Bot.reopen_forum_topic, ["chat_id"], [] + ) + assert await check_shortcut_call( + chat.reopen_forum_topic, + chat.get_bot(), + "reopen_forum_topic", + shortcut_kwargs=["chat_id"], + ) + assert await check_defaults_handling(chat.reopen_forum_topic, chat.get_bot()) + + monkeypatch.setattr(chat.get_bot(), "reopen_forum_topic", make_assertion) + assert await chat.reopen_forum_topic(message_thread_id=42) + + async def test_delete_forum_topic(self, monkeypatch, chat): + async def make_assertion(*_, **kwargs): + return kwargs["chat_id"] == chat.id and kwargs["message_thread_id"] == 42 + + assert check_shortcut_signature( + Chat.delete_forum_topic, Bot.delete_forum_topic, ["chat_id"], [] + ) + assert await check_shortcut_call( + chat.delete_forum_topic, + chat.get_bot(), + "delete_forum_topic", + shortcut_kwargs=["chat_id"], + ) + assert await check_defaults_handling(chat.delete_forum_topic, chat.get_bot()) + + monkeypatch.setattr(chat.get_bot(), "delete_forum_topic", make_assertion) + assert await chat.delete_forum_topic(message_thread_id=42) + + async def test_unpin_all_forum_topic_messages(self, monkeypatch, chat): + async def make_assertion(*_, **kwargs): + return kwargs["chat_id"] == chat.id and kwargs["message_thread_id"] == 42 + + assert check_shortcut_signature( + Chat.unpin_all_forum_topic_messages, + Bot.unpin_all_forum_topic_messages, + ["chat_id"], + [], + ) + assert await check_shortcut_call( + chat.unpin_all_forum_topic_messages, + chat.get_bot(), + "unpin_all_forum_topic_messages", + shortcut_kwargs=["chat_id"], + ) + assert await check_defaults_handling(chat.unpin_all_forum_topic_messages, chat.get_bot()) + + monkeypatch.setattr(chat.get_bot(), "unpin_all_forum_topic_messages", make_assertion) + assert await chat.unpin_all_forum_topic_messages(message_thread_id=42) + + async def test_unpin_all_general_forum_topic_messages(self, monkeypatch, chat): + async def make_assertion(*_, **kwargs): + return kwargs["chat_id"] == chat.id + + assert check_shortcut_signature( + Chat.unpin_all_general_forum_topic_messages, + Bot.unpin_all_general_forum_topic_messages, + ["chat_id"], + [], + ) + assert await check_shortcut_call( + chat.unpin_all_general_forum_topic_messages, + chat.get_bot(), + "unpin_all_general_forum_topic_messages", + shortcut_kwargs=["chat_id"], + ) + assert await check_defaults_handling( + chat.unpin_all_general_forum_topic_messages, chat.get_bot() + ) + + monkeypatch.setattr( + chat.get_bot(), "unpin_all_general_forum_topic_messages", make_assertion + ) + assert await chat.unpin_all_general_forum_topic_messages() + + async def test_edit_general_forum_topic(self, monkeypatch, chat): + async def make_assertion(*_, **kwargs): + return kwargs["chat_id"] == chat.id and kwargs["name"] == "WhatAName" + + assert check_shortcut_signature( + Chat.edit_general_forum_topic, + Bot.edit_general_forum_topic, + ["chat_id"], + [], + ) + assert await check_shortcut_call( + chat.edit_general_forum_topic, + chat.get_bot(), + "edit_general_forum_topic", + shortcut_kwargs=["chat_id"], + ) + assert await check_defaults_handling(chat.edit_general_forum_topic, chat.get_bot()) + + monkeypatch.setattr(chat.get_bot(), "edit_general_forum_topic", make_assertion) + assert await chat.edit_general_forum_topic(name="WhatAName") + + async def test_close_general_forum_topic(self, monkeypatch, chat): + async def make_assertion(*_, **kwargs): + return kwargs["chat_id"] == chat.id + + assert check_shortcut_signature( + Chat.close_general_forum_topic, + Bot.close_general_forum_topic, + ["chat_id"], + [], + ) + assert await check_shortcut_call( + chat.close_general_forum_topic, + chat.get_bot(), + "close_general_forum_topic", + shortcut_kwargs=["chat_id"], + ) + assert await check_defaults_handling(chat.close_general_forum_topic, chat.get_bot()) + + monkeypatch.setattr(chat.get_bot(), "close_general_forum_topic", make_assertion) + assert await chat.close_general_forum_topic() + + async def test_reopen_general_forum_topic(self, monkeypatch, chat): + async def make_assertion(*_, **kwargs): + return kwargs["chat_id"] == chat.id + + assert check_shortcut_signature( + Chat.reopen_general_forum_topic, + Bot.reopen_general_forum_topic, + ["chat_id"], + [], + ) + assert await check_shortcut_call( + chat.reopen_general_forum_topic, + chat.get_bot(), + "reopen_general_forum_topic", + shortcut_kwargs=["chat_id"], + ) + assert await check_defaults_handling(chat.reopen_general_forum_topic, chat.get_bot()) + + monkeypatch.setattr(chat.get_bot(), "reopen_general_forum_topic", make_assertion) + assert await chat.reopen_general_forum_topic() + + async def test_hide_general_forum_topic(self, monkeypatch, chat): + async def make_assertion(*_, **kwargs): + return kwargs["chat_id"] == chat.id + + assert check_shortcut_signature( + Chat.hide_general_forum_topic, + Bot.hide_general_forum_topic, + ["chat_id"], + [], + ) + assert await check_shortcut_call( + chat.hide_general_forum_topic, + chat.get_bot(), + "hide_general_forum_topic", + shortcut_kwargs=["chat_id"], + ) + assert await check_defaults_handling(chat.hide_general_forum_topic, chat.get_bot()) + + monkeypatch.setattr(chat.get_bot(), "hide_general_forum_topic", make_assertion) + assert await chat.hide_general_forum_topic() + + async def test_unhide_general_forum_topic(self, monkeypatch, chat): + async def make_assertion(*_, **kwargs): + return kwargs["chat_id"] == chat.id + + assert check_shortcut_signature( + Chat.unhide_general_forum_topic, + Bot.unhide_general_forum_topic, + ["chat_id"], + [], + ) + assert await check_shortcut_call( + chat.unhide_general_forum_topic, + chat.get_bot(), + "unhide_general_forum_topic", + shortcut_kwargs=["chat_id"], + ) + assert await check_defaults_handling(chat.unhide_general_forum_topic, chat.get_bot()) + + monkeypatch.setattr(chat.get_bot(), "unhide_general_forum_topic", make_assertion) + assert await chat.unhide_general_forum_topic() + + async def test_instance_method_get_user_chat_boosts(self, monkeypatch, chat): + async def make_assertion(*_, **kwargs): + user_id = kwargs["user_id"] == "user_id" + chat_id = kwargs["chat_id"] == chat.id + return chat_id and user_id + + assert check_shortcut_signature( + Chat.get_user_chat_boosts, Bot.get_user_chat_boosts, ["chat_id"], [] + ) + assert await check_shortcut_call( + chat.get_user_chat_boosts, chat.get_bot(), "get_user_chat_boosts" + ) + assert await check_defaults_handling(chat.get_user_chat_boosts, chat.get_bot()) + + monkeypatch.setattr(chat.get_bot(), "get_user_chat_boosts", make_assertion) + assert await chat.get_user_chat_boosts(user_id="user_id") + + async def test_instance_method_set_message_reaction(self, monkeypatch, chat): + async def make_assertion(*_, **kwargs): + message_id = kwargs["message_id"] == 123 + chat_id = kwargs["chat_id"] == chat.id + reaction = kwargs["reaction"] == [ReactionTypeEmoji(ReactionEmoji.THUMBS_DOWN)] + return chat_id and message_id and reaction and kwargs["is_big"] + + assert check_shortcut_signature( + Chat.set_message_reaction, Bot.set_message_reaction, ["chat_id"], [] + ) + assert await check_shortcut_call( + chat.set_message_reaction, chat.get_bot(), "set_message_reaction" + ) + assert await check_defaults_handling(chat.set_message_reaction, chat.get_bot()) + + monkeypatch.setattr(chat.get_bot(), "set_message_reaction", make_assertion) + assert await chat.set_message_reaction( + 123, [ReactionTypeEmoji(ReactionEmoji.THUMBS_DOWN)], True + ) + + async def test_instance_method_send_paid_media(self, monkeypatch, chat): + async def make_assertion(*_, **kwargs): + return ( + kwargs["chat_id"] == chat.id + and kwargs["media"] == "media" + and kwargs["star_count"] == 42 + and kwargs["caption"] == "stars" + ) + + assert check_shortcut_signature(Chat.send_paid_media, Bot.send_paid_media, ["chat_id"], []) + assert await check_shortcut_call(chat.send_paid_media, chat.get_bot(), "send_paid_media") + assert await check_defaults_handling(chat.send_paid_media, chat.get_bot()) + + monkeypatch.setattr(chat.get_bot(), "send_paid_media", make_assertion) + assert await chat.send_paid_media(media="media", star_count=42, caption="stars") + + def test_mention_html(self): + chat = Chat(id=1, type="foo") + with pytest.raises(TypeError, match="Can not create a mention to a private group chat"): + chat.mention_html() + + expected = '{}' + chat = Chat( + id=1, type=Chat.PRIVATE, first_name="first\u2022name", last_name="last\u2022name" + ) + assert chat.mention_html("the_name*\u2022") == expected.format(chat.id, "the_name*\u2022") + assert chat.mention_html() == expected.format(chat.id, chat.full_name) + chat = Chat(id=1, type=Chat.PRIVATE, last_name="last\u2022name") + with pytest.raises( + TypeError, match="Can not create a mention to a private chat without first name" + ): + chat.mention_html() + + expected = '{}' + chat = Chat(id=1, type="foo", username="user\u2022name", title="\u2022title") + assert chat.mention_html("the_name*\u2022") == expected.format( + chat.username, "the_name*\u2022" + ) + assert chat.mention_html() == expected.format(chat.username, chat.title) + chat = Chat(id=1, type="foo", username="user\u2022name") + with pytest.raises( + TypeError, match="Can not create a mention to a public chat without title" + ): + chat.mention_html() + + def test_mention_markdown(self): + chat = Chat(id=1, type="foo") + with pytest.raises(TypeError, match="Can not create a mention to a private group chat"): + chat.mention_markdown() + + expected = "[{}](tg://user?id={})" + chat = Chat( + id=1, type=Chat.PRIVATE, first_name="first\u2022name", last_name="last\u2022name" + ) + assert chat.mention_markdown("the_name*\u2022") == expected.format( + "the_name*\u2022", chat.id + ) + assert chat.mention_markdown() == expected.format(chat.full_name, chat.id) + chat = Chat(id=1, type=Chat.PRIVATE, last_name="last\u2022name") + with pytest.raises( + TypeError, match="Can not create a mention to a private chat without first name" + ): + chat.mention_markdown() + + expected = "[{}](https://t.me/{})" + chat = Chat(id=1, type="foo", username="user\u2022name", title="\u2022title") + assert chat.mention_markdown("the_name*\u2022") == expected.format( + "the_name*\u2022", chat.username + ) + assert chat.mention_markdown() == expected.format(chat.title, chat.username) + chat = Chat(id=1, type="foo", username="user\u2022name") + with pytest.raises( + TypeError, match="Can not create a mention to a public chat without title" + ): + chat.mention_markdown() + + def test_mention_markdown_v2(self): + chat = Chat(id=1, type="foo") + with pytest.raises(TypeError, match="Can not create a mention to a private group chat"): + chat.mention_markdown_v2() + + expected = "[{}](tg://user?id={})" + chat = Chat(id=1, type=Chat.PRIVATE, first_name="first{name", last_name="last_name") + assert chat.mention_markdown_v2("the{name>\u2022") == expected.format( + "the\\{name\\>\u2022", chat.id + ) + assert chat.mention_markdown_v2() == expected.format( + escape_markdown(chat.full_name, version=2), chat.id + ) + chat = Chat(id=1, type=Chat.PRIVATE, last_name="last_name") + with pytest.raises( + TypeError, match="Can not create a mention to a private chat without first name" + ): + chat.mention_markdown_v2() + + expected = "[{}](https://t.me/{})" + chat = Chat(id=1, type="foo", username="user{name", title="{title") + assert chat.mention_markdown_v2("the{name>\u2022") == expected.format( + "the\\{name\\>\u2022", chat.username + ) + assert chat.mention_markdown_v2() == expected.format( + escape_markdown(chat.title, version=2), chat.username + ) + chat = Chat(id=1, type="foo", username="user\u2022name") + with pytest.raises( + TypeError, match="Can not create a mention to a public chat without title" + ): + chat.mention_markdown_v2() diff --git a/test_chatadministratorrights.py b/test_chatadministratorrights.py new file mode 100644 index 0000000000000000000000000000000000000000..e630693c2d77d4eaca0c6748d65229f30473fa82 --- /dev/null +++ b/test_chatadministratorrights.py @@ -0,0 +1,180 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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/]. +import pytest + +from telegram import ChatAdministratorRights +from tests.auxil.slots import mro_slots + + +@pytest.fixture(scope="module") +def chat_admin_rights(): + return ChatAdministratorRights( + can_change_info=True, + can_delete_messages=True, + can_invite_users=True, + can_pin_messages=True, + can_promote_members=True, + can_restrict_members=True, + can_post_messages=True, + can_edit_messages=True, + can_manage_chat=True, + can_manage_video_chats=True, + can_manage_topics=True, + is_anonymous=True, + can_post_stories=True, + can_edit_stories=True, + can_delete_stories=True, + ) + + +class TestChatAdministratorRightsWithoutRequest: + def test_slot_behaviour(self, chat_admin_rights): + inst = chat_admin_rights + for attr in inst.__slots__: + assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" + + def test_de_json(self, bot, chat_admin_rights): + json_dict = { + "can_change_info": True, + "can_delete_messages": True, + "can_invite_users": True, + "can_pin_messages": True, + "can_promote_members": True, + "can_restrict_members": True, + "can_post_messages": True, + "can_edit_messages": True, + "can_manage_chat": True, + "can_manage_video_chats": True, + "can_manage_topics": True, + "is_anonymous": True, + "can_post_stories": True, + "can_edit_stories": True, + "can_delete_stories": True, + } + chat_administrator_rights_de = ChatAdministratorRights.de_json(json_dict, bot) + assert chat_administrator_rights_de.api_kwargs == {} + + assert chat_admin_rights == chat_administrator_rights_de + + def test_to_dict(self, chat_admin_rights): + car = chat_admin_rights + admin_rights_dict = car.to_dict() + + assert isinstance(admin_rights_dict, dict) + assert admin_rights_dict["can_change_info"] == car.can_change_info + assert admin_rights_dict["can_delete_messages"] == car.can_delete_messages + assert admin_rights_dict["can_invite_users"] == car.can_invite_users + assert admin_rights_dict["can_pin_messages"] == car.can_pin_messages + assert admin_rights_dict["can_promote_members"] == car.can_promote_members + assert admin_rights_dict["can_restrict_members"] == car.can_restrict_members + assert admin_rights_dict["can_post_messages"] == car.can_post_messages + assert admin_rights_dict["can_edit_messages"] == car.can_edit_messages + assert admin_rights_dict["can_manage_chat"] == car.can_manage_chat + assert admin_rights_dict["is_anonymous"] == car.is_anonymous + assert admin_rights_dict["can_manage_video_chats"] == car.can_manage_video_chats + assert admin_rights_dict["can_manage_topics"] == car.can_manage_topics + assert admin_rights_dict["can_post_stories"] == car.can_post_stories + assert admin_rights_dict["can_edit_stories"] == car.can_edit_stories + assert admin_rights_dict["can_delete_stories"] == car.can_delete_stories + + def test_equality(self): + a = ChatAdministratorRights( + True, + *((False,) * 11), + ) + b = ChatAdministratorRights( + True, + *((False,) * 11), + ) + c = ChatAdministratorRights( + *(False,) * 12, + ) + d = ChatAdministratorRights( + True, + True, + *((False,) * 10), + ) + e = ChatAdministratorRights( + True, + True, + *((False,) * 10), + ) + + assert a == b + assert hash(a) == hash(b) + assert a is not b + + assert a != c + assert hash(a) != hash(c) + + assert a != d + assert hash(a) != hash(d) + + assert d == e + assert hash(d) == hash(e) + + def test_all_rights(self): + f = ChatAdministratorRights( + True, + True, + True, + True, + True, + True, + True, + True, + True, + True, + True, + ) + t = ChatAdministratorRights.all_rights() + # if the dirs are the same, the attributes will all be there + assert dir(f) == dir(t) + # now we just need to check that all attributes are True. __slots__ returns all values, + # if a new one is added without defaulting to True, this will fail + for key in t.__slots__: + assert t[key] is True + # and as a finisher, make sure the default is different. + assert f != t + + def test_no_rights(self): + f = ChatAdministratorRights( + False, + False, + False, + False, + False, + False, + False, + False, + False, + False, + False, + False, + ) + t = ChatAdministratorRights.no_rights() + # if the dirs are the same, the attributes will all be there + assert dir(f) == dir(t) + # now we just need to check that all attributes are True. __slots__ returns all values, + # if a new one is added without defaulting to True, this will fail + for key in t.__slots__: + assert t[key] is False + # and as a finisher, make sure the default is different. + assert f != t diff --git a/test_chatbackground.py b/test_chatbackground.py new file mode 100644 index 0000000000000000000000000000000000000000..900fc58709f4ec3afcc2e246d1f397198889e46f --- /dev/null +++ b/test_chatbackground.py @@ -0,0 +1,361 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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/]. +import inspect +from copy import deepcopy +from typing import Union + +import pytest + +from telegram import ( + BackgroundFill, + BackgroundFillFreeformGradient, + BackgroundFillGradient, + BackgroundFillSolid, + BackgroundType, + BackgroundTypeChatTheme, + BackgroundTypeFill, + BackgroundTypePattern, + BackgroundTypeWallpaper, + Dice, + Document, +) +from tests.auxil.slots import mro_slots + +ignored = ["self", "api_kwargs"] + + +class BFDefaults: + color = 0 + top_color = 1 + bottom_color = 2 + rotation_angle = 45 + colors = [0, 1, 2] + + +def background_fill_solid(): + return BackgroundFillSolid(BFDefaults.color) + + +def background_fill_gradient(): + return BackgroundFillGradient( + BFDefaults.top_color, BFDefaults.bottom_color, BFDefaults.rotation_angle + ) + + +def background_fill_freeform_gradient(): + return BackgroundFillFreeformGradient(BFDefaults.colors) + + +class BTDefaults: + document = Document(1, 2) + fill = BackgroundFillSolid(color=0) + dark_theme_dimming = 20 + is_blurred = True + is_moving = False + intensity = 90 + is_inverted = False + theme_name = "ice" + + +def background_type_fill(): + return BackgroundTypeFill(BTDefaults.fill, BTDefaults.dark_theme_dimming) + + +def background_type_wallpaper(): + return BackgroundTypeWallpaper( + BTDefaults.document, + BTDefaults.dark_theme_dimming, + BTDefaults.is_blurred, + BTDefaults.is_moving, + ) + + +def background_type_pattern(): + return BackgroundTypePattern( + BTDefaults.document, + BTDefaults.fill, + BTDefaults.intensity, + BTDefaults.is_inverted, + BTDefaults.is_moving, + ) + + +def background_type_chat_theme(): + return BackgroundTypeChatTheme(BTDefaults.theme_name) + + +def make_json_dict( + instance: Union[BackgroundType, BackgroundFill], include_optional_args: bool = False +) -> dict: + """Used to make the json dict which we use for testing de_json. Similar to iter_args()""" + json_dict = {"type": instance.type} + sig = inspect.signature(instance.__class__.__init__) + + for param in sig.parameters.values(): + if param.name in ignored: # ignore irrelevant params + continue + + val = getattr(instance, param.name) + # Compulsory args- + if param.default is inspect.Parameter.empty: + if hasattr(val, "to_dict"): # convert the user object or any future ones to dict. + val = val.to_dict() + json_dict[param.name] = val + + # If we want to test all args (for de_json)- + elif param.default is not inspect.Parameter.empty and include_optional_args: + json_dict[param.name] = val + return json_dict + + +def iter_args( + instance: Union[BackgroundType, BackgroundFill], + de_json_inst: Union[BackgroundType, BackgroundFill], + include_optional: bool = False, +): + """ + We accept both the regular instance and de_json created instance and iterate over them for + easy one line testing later one. + """ + yield instance.type, de_json_inst.type # yield this here cause it's not available in sig. + + sig = inspect.signature(instance.__class__.__init__) + for param in sig.parameters.values(): + if param.name in ignored: + continue + inst_at, json_at = getattr(instance, param.name), getattr(de_json_inst, param.name) + if ( + param.default is not inspect.Parameter.empty and include_optional + ) or param.default is inspect.Parameter.empty: + yield inst_at, json_at + + +@pytest.fixture +def background_type(request): + return request.param() + + +@pytest.mark.parametrize( + "background_type", + [ + background_type_fill, + background_type_wallpaper, + background_type_pattern, + background_type_chat_theme, + ], + indirect=True, +) +class TestBackgroundTypeWithoutRequest: + def test_slot_behaviour(self, background_type): + inst = background_type + for attr in inst.__slots__: + assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" + + def test_de_json_required_args(self, bot, background_type): + cls = background_type.__class__ + assert cls.de_json({}, bot) is None + + json_dict = make_json_dict(background_type) + const_background_type = BackgroundType.de_json(json_dict, bot) + assert const_background_type.api_kwargs == {} + + assert isinstance(const_background_type, BackgroundType) + assert isinstance(const_background_type, cls) + for bg_type_at, const_bg_type_at in iter_args(background_type, const_background_type): + assert bg_type_at == const_bg_type_at + + def test_de_json_all_args(self, bot, background_type): + json_dict = make_json_dict(background_type, include_optional_args=True) + const_background_type = BackgroundType.de_json(json_dict, bot) + + assert const_background_type.api_kwargs == {} + + assert isinstance(const_background_type, BackgroundType) + assert isinstance(const_background_type, background_type.__class__) + for bg_type_at, const_bg_type_at in iter_args( + background_type, const_background_type, True + ): + assert bg_type_at == const_bg_type_at + + def test_de_json_invalid_type(self, background_type, bot): + json_dict = {"type": "invalid", "theme_name": BTDefaults.theme_name} + background_type = BackgroundType.de_json(json_dict, bot) + + assert type(background_type) is BackgroundType + assert background_type.type == "invalid" + + def test_de_json_subclass(self, background_type, bot, chat_id): + """This makes sure that e.g. BackgroundTypeFill(data, bot) never returns a + BackgroundTypeWallpaper instance.""" + cls = background_type.__class__ + json_dict = make_json_dict(background_type, True) + assert type(cls.de_json(json_dict, bot)) is cls + + def test_to_dict(self, background_type): + bg_type_dict = background_type.to_dict() + + assert isinstance(bg_type_dict, dict) + assert bg_type_dict["type"] == background_type.type + + for slot in background_type.__slots__: # additional verification for the optional args + if slot in ("fill", "document"): + assert (getattr(background_type, slot)).to_dict() == bg_type_dict[slot] + continue + assert getattr(background_type, slot) == bg_type_dict[slot] + + def test_equality(self, background_type): + a = BackgroundType(type="type") + b = BackgroundType(type="type") + c = background_type + d = deepcopy(background_type) + e = Dice(4, "emoji") + sig = inspect.signature(background_type.__class__.__init__) + params = [ + "random" for param in sig.parameters.values() if param.name not in [*ignored, "type"] + ] + f = background_type.__class__(*params) + + assert a == b + assert hash(a) == hash(b) + + assert a != c + assert hash(a) != hash(c) + + assert a != d + assert hash(a) != hash(d) + + assert a != e + assert hash(a) != hash(e) + + assert c == d + assert hash(c) == hash(d) + + assert c != e + assert hash(c) != hash(e) + + assert f != c + assert hash(f) != hash(c) + + +@pytest.fixture +def background_fill(request): + return request.param() + + +@pytest.mark.parametrize( + "background_fill", + [ + background_fill_solid, + background_fill_gradient, + background_fill_freeform_gradient, + ], + indirect=True, +) +class TestBackgroundFillWithoutRequest: + def test_slot_behaviour(self, background_fill): + inst = background_fill + for attr in inst.__slots__: + assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" + + def test_de_json_required_args(self, bot, background_fill): + cls = background_fill.__class__ + assert cls.de_json({}, bot) is None + + json_dict = make_json_dict(background_fill) + const_background_fill = BackgroundFill.de_json(json_dict, bot) + assert const_background_fill.api_kwargs == {} + + assert isinstance(const_background_fill, BackgroundFill) + assert isinstance(const_background_fill, cls) + for bg_fill_at, const_bg_fill_at in iter_args(background_fill, const_background_fill): + assert bg_fill_at == const_bg_fill_at + + def test_de_json_all_args(self, bot, background_fill): + json_dict = make_json_dict(background_fill, include_optional_args=True) + const_background_fill = BackgroundFill.de_json(json_dict, bot) + + assert const_background_fill.api_kwargs == {} + + assert isinstance(const_background_fill, BackgroundFill) + assert isinstance(const_background_fill, background_fill.__class__) + for bg_fill_at, const_bg_fill_at in iter_args( + background_fill, const_background_fill, True + ): + assert bg_fill_at == const_bg_fill_at + + def test_de_json_invalid_type(self, background_fill, bot): + json_dict = {"type": "invalid", "theme_name": BTDefaults.theme_name} + background_fill = BackgroundFill.de_json(json_dict, bot) + + assert type(background_fill) is BackgroundFill + assert background_fill.type == "invalid" + + def test_de_json_subclass(self, background_fill, bot): + """This makes sure that e.g. BackgroundFillSolid(data, bot) never returns a + BackgroundFillGradient instance.""" + cls = background_fill.__class__ + json_dict = make_json_dict(background_fill, True) + assert type(cls.de_json(json_dict, bot)) is cls + + def test_to_dict(self, background_fill): + bg_fill_dict = background_fill.to_dict() + + assert isinstance(bg_fill_dict, dict) + assert bg_fill_dict["type"] == background_fill.type + + for slot in background_fill.__slots__: # additional verification for the optional args + if slot == "colors": + assert getattr(background_fill, slot) == tuple(bg_fill_dict[slot]) + continue + assert getattr(background_fill, slot) == bg_fill_dict[slot] + + def test_equality(self, background_fill): + a = BackgroundFill(type="type") + b = BackgroundFill(type="type") + c = background_fill + d = deepcopy(background_fill) + e = Dice(4, "emoji") + sig = inspect.signature(background_fill.__class__.__init__) + params = [ + "random" for param in sig.parameters.values() if param.name not in [*ignored, "type"] + ] + f = background_fill.__class__(*params) + + assert a == b + assert hash(a) == hash(b) + + assert a != c + assert hash(a) != hash(c) + + assert a != d + assert hash(a) != hash(d) + + assert a != e + assert hash(a) != hash(e) + + assert c == d + assert hash(c) == hash(d) + + assert c != e + assert hash(c) != hash(e) + + assert f != c + assert hash(f) != hash(c) diff --git a/test_chatboost.py b/test_chatboost.py new file mode 100644 index 0000000000000000000000000000000000000000..f0ef143618ac1ea88211f8c5f4bd42bdcf8e4658 --- /dev/null +++ b/test_chatboost.py @@ -0,0 +1,584 @@ +# python-telegram-bot - a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# by the python-telegram-bot contributors +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser Public License for more details. +# +# You should have received a copy of the GNU Lesser Public License +# along with this program. If not, see [http://www.gnu.org/licenses/]. + +import datetime +import inspect +from copy import deepcopy + +import pytest + +from telegram import ( + Chat, + ChatBoost, + ChatBoostAdded, + ChatBoostRemoved, + ChatBoostSource, + ChatBoostSourceGiftCode, + ChatBoostSourceGiveaway, + ChatBoostSourcePremium, + ChatBoostUpdated, + Dice, + User, + UserChatBoosts, +) +from telegram._utils.datetime import UTC, to_timestamp +from telegram.constants import ChatBoostSources +from telegram.request import RequestData +from tests.auxil.slots import mro_slots + + +class ChatBoostDefaults: + chat_id = 1 + boost_id = "2" + giveaway_message_id = 3 + is_unclaimed = False + chat = Chat(1, "group") + user = User(1, "user", False) + date = to_timestamp(datetime.datetime.utcnow()) + default_source = ChatBoostSourcePremium(user) + + +@pytest.fixture(scope="module") +def chat_boost_removed(): + return ChatBoostRemoved( + chat=ChatBoostDefaults.chat, + boost_id=ChatBoostDefaults.boost_id, + remove_date=ChatBoostDefaults.date, + source=ChatBoostDefaults.default_source, + ) + + +@pytest.fixture(scope="module") +def chat_boost(): + return ChatBoost( + boost_id=ChatBoostDefaults.boost_id, + add_date=ChatBoostDefaults.date, + expiration_date=ChatBoostDefaults.date, + source=ChatBoostDefaults.default_source, + ) + + +@pytest.fixture(scope="module") +def chat_boost_updated(chat_boost): + return ChatBoostUpdated( + chat=ChatBoostDefaults.chat, + boost=chat_boost, + ) + + +def chat_boost_source_gift_code(): + return ChatBoostSourceGiftCode( + user=ChatBoostDefaults.user, + ) + + +def chat_boost_source_giveaway(): + return ChatBoostSourceGiveaway( + user=ChatBoostDefaults.user, + giveaway_message_id=ChatBoostDefaults.giveaway_message_id, + is_unclaimed=ChatBoostDefaults.is_unclaimed, + ) + + +def chat_boost_source_premium(): + return ChatBoostSourcePremium( + user=ChatBoostDefaults.user, + ) + + +@pytest.fixture(scope="module") +def user_chat_boosts(chat_boost): + return UserChatBoosts( + boosts=[chat_boost], + ) + + +@pytest.fixture +def chat_boost_source(request): + return request.param() + + +ignored = ["self", "api_kwargs"] + + +def make_json_dict(instance: ChatBoostSource, include_optional_args: bool = False) -> dict: + """Used to make the json dict which we use for testing de_json. Similar to iter_args()""" + json_dict = {"source": instance.source} + sig = inspect.signature(instance.__class__.__init__) + + for param in sig.parameters.values(): + if param.name in ignored: # ignore irrelevant params + continue + + val = getattr(instance, param.name) + if hasattr(val, "to_dict"): # convert the user object or any future ones to dict. + val = val.to_dict() + json_dict[param.name] = val + + return json_dict + + +def iter_args( + instance: ChatBoostSource, de_json_inst: ChatBoostSource, include_optional: bool = False +): + """ + We accept both the regular instance and de_json created instance and iterate over them for + easy one line testing later one. + """ + yield instance.source, de_json_inst.source # yield this here cause it's not available in sig. + + sig = inspect.signature(instance.__class__.__init__) + for param in sig.parameters.values(): + if param.name in ignored: + continue + inst_at, json_at = getattr(instance, param.name), getattr(de_json_inst, param.name) + if isinstance(json_at, datetime.datetime): # Convert datetime to int + json_at = to_timestamp(json_at) + if ( + param.default is not inspect.Parameter.empty and include_optional + ) or param.default is inspect.Parameter.empty: + yield inst_at, json_at + + +@pytest.mark.parametrize( + "chat_boost_source", + [ + chat_boost_source_gift_code, + chat_boost_source_giveaway, + chat_boost_source_premium, + ], + indirect=True, +) +class TestChatBoostSourceTypesWithoutRequest: + def test_slot_behaviour(self, chat_boost_source): + inst = chat_boost_source + for attr in inst.__slots__: + assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" + + def test_de_json_required_args(self, bot, chat_boost_source): + cls = chat_boost_source.__class__ + assert cls.de_json({}, bot) is None + assert ChatBoost.de_json({}, bot) is None + + json_dict = make_json_dict(chat_boost_source) + const_boost_source = ChatBoostSource.de_json(json_dict, bot) + assert const_boost_source.api_kwargs == {} + + assert isinstance(const_boost_source, ChatBoostSource) + assert isinstance(const_boost_source, cls) + for chat_mem_type_at, const_chat_mem_at in iter_args( + chat_boost_source, const_boost_source + ): + assert chat_mem_type_at == const_chat_mem_at + + def test_de_json_all_args(self, bot, chat_boost_source): + json_dict = make_json_dict(chat_boost_source, include_optional_args=True) + const_boost_source = ChatBoostSource.de_json(json_dict, bot) + assert const_boost_source.api_kwargs == {} + + assert isinstance(const_boost_source, ChatBoostSource) + assert isinstance(const_boost_source, chat_boost_source.__class__) + for c_mem_type_at, const_c_mem_at in iter_args( + chat_boost_source, const_boost_source, True + ): + assert c_mem_type_at == const_c_mem_at + + def test_de_json_invalid_source(self, chat_boost_source, bot): + json_dict = {"source": "invalid"} + chat_boost_source = ChatBoostSource.de_json(json_dict, bot) + + assert type(chat_boost_source) is ChatBoostSource + assert chat_boost_source.source == "invalid" + + def test_de_json_subclass(self, chat_boost_source, bot): + """This makes sure that e.g. ChatBoostSourcePremium(data, bot) never returns a + ChatBoostSourceGiftCode instance.""" + cls = chat_boost_source.__class__ + json_dict = make_json_dict(chat_boost_source, True) + assert type(cls.de_json(json_dict, bot)) is cls + + def test_to_dict(self, chat_boost_source): + chat_boost_dict = chat_boost_source.to_dict() + + assert isinstance(chat_boost_dict, dict) + assert chat_boost_dict["source"] == chat_boost_source.source + assert chat_boost_dict["user"] == chat_boost_source.user.to_dict() + + for slot in chat_boost_source.__slots__: # additional verification for the optional args + if slot == "user": # we already test "user" above: + continue + assert getattr(chat_boost_source, slot) == chat_boost_dict[slot] + + def test_equality(self, chat_boost_source): + a = ChatBoostSource(source="status") + b = ChatBoostSource(source="status") + c = chat_boost_source + d = deepcopy(chat_boost_source) + e = Dice(4, "emoji") + + assert a == b + assert hash(a) == hash(b) + + assert a != c + assert hash(a) != hash(c) + + assert a != d + assert hash(a) != hash(d) + + assert a != e + assert hash(a) != hash(e) + + assert c == d + assert hash(c) == hash(d) + + assert c != e + assert hash(c) != hash(e) + + def test_enum_init(self, chat_boost_source): + cbs = ChatBoostSource(source="foo") + assert cbs.source == "foo" + cbs = ChatBoostSource(source="premium") + assert cbs.source == ChatBoostSources.PREMIUM + + +class TestChatBoostWithoutRequest(ChatBoostDefaults): + def test_slot_behaviour(self, chat_boost): + inst = chat_boost + for attr in inst.__slots__: + assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" + + def test_de_json(self, bot, chat_boost): + json_dict = { + "boost_id": "2", + "add_date": self.date, + "expiration_date": self.date, + "source": self.default_source.to_dict(), + } + cb = ChatBoost.de_json(json_dict, bot) + + assert isinstance(cb, ChatBoost) + assert isinstance(cb.add_date, datetime.datetime) + assert isinstance(cb.expiration_date, datetime.datetime) + assert isinstance(cb.source, ChatBoostSource) + with cb._unfrozen(): + cb.add_date = to_timestamp(cb.add_date) + cb.expiration_date = to_timestamp(cb.expiration_date) + + # We don't compare cbu.boost to self.boost because we have to update the _id_attrs (sigh) + for slot in cb.__slots__: + assert getattr(cb, slot) == getattr(chat_boost, slot), f"attribute {slot} differs" + + def test_de_json_localization(self, bot, raw_bot, tz_bot): + json_dict = { + "boost_id": "2", + "add_date": self.date, + "expiration_date": self.date, + "source": self.default_source.to_dict(), + } + + cb_bot = ChatBoost.de_json(json_dict, bot) + cb_raw = ChatBoost.de_json(json_dict, raw_bot) + cb_tz = ChatBoost.de_json(json_dict, tz_bot) + + # comparing utcoffsets because comparing timezones is unpredicatable + message_offset = cb_tz.add_date.utcoffset() + tz_bot_offset = tz_bot.defaults.tzinfo.utcoffset(cb_tz.add_date.replace(tzinfo=None)) + + assert cb_raw.add_date.tzinfo == UTC + assert cb_bot.add_date.tzinfo == UTC + assert message_offset == tz_bot_offset + + def test_to_dict(self, chat_boost): + chat_boost_dict = chat_boost.to_dict() + + assert isinstance(chat_boost_dict, dict) + assert chat_boost_dict["boost_id"] == chat_boost.boost_id + assert chat_boost_dict["add_date"] == chat_boost.add_date + assert chat_boost_dict["expiration_date"] == chat_boost.expiration_date + assert chat_boost_dict["source"] == chat_boost.source.to_dict() + + def test_equality(self): + a = ChatBoost( + boost_id="2", + add_date=self.date, + expiration_date=self.date, + source=self.default_source, + ) + b = ChatBoost( + boost_id="2", + add_date=self.date, + expiration_date=self.date, + source=self.default_source, + ) + c = ChatBoost( + boost_id="3", + add_date=self.date, + expiration_date=self.date, + source=self.default_source, + ) + + assert a == b + assert hash(a) == hash(b) + + assert a != c + assert hash(a) != hash(c) + + +class TestChatBoostUpdatedWithoutRequest(ChatBoostDefaults): + def test_slot_behaviour(self, chat_boost_updated): + inst = chat_boost_updated + for attr in inst.__slots__: + assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" + + def test_de_json(self, bot, chat_boost): + json_dict = { + "chat": self.chat.to_dict(), + "boost": { + "boost_id": "2", + "add_date": self.date, + "expiration_date": self.date, + "source": self.default_source.to_dict(), + }, + } + cbu = ChatBoostUpdated.de_json(json_dict, bot) + + assert isinstance(cbu, ChatBoostUpdated) + assert cbu.chat == self.chat + # We don't compare cbu.boost to chat_boost because we have to update the _id_attrs (sigh) + with cbu.boost._unfrozen(): + cbu.boost.add_date = to_timestamp(cbu.boost.add_date) + cbu.boost.expiration_date = to_timestamp(cbu.boost.expiration_date) + for slot in cbu.boost.__slots__: # Assumes _id_attrs are same as slots + assert getattr(cbu.boost, slot) == getattr(chat_boost, slot), f"attr {slot} differs" + + # no need to test localization since that is already tested in the above class. + + def test_to_dict(self, chat_boost_updated): + chat_boost_updated_dict = chat_boost_updated.to_dict() + + assert isinstance(chat_boost_updated_dict, dict) + assert chat_boost_updated_dict["chat"] == chat_boost_updated.chat.to_dict() + assert chat_boost_updated_dict["boost"] == chat_boost_updated.boost.to_dict() + + def test_equality(self): + a = ChatBoostUpdated( + chat=Chat(1, "group"), + boost=ChatBoost( + boost_id="2", + add_date=self.date, + expiration_date=self.date, + source=self.default_source, + ), + ) + b = ChatBoostUpdated( + chat=Chat(1, "group"), + boost=ChatBoost( + boost_id="2", + add_date=self.date, + expiration_date=self.date, + source=self.default_source, + ), + ) + c = ChatBoostUpdated( + chat=Chat(2, "group"), + boost=ChatBoost( + boost_id="3", + add_date=self.date, + expiration_date=self.date, + source=self.default_source, + ), + ) + + assert a == b + assert hash(a) == hash(b) + + assert a != c + assert hash(a) != hash(c) + + +class TestChatBoostRemovedWithoutRequest(ChatBoostDefaults): + def test_slot_behaviour(self, chat_boost_removed): + inst = chat_boost_removed + for attr in inst.__slots__: + assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" + + def test_de_json(self, bot, chat_boost_removed): + json_dict = { + "chat": self.chat.to_dict(), + "boost_id": "2", + "remove_date": self.date, + "source": self.default_source.to_dict(), + } + cbr = ChatBoostRemoved.de_json(json_dict, bot) + + assert isinstance(cbr, ChatBoostRemoved) + assert cbr.chat == self.chat + assert cbr.boost_id == self.boost_id + assert to_timestamp(cbr.remove_date) == self.date + assert cbr.source == self.default_source + + def test_de_json_localization(self, bot, raw_bot, tz_bot): + json_dict = { + "chat": self.chat.to_dict(), + "boost_id": "2", + "remove_date": self.date, + "source": self.default_source.to_dict(), + } + + cbr_bot = ChatBoostRemoved.de_json(json_dict, bot) + cbr_raw = ChatBoostRemoved.de_json(json_dict, raw_bot) + cbr_tz = ChatBoostRemoved.de_json(json_dict, tz_bot) + + # comparing utcoffsets because comparing timezones is unpredicatable + message_offset = cbr_tz.remove_date.utcoffset() + tz_bot_offset = tz_bot.defaults.tzinfo.utcoffset(cbr_tz.remove_date.replace(tzinfo=None)) + + assert cbr_raw.remove_date.tzinfo == UTC + assert cbr_bot.remove_date.tzinfo == UTC + assert message_offset == tz_bot_offset + + def test_to_dict(self, chat_boost_removed): + chat_boost_removed_dict = chat_boost_removed.to_dict() + + assert isinstance(chat_boost_removed_dict, dict) + assert chat_boost_removed_dict["chat"] == chat_boost_removed.chat.to_dict() + assert chat_boost_removed_dict["boost_id"] == chat_boost_removed.boost_id + assert chat_boost_removed_dict["remove_date"] == chat_boost_removed.remove_date + assert chat_boost_removed_dict["source"] == chat_boost_removed.source.to_dict() + + def test_equality(self): + a = ChatBoostRemoved( + chat=Chat(1, "group"), + boost_id="2", + remove_date=self.date, + source=self.default_source, + ) + b = ChatBoostRemoved( + chat=Chat(1, "group"), + boost_id="2", + remove_date=self.date, + source=self.default_source, + ) + c = ChatBoostRemoved( + chat=Chat(2, "group"), + boost_id="3", + remove_date=self.date, + source=self.default_source, + ) + + assert a == b + assert hash(a) == hash(b) + + assert a != c + assert hash(a) != hash(c) + + +class TestUserChatBoostsWithoutRequest(ChatBoostDefaults): + def test_slot_behaviour(self, user_chat_boosts): + inst = user_chat_boosts + for attr in inst.__slots__: + assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" + + def test_de_json(self, bot, user_chat_boosts): + json_dict = { + "boosts": [ + { + "boost_id": "2", + "add_date": self.date, + "expiration_date": self.date, + "source": self.default_source.to_dict(), + } + ] + } + ucb = UserChatBoosts.de_json(json_dict, bot) + + assert isinstance(ucb, UserChatBoosts) + assert isinstance(ucb.boosts[0], ChatBoost) + assert ucb.boosts[0].boost_id == self.boost_id + assert to_timestamp(ucb.boosts[0].add_date) == self.date + assert to_timestamp(ucb.boosts[0].expiration_date) == self.date + assert ucb.boosts[0].source == self.default_source + + def test_to_dict(self, user_chat_boosts): + user_chat_boosts_dict = user_chat_boosts.to_dict() + + assert isinstance(user_chat_boosts_dict, dict) + assert isinstance(user_chat_boosts_dict["boosts"], list) + assert user_chat_boosts_dict["boosts"][0] == user_chat_boosts.boosts[0].to_dict() + + async def test_get_user_chat_boosts(self, monkeypatch, bot): + async def make_assertion(url, request_data: RequestData, *args, **kwargs): + data = request_data.json_parameters + chat_id = data["chat_id"] == "3" + user_id = data["user_id"] == "2" + if not all((chat_id, user_id)): + pytest.fail("I got wrong parameters in post") + return data + + monkeypatch.setattr(bot.request, "post", make_assertion) + + assert await bot.get_user_chat_boosts("3", 2) + + +class TestUserChatBoostsWithRequest(ChatBoostDefaults): + async def test_get_user_chat_boosts(self, bot, channel_id, chat_id): + chat_boosts = await bot.get_user_chat_boosts(channel_id, chat_id) + assert isinstance(chat_boosts, UserChatBoosts) + + +class TestChatBoostAddedWithoutRequest: + boost_count = 100 + + def test_slot_behaviour(self): + action = ChatBoostAdded(8) + for attr in action.__slots__: + assert getattr(action, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(action)) == len(set(mro_slots(action))), "duplicate slot" + + def test_de_json(self): + json_dict = {"boost_count": self.boost_count} + chat_boost_added = ChatBoostAdded.de_json(json_dict, None) + assert chat_boost_added.api_kwargs == {} + + assert chat_boost_added.boost_count == self.boost_count + + def test_to_dict(self): + chat_boost_added = ChatBoostAdded(self.boost_count) + chat_boost_added_dict = chat_boost_added.to_dict() + + assert isinstance(chat_boost_added_dict, dict) + assert chat_boost_added_dict["boost_count"] == self.boost_count + + def test_equality(self): + a = ChatBoostAdded(100) + b = ChatBoostAdded(100) + c = ChatBoostAdded(50) + d = Chat(1, "") + + assert a == b + assert hash(a) == hash(b) + + assert a != c + assert hash(a) != hash(c) + + assert a != d + assert hash(a) != hash(d) diff --git a/test_chatfullinfo.py b/test_chatfullinfo.py new file mode 100644 index 0000000000000000000000000000000000000000..7e5bc90baaec03ddd3376cfb42a36116b2172ad3 --- /dev/null +++ b/test_chatfullinfo.py @@ -0,0 +1,326 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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/]. +import datetime + +import pytest + +from telegram import ( + Birthdate, + BusinessIntro, + BusinessLocation, + BusinessOpeningHours, + BusinessOpeningHoursInterval, + Chat, + ChatFullInfo, + ChatLocation, + ChatPermissions, + Location, + ReactionTypeCustomEmoji, + ReactionTypeEmoji, +) +from telegram._utils.datetime import UTC, to_timestamp +from telegram.constants import ReactionEmoji +from tests.auxil.slots import mro_slots + + +@pytest.fixture(scope="module") +def chat_full_info(bot): + chat = ChatFullInfo( + ChatFullInfoTestBase.id_, + type=ChatFullInfoTestBase.type_, + accent_color_id=ChatFullInfoTestBase.accent_color_id, + max_reaction_count=ChatFullInfoTestBase.max_reaction_count, + title=ChatFullInfoTestBase.title, + username=ChatFullInfoTestBase.username, + sticker_set_name=ChatFullInfoTestBase.sticker_set_name, + can_set_sticker_set=ChatFullInfoTestBase.can_set_sticker_set, + permissions=ChatFullInfoTestBase.permissions, + slow_mode_delay=ChatFullInfoTestBase.slow_mode_delay, + bio=ChatFullInfoTestBase.bio, + linked_chat_id=ChatFullInfoTestBase.linked_chat_id, + location=ChatFullInfoTestBase.location, + has_private_forwards=ChatFullInfoTestBase.has_private_forwards, + has_protected_content=ChatFullInfoTestBase.has_protected_content, + has_visible_history=ChatFullInfoTestBase.has_visible_history, + join_to_send_messages=ChatFullInfoTestBase.join_to_send_messages, + join_by_request=ChatFullInfoTestBase.join_by_request, + has_restricted_voice_and_video_messages=( + ChatFullInfoTestBase.has_restricted_voice_and_video_messages + ), + is_forum=ChatFullInfoTestBase.is_forum, + active_usernames=ChatFullInfoTestBase.active_usernames, + emoji_status_custom_emoji_id=ChatFullInfoTestBase.emoji_status_custom_emoji_id, + emoji_status_expiration_date=ChatFullInfoTestBase.emoji_status_expiration_date, + has_aggressive_anti_spam_enabled=ChatFullInfoTestBase.has_aggressive_anti_spam_enabled, + has_hidden_members=ChatFullInfoTestBase.has_hidden_members, + available_reactions=ChatFullInfoTestBase.available_reactions, + background_custom_emoji_id=ChatFullInfoTestBase.background_custom_emoji_id, + profile_accent_color_id=ChatFullInfoTestBase.profile_accent_color_id, + profile_background_custom_emoji_id=ChatFullInfoTestBase.profile_background_custom_emoji_id, + unrestrict_boost_count=ChatFullInfoTestBase.unrestrict_boost_count, + custom_emoji_sticker_set_name=ChatFullInfoTestBase.custom_emoji_sticker_set_name, + business_intro=ChatFullInfoTestBase.business_intro, + business_location=ChatFullInfoTestBase.business_location, + business_opening_hours=ChatFullInfoTestBase.business_opening_hours, + birthdate=ChatFullInfoTestBase.birthdate, + personal_chat=ChatFullInfoTestBase.personal_chat, + first_name=ChatFullInfoTestBase.first_name, + last_name=ChatFullInfoTestBase.last_name, + can_send_paid_media=ChatFullInfoTestBase.can_send_paid_media, + ) + chat.set_bot(bot) + chat._unfreeze() + return chat + + +# Shortcut methods are tested in test_chat.py. +class ChatFullInfoTestBase: + id_ = -28767330 + max_reaction_count = 2 + title = "ToledosPalaceBot - Group" + type_ = "group" + username = "username" + sticker_set_name = "stickers" + can_set_sticker_set = False + permissions = ChatPermissions( + can_send_messages=True, + can_change_info=False, + can_invite_users=True, + ) + slow_mode_delay = 30 + bio = "I'm a Barbie Girl in a Barbie World" + linked_chat_id = 11880 + location = ChatLocation(Location(123, 456), "Barbie World") + has_protected_content = True + has_visible_history = True + has_private_forwards = True + join_to_send_messages = True + join_by_request = True + has_restricted_voice_and_video_messages = True + is_forum = True + active_usernames = ["These", "Are", "Usernames!"] + emoji_status_custom_emoji_id = "VeryUniqueCustomEmojiID" + emoji_status_expiration_date = datetime.datetime.now(tz=UTC).replace(microsecond=0) + has_aggressive_anti_spam_enabled = True + has_hidden_members = True + available_reactions = [ + ReactionTypeEmoji(ReactionEmoji.THUMBS_DOWN), + ReactionTypeCustomEmoji("custom_emoji_id"), + ] + business_intro = BusinessIntro("Title", "Description", None) + business_location = BusinessLocation("Address", Location(123, 456)) + business_opening_hours = BusinessOpeningHours( + "Country/City", + [BusinessOpeningHoursInterval(opening, opening + 60) for opening in (0, 24 * 60)], + ) + accent_color_id = 1 + background_custom_emoji_id = "background_custom_emoji_id" + profile_accent_color_id = 2 + profile_background_custom_emoji_id = "profile_background_custom_emoji_id" + unrestrict_boost_count = 100 + custom_emoji_sticker_set_name = "custom_emoji_sticker_set_name" + birthdate = Birthdate(1, 1) + personal_chat = Chat(3, "private", "private") + first_name = "first_name" + last_name = "last_name" + can_send_paid_media = True + + +class TestChatFullInfoWithoutRequest(ChatFullInfoTestBase): + def test_slot_behaviour(self, chat_full_info): + cfi = chat_full_info + for attr in cfi.__slots__: + assert getattr(cfi, attr, "err") != "err", f"got extra slot '{attr}'" + + assert len(mro_slots(cfi)) == len(set(mro_slots(cfi))), "duplicate slot" + + def test_de_json(self, bot): + json_dict = { + "id": self.id_, + "title": self.title, + "type": self.type_, + "accent_color_id": self.accent_color_id, + "max_reaction_count": self.max_reaction_count, + "username": self.username, + "sticker_set_name": self.sticker_set_name, + "can_set_sticker_set": self.can_set_sticker_set, + "permissions": self.permissions.to_dict(), + "slow_mode_delay": self.slow_mode_delay, + "bio": self.bio, + "business_intro": self.business_intro.to_dict(), + "business_location": self.business_location.to_dict(), + "business_opening_hours": self.business_opening_hours.to_dict(), + "has_protected_content": self.has_protected_content, + "has_visible_history": self.has_visible_history, + "has_private_forwards": self.has_private_forwards, + "linked_chat_id": self.linked_chat_id, + "location": self.location.to_dict(), + "join_to_send_messages": self.join_to_send_messages, + "join_by_request": self.join_by_request, + "has_restricted_voice_and_video_messages": ( + self.has_restricted_voice_and_video_messages + ), + "is_forum": self.is_forum, + "active_usernames": self.active_usernames, + "emoji_status_custom_emoji_id": self.emoji_status_custom_emoji_id, + "emoji_status_expiration_date": to_timestamp(self.emoji_status_expiration_date), + "has_aggressive_anti_spam_enabled": self.has_aggressive_anti_spam_enabled, + "has_hidden_members": self.has_hidden_members, + "available_reactions": [reaction.to_dict() for reaction in self.available_reactions], + "background_custom_emoji_id": self.background_custom_emoji_id, + "profile_accent_color_id": self.profile_accent_color_id, + "profile_background_custom_emoji_id": self.profile_background_custom_emoji_id, + "unrestrict_boost_count": self.unrestrict_boost_count, + "custom_emoji_sticker_set_name": self.custom_emoji_sticker_set_name, + "birthdate": self.birthdate.to_dict(), + "personal_chat": self.personal_chat.to_dict(), + "first_name": self.first_name, + "last_name": self.last_name, + "can_send_paid_media": self.can_send_paid_media, + } + cfi = ChatFullInfo.de_json(json_dict, bot) + assert cfi.id == self.id_ + assert cfi.title == self.title + assert cfi.type == self.type_ + assert cfi.username == self.username + assert cfi.sticker_set_name == self.sticker_set_name + assert cfi.can_set_sticker_set == self.can_set_sticker_set + assert cfi.permissions == self.permissions + assert cfi.slow_mode_delay == self.slow_mode_delay + assert cfi.bio == self.bio + assert cfi.business_intro == self.business_intro + assert cfi.business_location == self.business_location + assert cfi.business_opening_hours == self.business_opening_hours + assert cfi.has_protected_content == self.has_protected_content + assert cfi.has_visible_history == self.has_visible_history + assert cfi.has_private_forwards == self.has_private_forwards + assert cfi.linked_chat_id == self.linked_chat_id + assert cfi.location.location == self.location.location + assert cfi.location.address == self.location.address + assert cfi.join_to_send_messages == self.join_to_send_messages + assert cfi.join_by_request == self.join_by_request + assert ( + cfi.has_restricted_voice_and_video_messages + == self.has_restricted_voice_and_video_messages + ) + assert cfi.is_forum == self.is_forum + assert cfi.active_usernames == tuple(self.active_usernames) + assert cfi.emoji_status_custom_emoji_id == self.emoji_status_custom_emoji_id + assert cfi.emoji_status_expiration_date == (self.emoji_status_expiration_date) + assert cfi.has_aggressive_anti_spam_enabled == self.has_aggressive_anti_spam_enabled + assert cfi.has_hidden_members == self.has_hidden_members + assert cfi.available_reactions == tuple(self.available_reactions) + assert cfi.accent_color_id == self.accent_color_id + assert cfi.background_custom_emoji_id == self.background_custom_emoji_id + assert cfi.profile_accent_color_id == self.profile_accent_color_id + assert cfi.profile_background_custom_emoji_id == self.profile_background_custom_emoji_id + assert cfi.unrestrict_boost_count == self.unrestrict_boost_count + assert cfi.custom_emoji_sticker_set_name == self.custom_emoji_sticker_set_name + assert cfi.birthdate == self.birthdate + assert cfi.personal_chat == self.personal_chat + assert cfi.first_name == self.first_name + assert cfi.last_name == self.last_name + assert cfi.max_reaction_count == self.max_reaction_count + assert cfi.can_send_paid_media == self.can_send_paid_media + + def test_de_json_localization(self, bot, raw_bot, tz_bot): + json_dict = { + "id": self.id_, + "type": self.type_, + "accent_color_id": self.accent_color_id, + "max_reaction_count": self.max_reaction_count, + "emoji_status_expiration_date": to_timestamp(self.emoji_status_expiration_date), + } + cfi_bot = ChatFullInfo.de_json(json_dict, bot) + cfi_bot_raw = ChatFullInfo.de_json(json_dict, raw_bot) + cfi_bot_tz = ChatFullInfo.de_json(json_dict, tz_bot) + + # comparing utcoffsets because comparing tzinfo objects is not reliable + emoji_expire_offset = cfi_bot_tz.emoji_status_expiration_date.utcoffset() + emoji_expire_offset_tz = tz_bot.defaults.tzinfo.utcoffset( + cfi_bot_tz.emoji_status_expiration_date.replace(tzinfo=None) + ) + + assert cfi_bot.emoji_status_expiration_date.tzinfo == UTC + assert cfi_bot_raw.emoji_status_expiration_date.tzinfo == UTC + assert emoji_expire_offset_tz == emoji_expire_offset + + def test_to_dict(self, chat_full_info): + cfi = chat_full_info + cfi_dict = cfi.to_dict() + + assert isinstance(cfi_dict, dict) + assert cfi_dict["id"] == cfi.id + assert cfi_dict["title"] == cfi.title + assert cfi_dict["type"] == cfi.type + assert cfi_dict["username"] == cfi.username + assert cfi_dict["permissions"] == cfi.permissions.to_dict() + assert cfi_dict["slow_mode_delay"] == cfi.slow_mode_delay + assert cfi_dict["bio"] == cfi.bio + assert cfi_dict["business_intro"] == cfi.business_intro.to_dict() + assert cfi_dict["business_location"] == cfi.business_location.to_dict() + assert cfi_dict["business_opening_hours"] == cfi.business_opening_hours.to_dict() + assert cfi_dict["has_private_forwards"] == cfi.has_private_forwards + assert cfi_dict["has_protected_content"] == cfi.has_protected_content + assert cfi_dict["has_visible_history"] == cfi.has_visible_history + assert cfi_dict["linked_chat_id"] == cfi.linked_chat_id + assert cfi_dict["location"] == cfi.location.to_dict() + assert cfi_dict["join_to_send_messages"] == cfi.join_to_send_messages + assert cfi_dict["join_by_request"] == cfi.join_by_request + assert ( + cfi_dict["has_restricted_voice_and_video_messages"] + == cfi.has_restricted_voice_and_video_messages + ) + assert cfi_dict["is_forum"] == cfi.is_forum + assert cfi_dict["active_usernames"] == list(cfi.active_usernames) + assert cfi_dict["emoji_status_custom_emoji_id"] == cfi.emoji_status_custom_emoji_id + assert cfi_dict["emoji_status_expiration_date"] == to_timestamp( + cfi.emoji_status_expiration_date + ) + assert cfi_dict["has_aggressive_anti_spam_enabled"] == cfi.has_aggressive_anti_spam_enabled + assert cfi_dict["has_hidden_members"] == cfi.has_hidden_members + assert cfi_dict["available_reactions"] == [ + reaction.to_dict() for reaction in cfi.available_reactions + ] + assert cfi_dict["accent_color_id"] == cfi.accent_color_id + assert cfi_dict["background_custom_emoji_id"] == cfi.background_custom_emoji_id + assert cfi_dict["profile_accent_color_id"] == cfi.profile_accent_color_id + assert ( + cfi_dict["profile_background_custom_emoji_id"] + == cfi.profile_background_custom_emoji_id + ) + assert cfi_dict["custom_emoji_sticker_set_name"] == cfi.custom_emoji_sticker_set_name + assert cfi_dict["unrestrict_boost_count"] == cfi.unrestrict_boost_count + assert cfi_dict["birthdate"] == cfi.birthdate.to_dict() + assert cfi_dict["personal_chat"] == cfi.personal_chat.to_dict() + assert cfi_dict["first_name"] == cfi.first_name + assert cfi_dict["last_name"] == cfi.last_name + assert cfi_dict["can_send_paid_media"] == cfi.can_send_paid_media + + assert cfi_dict["max_reaction_count"] == cfi.max_reaction_count + + def test_always_tuples_attributes(self): + cfi = ChatFullInfo( + id=123, + type=Chat.PRIVATE, + accent_color_id=1, + max_reaction_count=2, + ) + assert isinstance(cfi.active_usernames, tuple) + assert cfi.active_usernames == () diff --git a/test_chatinvitelink.py b/test_chatinvitelink.py new file mode 100644 index 0000000000000000000000000000000000000000..9331166b4663320c9eb3789e26a666f8b70fe7ae --- /dev/null +++ b/test_chatinvitelink.py @@ -0,0 +1,194 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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/]. +import datetime + +import pytest + +from telegram import ChatInviteLink, User +from telegram._utils.datetime import UTC, to_timestamp +from tests.auxil.slots import mro_slots + + +@pytest.fixture(scope="module") +def creator(): + return User(1, "First name", False) + + +@pytest.fixture(scope="module") +def invite_link(creator): + return ChatInviteLink( + ChatInviteLinkTestBase.link, + creator, + ChatInviteLinkTestBase.creates_join_request, + ChatInviteLinkTestBase.primary, + ChatInviteLinkTestBase.revoked, + expire_date=ChatInviteLinkTestBase.expire_date, + member_limit=ChatInviteLinkTestBase.member_limit, + name=ChatInviteLinkTestBase.name, + pending_join_request_count=ChatInviteLinkTestBase.pending_join_request_count, + subscription_period=ChatInviteLinkTestBase.subscription_period, + subscription_price=ChatInviteLinkTestBase.subscription_price, + ) + + +class ChatInviteLinkTestBase: + link = "thisialink" + creates_join_request = False + primary = True + revoked = False + expire_date = datetime.datetime.now(datetime.timezone.utc) + member_limit = 42 + name = "LinkName" + pending_join_request_count = 42 + subscription_period = 43 + subscription_price = 44 + + +class TestChatInviteLinkWithoutRequest(ChatInviteLinkTestBase): + def test_slot_behaviour(self, invite_link): + for attr in invite_link.__slots__: + assert getattr(invite_link, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(invite_link)) == len(set(mro_slots(invite_link))), "duplicate slot" + + def test_de_json_required_args(self, bot, creator): + json_dict = { + "invite_link": self.link, + "creator": creator.to_dict(), + "creates_join_request": self.creates_join_request, + "is_primary": self.primary, + "is_revoked": self.revoked, + } + + invite_link = ChatInviteLink.de_json(json_dict, bot) + assert invite_link.api_kwargs == {} + + assert invite_link.invite_link == self.link + assert invite_link.creator == creator + assert invite_link.creates_join_request == self.creates_join_request + assert invite_link.is_primary == self.primary + assert invite_link.is_revoked == self.revoked + + def test_de_json_all_args(self, bot, creator): + json_dict = { + "invite_link": self.link, + "creator": creator.to_dict(), + "creates_join_request": self.creates_join_request, + "is_primary": self.primary, + "is_revoked": self.revoked, + "expire_date": to_timestamp(self.expire_date), + "member_limit": self.member_limit, + "name": self.name, + "pending_join_request_count": str(self.pending_join_request_count), + "subscription_period": self.subscription_period, + "subscription_price": self.subscription_price, + } + + invite_link = ChatInviteLink.de_json(json_dict, bot) + assert invite_link.api_kwargs == {} + + assert invite_link.invite_link == self.link + assert invite_link.creator == creator + assert invite_link.creates_join_request == self.creates_join_request + assert invite_link.is_primary == self.primary + assert invite_link.is_revoked == self.revoked + assert abs(invite_link.expire_date - self.expire_date) < datetime.timedelta(seconds=1) + assert to_timestamp(invite_link.expire_date) == to_timestamp(self.expire_date) + assert invite_link.member_limit == self.member_limit + assert invite_link.name == self.name + assert invite_link.pending_join_request_count == self.pending_join_request_count + assert invite_link.subscription_period == self.subscription_period + assert invite_link.subscription_price == self.subscription_price + + def test_de_json_localization(self, tz_bot, bot, raw_bot, creator): + json_dict = { + "invite_link": self.link, + "creator": creator.to_dict(), + "creates_join_request": self.creates_join_request, + "is_primary": self.primary, + "is_revoked": self.revoked, + "expire_date": to_timestamp(self.expire_date), + "member_limit": self.member_limit, + "name": self.name, + "pending_join_request_count": str(self.pending_join_request_count), + } + + invite_link_raw = ChatInviteLink.de_json(json_dict, raw_bot) + invite_link_bot = ChatInviteLink.de_json(json_dict, bot) + invite_link_tz = ChatInviteLink.de_json(json_dict, tz_bot) + + # comparing utcoffsets because comparing timezones is unpredicatable + invite_offset = invite_link_tz.expire_date.utcoffset() + tz_bot_offset = tz_bot.defaults.tzinfo.utcoffset( + invite_link_tz.expire_date.replace(tzinfo=None) + ) + + assert invite_link_raw.expire_date.tzinfo == UTC + assert invite_link_bot.expire_date.tzinfo == UTC + assert invite_offset == tz_bot_offset + + def test_to_dict(self, invite_link): + invite_link_dict = invite_link.to_dict() + assert isinstance(invite_link_dict, dict) + assert invite_link_dict["creator"] == invite_link.creator.to_dict() + assert invite_link_dict["invite_link"] == invite_link.invite_link + assert invite_link_dict["creates_join_request"] == invite_link.creates_join_request + assert invite_link_dict["is_primary"] == self.primary + assert invite_link_dict["is_revoked"] == self.revoked + assert invite_link_dict["expire_date"] == to_timestamp(self.expire_date) + assert invite_link_dict["member_limit"] == self.member_limit + assert invite_link_dict["name"] == self.name + assert invite_link_dict["pending_join_request_count"] == self.pending_join_request_count + assert invite_link_dict["subscription_period"] == self.subscription_period + assert invite_link_dict["subscription_price"] == self.subscription_price + + def test_equality(self): + a = ChatInviteLink("link", User(1, "", False), True, True, True) + b = ChatInviteLink("link", User(1, "", False), True, True, True) + c = ChatInviteLink("link", User(2, "", False), True, True, True) + d1 = ChatInviteLink("link", User(1, "", False), False, True, True) + d2 = ChatInviteLink("link", User(1, "", False), True, False, True) + d3 = ChatInviteLink("link", User(1, "", False), True, True, False) + e = ChatInviteLink("notalink", User(1, "", False), True, False, True) + f = ChatInviteLink("notalink", User(1, "", False), True, True, True) + g = User(1, "", False) + + assert a == b + assert hash(a) == hash(b) + assert a is not b + + assert a != c + assert hash(a) != hash(c) + + assert a != d1 + assert hash(a) != hash(d1) + + assert a != d2 + assert hash(a) != hash(d2) + + assert d2 != d3 + assert hash(d2) != hash(d3) + + assert a != e + assert hash(a) != hash(e) + + assert a != f + assert hash(a) != hash(f) + + assert a != g + assert hash(a) != hash(g) diff --git a/test_chatjoinrequest.py b/test_chatjoinrequest.py new file mode 100644 index 0000000000000000000000000000000000000000..cdf2787e8ea5255ab094f7e2af5135990aa434d9 --- /dev/null +++ b/test_chatjoinrequest.py @@ -0,0 +1,200 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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/]. +import datetime + +import pytest + +from telegram import Bot, Chat, ChatInviteLink, ChatJoinRequest, User +from telegram._utils.datetime import UTC, to_timestamp +from tests.auxil.bot_method_checks import ( + check_defaults_handling, + check_shortcut_call, + check_shortcut_signature, +) +from tests.auxil.slots import mro_slots + + +@pytest.fixture(scope="module") +def time(): + return datetime.datetime.now(tz=UTC) + + +@pytest.fixture(scope="module") +def chat_join_request(bot, time): + cjr = ChatJoinRequest( + chat=ChatJoinRequestTestBase.chat, + from_user=ChatJoinRequestTestBase.from_user, + date=time, + bio=ChatJoinRequestTestBase.bio, + invite_link=ChatJoinRequestTestBase.invite_link, + user_chat_id=ChatJoinRequestTestBase.from_user.id, + ) + cjr.set_bot(bot) + return cjr + + +class ChatJoinRequestTestBase: + chat = Chat(1, Chat.SUPERGROUP) + from_user = User(2, "first_name", False) + bio = "bio" + invite_link = ChatInviteLink( + "https://invite.link", + User(42, "creator", False), + creates_join_request=False, + name="InviteLink", + is_revoked=False, + is_primary=False, + ) + + +class TestChatJoinRequestWithoutRequest(ChatJoinRequestTestBase): + def test_slot_behaviour(self, chat_join_request): + inst = chat_join_request + for attr in inst.__slots__: + assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" + + def test_de_json(self, bot, time): + json_dict = { + "chat": self.chat.to_dict(), + "from": self.from_user.to_dict(), + "date": to_timestamp(time), + "user_chat_id": self.from_user.id, + } + chat_join_request = ChatJoinRequest.de_json(json_dict, bot) + assert chat_join_request.api_kwargs == {} + + assert chat_join_request.chat == self.chat + assert chat_join_request.from_user == self.from_user + assert abs(chat_join_request.date - time) < datetime.timedelta(seconds=1) + assert to_timestamp(chat_join_request.date) == to_timestamp(time) + assert chat_join_request.user_chat_id == self.from_user.id + + json_dict.update({"bio": self.bio, "invite_link": self.invite_link.to_dict()}) + chat_join_request = ChatJoinRequest.de_json(json_dict, bot) + assert chat_join_request.api_kwargs == {} + + assert chat_join_request.chat == self.chat + assert chat_join_request.from_user == self.from_user + assert abs(chat_join_request.date - time) < datetime.timedelta(seconds=1) + assert to_timestamp(chat_join_request.date) == to_timestamp(time) + assert chat_join_request.user_chat_id == self.from_user.id + assert chat_join_request.bio == self.bio + assert chat_join_request.invite_link == self.invite_link + + def test_de_json_localization(self, tz_bot, bot, raw_bot, time): + json_dict = { + "chat": self.chat.to_dict(), + "from": self.from_user.to_dict(), + "date": to_timestamp(time), + "user_chat_id": self.from_user.id, + } + + chatjoin_req_raw = ChatJoinRequest.de_json(json_dict, raw_bot) + chatjoin_req_bot = ChatJoinRequest.de_json(json_dict, bot) + chatjoin_req_tz = ChatJoinRequest.de_json(json_dict, tz_bot) + + # comparing utcoffsets because comparing timezones is unpredicatable + chatjoin_req_offset = chatjoin_req_tz.date.utcoffset() + tz_bot_offset = tz_bot.defaults.tzinfo.utcoffset(chatjoin_req_tz.date.replace(tzinfo=None)) + + assert chatjoin_req_raw.date.tzinfo == UTC + assert chatjoin_req_bot.date.tzinfo == UTC + assert chatjoin_req_offset == tz_bot_offset + + def test_to_dict(self, chat_join_request, time): + chat_join_request_dict = chat_join_request.to_dict() + + assert isinstance(chat_join_request_dict, dict) + assert chat_join_request_dict["chat"] == chat_join_request.chat.to_dict() + assert chat_join_request_dict["from"] == chat_join_request.from_user.to_dict() + assert chat_join_request_dict["date"] == to_timestamp(chat_join_request.date) + assert chat_join_request_dict["bio"] == chat_join_request.bio + assert chat_join_request_dict["invite_link"] == chat_join_request.invite_link.to_dict() + assert chat_join_request_dict["user_chat_id"] == self.from_user.id + + def test_equality(self, chat_join_request, time): + a = chat_join_request + b = ChatJoinRequest(self.chat, self.from_user, time, self.from_user.id) + c = ChatJoinRequest(self.chat, self.from_user, time, self.from_user.id, bio="bio") + d = ChatJoinRequest( + self.chat, self.from_user, time + datetime.timedelta(1), self.from_user.id + ) + e = ChatJoinRequest(self.chat, User(-1, "last_name", True), time, -1) + f = User(456, "", False) + + assert a == b + assert hash(a) == hash(b) + assert a is not b + + assert a == c + assert hash(a) == hash(c) + + assert a != d + assert hash(a) != hash(d) + + assert a != e + assert hash(a) != hash(e) + + assert a != f + assert hash(a) != hash(f) + + async def test_approve(self, monkeypatch, chat_join_request): + async def make_assertion(*_, **kwargs): + chat_id_test = kwargs["chat_id"] == chat_join_request.chat.id + user_id_test = kwargs["user_id"] == chat_join_request.from_user.id + + return chat_id_test and user_id_test + + assert check_shortcut_signature( + ChatJoinRequest.approve, Bot.approve_chat_join_request, ["chat_id", "user_id"], [] + ) + assert await check_shortcut_call( + chat_join_request.approve, chat_join_request.get_bot(), "approve_chat_join_request" + ) + assert await check_defaults_handling( + chat_join_request.approve, chat_join_request.get_bot() + ) + + monkeypatch.setattr( + chat_join_request.get_bot(), "approve_chat_join_request", make_assertion + ) + assert await chat_join_request.approve() + + async def test_decline(self, monkeypatch, chat_join_request): + async def make_assertion(*_, **kwargs): + chat_id_test = kwargs["chat_id"] == chat_join_request.chat.id + user_id_test = kwargs["user_id"] == chat_join_request.from_user.id + + return chat_id_test and user_id_test + + assert check_shortcut_signature( + ChatJoinRequest.decline, Bot.decline_chat_join_request, ["chat_id", "user_id"], [] + ) + assert await check_shortcut_call( + chat_join_request.decline, chat_join_request.get_bot(), "decline_chat_join_request" + ) + assert await check_defaults_handling( + chat_join_request.decline, chat_join_request.get_bot() + ) + + monkeypatch.setattr( + chat_join_request.get_bot(), "decline_chat_join_request", make_assertion + ) + assert await chat_join_request.decline() diff --git a/test_chatlocation.py b/test_chatlocation.py new file mode 100644 index 0000000000000000000000000000000000000000..00481d644d5519fc8e233d40b58a394726edfad4 --- /dev/null +++ b/test_chatlocation.py @@ -0,0 +1,79 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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/]. + +import pytest + +from telegram import ChatLocation, Location, User +from tests.auxil.slots import mro_slots + + +@pytest.fixture(scope="module") +def chat_location(): + return ChatLocation(ChatLocationTestBase.location, ChatLocationTestBase.address) + + +class ChatLocationTestBase: + location = Location(123, 456) + address = "The Shire" + + +class TestChatLocationWithoutRequest(ChatLocationTestBase): + def test_slot_behaviour(self, chat_location): + inst = chat_location + for attr in inst.__slots__: + assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" + + def test_de_json(self, bot): + json_dict = { + "location": self.location.to_dict(), + "address": self.address, + } + chat_location = ChatLocation.de_json(json_dict, bot) + assert chat_location.api_kwargs == {} + + assert chat_location.location == self.location + assert chat_location.address == self.address + + def test_to_dict(self, chat_location): + chat_location_dict = chat_location.to_dict() + + assert isinstance(chat_location_dict, dict) + assert chat_location_dict["location"] == chat_location.location.to_dict() + assert chat_location_dict["address"] == chat_location.address + + def test_equality(self, chat_location): + a = chat_location + b = ChatLocation(self.location, self.address) + c = ChatLocation(self.location, "Mordor") + d = ChatLocation(Location(456, 132), self.address) + e = User(456, "", False) + + assert a == b + assert hash(a) == hash(b) + assert a is not b + + assert a == c + assert hash(a) == hash(c) + + assert a != d + assert hash(a) != hash(d) + + assert a != e + assert hash(a) != hash(e) diff --git a/test_chatmember.py b/test_chatmember.py new file mode 100644 index 0000000000000000000000000000000000000000..4296fdd272379af5cfe53def7553ce77b698dad3 --- /dev/null +++ b/test_chatmember.py @@ -0,0 +1,306 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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/]. +import datetime +import inspect +from copy import deepcopy + +import pytest + +from telegram import ( + ChatMember, + ChatMemberAdministrator, + ChatMemberBanned, + ChatMemberLeft, + ChatMemberMember, + ChatMemberOwner, + ChatMemberRestricted, + Dice, + User, +) +from telegram._utils.datetime import UTC, to_timestamp +from tests.auxil.slots import mro_slots + +ignored = ["self", "api_kwargs"] + + +class CMDefaults: + user = User(1, "First name", False) + custom_title: str = "PTB" + is_anonymous: bool = True + until_date: datetime.datetime = to_timestamp(datetime.datetime.utcnow()) + can_be_edited: bool = False + can_change_info: bool = True + can_post_messages: bool = True + can_edit_messages: bool = True + can_delete_messages: bool = True + can_invite_users: bool = True + can_restrict_members: bool = True + can_pin_messages: bool = True + can_promote_members: bool = True + can_send_messages: bool = True + can_send_media_messages: bool = True + can_send_polls: bool = True + can_send_other_messages: bool = True + can_add_web_page_previews: bool = True + is_member: bool = True + can_manage_chat: bool = True + can_manage_video_chats: bool = True + can_manage_topics: bool = True + can_send_audios: bool = True + can_send_documents: bool = True + can_send_photos: bool = True + can_send_videos: bool = True + can_send_video_notes: bool = True + can_send_voice_notes: bool = True + can_post_stories: bool = True + can_edit_stories: bool = True + can_delete_stories: bool = True + + +def chat_member_owner(): + return ChatMemberOwner(CMDefaults.user, CMDefaults.is_anonymous, CMDefaults.custom_title) + + +def chat_member_administrator(): + return ChatMemberAdministrator( + CMDefaults.user, + CMDefaults.can_be_edited, + CMDefaults.is_anonymous, + CMDefaults.can_manage_chat, + CMDefaults.can_delete_messages, + CMDefaults.can_manage_video_chats, + CMDefaults.can_restrict_members, + CMDefaults.can_promote_members, + CMDefaults.can_change_info, + CMDefaults.can_invite_users, + CMDefaults.can_post_stories, + CMDefaults.can_edit_stories, + CMDefaults.can_delete_stories, + CMDefaults.can_post_messages, + CMDefaults.can_edit_messages, + CMDefaults.can_pin_messages, + CMDefaults.can_manage_topics, + CMDefaults.custom_title, + ) + + +def chat_member_member(): + return ChatMemberMember(CMDefaults.user, until_date=CMDefaults.until_date) + + +def chat_member_restricted(): + return ChatMemberRestricted( + CMDefaults.user, + CMDefaults.is_member, + CMDefaults.can_change_info, + CMDefaults.can_invite_users, + CMDefaults.can_pin_messages, + CMDefaults.can_send_messages, + CMDefaults.can_send_polls, + CMDefaults.can_send_other_messages, + CMDefaults.can_add_web_page_previews, + CMDefaults.can_manage_topics, + CMDefaults.until_date, + CMDefaults.can_send_audios, + CMDefaults.can_send_documents, + CMDefaults.can_send_photos, + CMDefaults.can_send_videos, + CMDefaults.can_send_video_notes, + CMDefaults.can_send_voice_notes, + ) + + +def chat_member_left(): + return ChatMemberLeft(CMDefaults.user) + + +def chat_member_banned(): + return ChatMemberBanned(CMDefaults.user, CMDefaults.until_date) + + +def make_json_dict(instance: ChatMember, include_optional_args: bool = False) -> dict: + """Used to make the json dict which we use for testing de_json. Similar to iter_args()""" + json_dict = {"status": instance.status} + sig = inspect.signature(instance.__class__.__init__) + + for param in sig.parameters.values(): + if param.name in ignored: # ignore irrelevant params + continue + + val = getattr(instance, param.name) + # Compulsory args- + if param.default is inspect.Parameter.empty: + if hasattr(val, "to_dict"): # convert the user object or any future ones to dict. + val = val.to_dict() + json_dict[param.name] = val + + # If we want to test all args (for de_json) + # or if the param is optional but for backwards compatability + elif ( + param.default is not inspect.Parameter.empty + and include_optional_args + or param.name in ["can_delete_stories", "can_post_stories", "can_edit_stories"] + ): + json_dict[param.name] = val + return json_dict + + +def iter_args(instance: ChatMember, de_json_inst: ChatMember, include_optional: bool = False): + """ + We accept both the regular instance and de_json created instance and iterate over them for + easy one line testing later one. + """ + yield instance.status, de_json_inst.status # yield this here cause it's not available in sig. + + sig = inspect.signature(instance.__class__.__init__) + for param in sig.parameters.values(): + if param.name in ignored: + continue + inst_at, json_at = getattr(instance, param.name), getattr(de_json_inst, param.name) + if isinstance(json_at, datetime.datetime): # Convert datetime to int + json_at = to_timestamp(json_at) + if ( + param.default is not inspect.Parameter.empty and include_optional + ) or param.default is inspect.Parameter.empty: + yield inst_at, json_at + + +@pytest.fixture +def chat_member_type(request): + return request.param() + + +@pytest.mark.parametrize( + "chat_member_type", + [ + chat_member_owner, + chat_member_administrator, + chat_member_member, + chat_member_restricted, + chat_member_left, + chat_member_banned, + ], + indirect=True, +) +class TestChatMemberTypesWithoutRequest: + def test_slot_behaviour(self, chat_member_type): + inst = chat_member_type + for attr in inst.__slots__: + assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" + + def test_de_json_required_args(self, bot, chat_member_type): + cls = chat_member_type.__class__ + assert cls.de_json({}, bot) is None + + json_dict = make_json_dict(chat_member_type) + const_chat_member = ChatMember.de_json(json_dict, bot) + assert const_chat_member.api_kwargs == {} + + assert isinstance(const_chat_member, ChatMember) + assert isinstance(const_chat_member, cls) + for chat_mem_type_at, const_chat_mem_at in iter_args(chat_member_type, const_chat_member): + assert chat_mem_type_at == const_chat_mem_at + + def test_de_json_all_args(self, bot, chat_member_type): + json_dict = make_json_dict(chat_member_type, include_optional_args=True) + const_chat_member = ChatMember.de_json(json_dict, bot) + assert const_chat_member.api_kwargs == {} + + assert isinstance(const_chat_member, ChatMember) + assert isinstance(const_chat_member, chat_member_type.__class__) + for c_mem_type_at, const_c_mem_at in iter_args(chat_member_type, const_chat_member, True): + assert c_mem_type_at == const_c_mem_at + + def test_de_json_chatmemberbanned_localization(self, chat_member_type, tz_bot, bot, raw_bot): + # We only test two classes because the other three don't have datetimes in them. + if isinstance( + chat_member_type, (ChatMemberBanned, ChatMemberRestricted, ChatMemberMember) + ): + json_dict = make_json_dict(chat_member_type, include_optional_args=True) + chatmember_raw = ChatMember.de_json(json_dict, raw_bot) + chatmember_bot = ChatMember.de_json(json_dict, bot) + chatmember_tz = ChatMember.de_json(json_dict, tz_bot) + + # comparing utcoffsets because comparing timezones is unpredicatable + chatmember_offset = chatmember_tz.until_date.utcoffset() + tz_bot_offset = tz_bot.defaults.tzinfo.utcoffset( + chatmember_tz.until_date.replace(tzinfo=None) + ) + + assert chatmember_raw.until_date.tzinfo == UTC + assert chatmember_bot.until_date.tzinfo == UTC + assert chatmember_offset == tz_bot_offset + + def test_de_json_invalid_status(self, chat_member_type, bot): + json_dict = {"status": "invalid", "user": CMDefaults.user.to_dict()} + chat_member_type = ChatMember.de_json(json_dict, bot) + + assert type(chat_member_type) is ChatMember + assert chat_member_type.status == "invalid" + + def test_de_json_subclass(self, chat_member_type, bot, chat_id): + """This makes sure that e.g. ChatMemberAdministrator(data, bot) never returns a + ChatMemberBanned instance.""" + cls = chat_member_type.__class__ + json_dict = make_json_dict(chat_member_type, True) + assert type(cls.de_json(json_dict, bot)) is cls + + def test_to_dict(self, chat_member_type): + chat_member_dict = chat_member_type.to_dict() + + assert isinstance(chat_member_dict, dict) + assert chat_member_dict["status"] == chat_member_type.status + assert chat_member_dict["user"] == chat_member_type.user.to_dict() + + for slot in chat_member_type.__slots__: # additional verification for the optional args + assert getattr(chat_member_type, slot) == chat_member_dict[slot] + + def test_chat_member_restricted_api_kwargs(self, chat_member_type): + json_dict = make_json_dict(chat_member_restricted()) + json_dict["can_send_media_messages"] = "can_send_media_messages" + chat_member_restricted_instance = ChatMember.de_json(json_dict, None) + assert chat_member_restricted_instance.api_kwargs == { + "can_send_media_messages": "can_send_media_messages", + } + + def test_equality(self, chat_member_type): + a = ChatMember(status="status", user=CMDefaults.user) + b = ChatMember(status="status", user=CMDefaults.user) + c = chat_member_type + d = deepcopy(chat_member_type) + e = Dice(4, "emoji") + + assert a == b + assert hash(a) == hash(b) + + assert a != c + assert hash(a) != hash(c) + + assert a != d + assert hash(a) != hash(d) + + assert a != e + assert hash(a) != hash(e) + + assert c == d + assert hash(c) == hash(d) + + assert c != e + assert hash(c) != hash(e) diff --git a/test_chatmemberupdated.py b/test_chatmemberupdated.py new file mode 100644 index 0000000000000000000000000000000000000000..33b07863a5a95a1c7918638c5aee3aaef0ba6005 --- /dev/null +++ b/test_chatmemberupdated.py @@ -0,0 +1,331 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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/]. +import datetime +import inspect + +import pytest + +from telegram import ( + Chat, + ChatInviteLink, + ChatMember, + ChatMemberAdministrator, + ChatMemberBanned, + ChatMemberOwner, + ChatMemberUpdated, + User, +) +from telegram._utils.datetime import UTC, to_timestamp +from tests.auxil.slots import mro_slots + + +@pytest.fixture(scope="module") +def user(): + return User(1, "First name", False) + + +@pytest.fixture(scope="module") +def chat(): + return Chat(1, Chat.SUPERGROUP, "Chat") + + +@pytest.fixture(scope="module") +def old_chat_member(user): + return ChatMember(user, ChatMemberUpdatedTestBase.old_status) + + +@pytest.fixture(scope="module") +def new_chat_member(user): + return ChatMemberAdministrator( + user, + True, + True, + True, + True, + True, + True, + True, + True, + True, + True, + True, + True, + custom_title=ChatMemberUpdatedTestBase.new_status, + ) + + +@pytest.fixture(scope="module") +def time(): + return datetime.datetime.now(tz=UTC) + + +@pytest.fixture(scope="module") +def invite_link(user): + return ChatInviteLink("link", user, False, True, True) + + +@pytest.fixture(scope="module") +def chat_member_updated(user, chat, old_chat_member, new_chat_member, invite_link, time): + return ChatMemberUpdated( + chat, user, time, old_chat_member, new_chat_member, invite_link, True, True + ) + + +class ChatMemberUpdatedTestBase: + old_status = ChatMember.MEMBER + new_status = ChatMember.ADMINISTRATOR + + +class TestChatMemberUpdatedWithoutRequest(ChatMemberUpdatedTestBase): + def test_slot_behaviour(self, chat_member_updated): + action = chat_member_updated + for attr in action.__slots__: + assert getattr(action, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(action)) == len(set(mro_slots(action))), "duplicate slot" + + def test_de_json_required_args(self, bot, user, chat, old_chat_member, new_chat_member, time): + json_dict = { + "chat": chat.to_dict(), + "from": user.to_dict(), + "date": to_timestamp(time), + "old_chat_member": old_chat_member.to_dict(), + "new_chat_member": new_chat_member.to_dict(), + } + + chat_member_updated = ChatMemberUpdated.de_json(json_dict, bot) + assert chat_member_updated.api_kwargs == {} + + assert chat_member_updated.chat == chat + assert chat_member_updated.from_user == user + assert abs(chat_member_updated.date - time) < datetime.timedelta(seconds=1) + assert to_timestamp(chat_member_updated.date) == to_timestamp(time) + assert chat_member_updated.old_chat_member == old_chat_member + assert chat_member_updated.new_chat_member == new_chat_member + assert chat_member_updated.invite_link is None + assert chat_member_updated.via_chat_folder_invite_link is None + + def test_de_json_all_args( + self, bot, user, time, invite_link, chat, old_chat_member, new_chat_member + ): + json_dict = { + "chat": chat.to_dict(), + "from": user.to_dict(), + "date": to_timestamp(time), + "old_chat_member": old_chat_member.to_dict(), + "new_chat_member": new_chat_member.to_dict(), + "invite_link": invite_link.to_dict(), + "via_chat_folder_invite_link": True, + "via_join_request": True, + } + + chat_member_updated = ChatMemberUpdated.de_json(json_dict, bot) + assert chat_member_updated.api_kwargs == {} + + assert chat_member_updated.chat == chat + assert chat_member_updated.from_user == user + assert abs(chat_member_updated.date - time) < datetime.timedelta(seconds=1) + assert to_timestamp(chat_member_updated.date) == to_timestamp(time) + assert chat_member_updated.old_chat_member == old_chat_member + assert chat_member_updated.new_chat_member == new_chat_member + assert chat_member_updated.invite_link == invite_link + assert chat_member_updated.via_chat_folder_invite_link is True + assert chat_member_updated.via_join_request is True + + def test_de_json_localization( + self, bot, raw_bot, tz_bot, user, chat, old_chat_member, new_chat_member, time, invite_link + ): + json_dict = { + "chat": chat.to_dict(), + "from": user.to_dict(), + "date": to_timestamp(time), + "old_chat_member": old_chat_member.to_dict(), + "new_chat_member": new_chat_member.to_dict(), + "invite_link": invite_link.to_dict(), + } + + chat_member_updated_bot = ChatMemberUpdated.de_json(json_dict, bot) + chat_member_updated_raw = ChatMemberUpdated.de_json(json_dict, raw_bot) + chat_member_updated_tz = ChatMemberUpdated.de_json(json_dict, tz_bot) + + # comparing utcoffsets because comparing timezones is unpredicatable + message_offset = chat_member_updated_tz.date.utcoffset() + tz_bot_offset = tz_bot.defaults.tzinfo.utcoffset( + chat_member_updated_tz.date.replace(tzinfo=None) + ) + + assert chat_member_updated_raw.date.tzinfo == UTC + assert chat_member_updated_bot.date.tzinfo == UTC + assert message_offset == tz_bot_offset + + def test_to_dict(self, chat_member_updated): + chat_member_updated_dict = chat_member_updated.to_dict() + assert isinstance(chat_member_updated_dict, dict) + assert chat_member_updated_dict["chat"] == chat_member_updated.chat.to_dict() + assert chat_member_updated_dict["from"] == chat_member_updated.from_user.to_dict() + assert chat_member_updated_dict["date"] == to_timestamp(chat_member_updated.date) + assert ( + chat_member_updated_dict["old_chat_member"] + == chat_member_updated.old_chat_member.to_dict() + ) + assert ( + chat_member_updated_dict["new_chat_member"] + == chat_member_updated.new_chat_member.to_dict() + ) + assert chat_member_updated_dict["invite_link"] == chat_member_updated.invite_link.to_dict() + assert ( + chat_member_updated_dict["via_chat_folder_invite_link"] + == chat_member_updated.via_chat_folder_invite_link + ) + assert chat_member_updated_dict["via_join_request"] == chat_member_updated.via_join_request + + def test_equality(self, time, old_chat_member, new_chat_member, invite_link): + a = ChatMemberUpdated( + Chat(1, "chat"), + User(1, "", False), + time, + old_chat_member, + new_chat_member, + invite_link, + ) + b = ChatMemberUpdated( + Chat(1, "chat"), User(1, "", False), time, old_chat_member, new_chat_member + ) + # wrong date + c = ChatMemberUpdated( + Chat(1, "chat"), + User(1, "", False), + time + datetime.timedelta(hours=1), + old_chat_member, + new_chat_member, + ) + # wrong chat & form_user + d = ChatMemberUpdated( + Chat(42, "wrong_chat"), + User(42, "wrong_user", False), + time, + old_chat_member, + new_chat_member, + ) + # wrong old_chat_member + e = ChatMemberUpdated( + Chat(1, "chat"), + User(1, "", False), + time, + ChatMember(User(1, "", False), ChatMember.OWNER), + new_chat_member, + ) + # wrong new_chat_member + f = ChatMemberUpdated( + Chat(1, "chat"), + User(1, "", False), + time, + old_chat_member, + ChatMember(User(1, "", False), ChatMember.OWNER), + ) + # wrong type + g = ChatMember(User(1, "", False), ChatMember.OWNER) + + assert a == b + assert hash(a) == hash(b) + assert a is not b + + for other in [c, d, e, f, g]: + assert a != other + assert hash(a) != hash(other) + + def test_difference_required(self, user, chat): + old_chat_member = ChatMember(user, "old_status") + new_chat_member = ChatMember(user, "new_status") + chat_member_updated = ChatMemberUpdated( + chat, user, datetime.datetime.utcnow(), old_chat_member, new_chat_member + ) + assert chat_member_updated.difference() == {"status": ("old_status", "new_status")} + + # We deliberately change an optional argument here to make sure that comparison doesn't + # just happens by id/required args + new_user = User(1, "First name", False, last_name="last name") + new_chat_member = ChatMember(new_user, "new_status") + chat_member_updated = ChatMemberUpdated( + chat, user, datetime.datetime.utcnow(), old_chat_member, new_chat_member + ) + assert chat_member_updated.difference() == { + "status": ("old_status", "new_status"), + "user": (user, new_user), + } + + @pytest.mark.parametrize( + "optional_attribute", + # This gives the names of all optional arguments of ChatMember + # skipping stories names because they aren't optional even though we pretend they are + [ + name + for name, param in inspect.signature(ChatMemberAdministrator).parameters.items() + if name + not in [ + "self", + "api_kwargs", + "can_delete_stories", + "can_post_stories", + "can_edit_stories", + ] + and param.default != inspect.Parameter.empty + ], + ) + def test_difference_optionals(self, optional_attribute, user, chat): + # We test with ChatMemberAdministrator, since that's currently the only interesting class + # with optional arguments + old_value = "old_value" + new_value = "new_value" + trues = tuple(True for _ in range(9)) + old_chat_member = ChatMemberAdministrator( + user, + *trues, + **{optional_attribute: old_value}, + can_delete_stories=True, + can_edit_stories=True, + can_post_stories=True, + ) + new_chat_member = ChatMemberAdministrator( + user, + *trues, + **{optional_attribute: new_value}, + can_delete_stories=True, + can_edit_stories=True, + can_post_stories=True, + ) + chat_member_updated = ChatMemberUpdated( + chat, user, datetime.datetime.utcnow(), old_chat_member, new_chat_member + ) + assert chat_member_updated.difference() == {optional_attribute: (old_value, new_value)} + + def test_difference_different_classes(self, user, chat): + old_chat_member = ChatMemberOwner(user=user, is_anonymous=False) + new_chat_member = ChatMemberBanned(user=user, until_date=datetime.datetime(2021, 1, 1)) + chat_member_updated = ChatMemberUpdated( + chat=chat, + from_user=user, + date=datetime.datetime.utcnow(), + old_chat_member=old_chat_member, + new_chat_member=new_chat_member, + ) + diff = chat_member_updated.difference() + assert diff.pop("is_anonymous") == (False, None) + assert diff.pop("until_date") == (None, datetime.datetime(2021, 1, 1)) + assert diff.pop("status") == (ChatMember.OWNER, ChatMember.BANNED) + assert diff == {} diff --git a/test_chatpermissions.py b/test_chatpermissions.py new file mode 100644 index 0000000000000000000000000000000000000000..79b6bab8081c47d66fe195812a72f170c069b60e --- /dev/null +++ b/test_chatpermissions.py @@ -0,0 +1,207 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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/]. + +import pytest + +from telegram import ChatPermissions, User +from tests.auxil.slots import mro_slots + + +@pytest.fixture(scope="module") +def chat_permissions(): + return ChatPermissions( + can_send_messages=True, + can_send_polls=True, + can_send_other_messages=True, + can_add_web_page_previews=True, + can_change_info=True, + can_invite_users=True, + can_pin_messages=True, + can_manage_topics=True, + can_send_audios=True, + can_send_documents=True, + can_send_photos=True, + can_send_videos=True, + can_send_video_notes=True, + can_send_voice_notes=True, + ) + + +class ChatPermissionsTestBase: + can_send_messages = True + can_send_polls = True + can_send_other_messages = False + can_add_web_page_previews = False + can_change_info = False + can_invite_users = None + can_pin_messages = None + can_manage_topics = None + can_send_audios = True + can_send_documents = False + can_send_photos = None + can_send_videos = True + can_send_video_notes = False + can_send_voice_notes = None + + +class TestChatPermissionsWithoutRequest(ChatPermissionsTestBase): + def test_slot_behaviour(self, chat_permissions): + inst = chat_permissions + for attr in inst.__slots__: + assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" + + def test_de_json(self, bot): + json_dict = { + "can_send_messages": self.can_send_messages, + "can_send_media_messages": "can_send_media_messages", + "can_send_polls": self.can_send_polls, + "can_send_other_messages": self.can_send_other_messages, + "can_add_web_page_previews": self.can_add_web_page_previews, + "can_change_info": self.can_change_info, + "can_invite_users": self.can_invite_users, + "can_pin_messages": self.can_pin_messages, + "can_send_audios": self.can_send_audios, + "can_send_documents": self.can_send_documents, + "can_send_photos": self.can_send_photos, + "can_send_videos": self.can_send_videos, + "can_send_video_notes": self.can_send_video_notes, + "can_send_voice_notes": self.can_send_voice_notes, + } + permissions = ChatPermissions.de_json(json_dict, bot) + assert permissions.api_kwargs == {"can_send_media_messages": "can_send_media_messages"} + + assert permissions.can_send_messages == self.can_send_messages + assert permissions.can_send_polls == self.can_send_polls + assert permissions.can_send_other_messages == self.can_send_other_messages + assert permissions.can_add_web_page_previews == self.can_add_web_page_previews + assert permissions.can_change_info == self.can_change_info + assert permissions.can_invite_users == self.can_invite_users + assert permissions.can_pin_messages == self.can_pin_messages + assert permissions.can_manage_topics == self.can_manage_topics + assert permissions.can_send_audios == self.can_send_audios + assert permissions.can_send_documents == self.can_send_documents + assert permissions.can_send_photos == self.can_send_photos + assert permissions.can_send_videos == self.can_send_videos + assert permissions.can_send_video_notes == self.can_send_video_notes + assert permissions.can_send_voice_notes == self.can_send_voice_notes + + def test_to_dict(self, chat_permissions): + permissions_dict = chat_permissions.to_dict() + + assert isinstance(permissions_dict, dict) + assert permissions_dict["can_send_messages"] == chat_permissions.can_send_messages + assert permissions_dict["can_send_polls"] == chat_permissions.can_send_polls + assert ( + permissions_dict["can_send_other_messages"] == chat_permissions.can_send_other_messages + ) + assert ( + permissions_dict["can_add_web_page_previews"] + == chat_permissions.can_add_web_page_previews + ) + assert permissions_dict["can_change_info"] == chat_permissions.can_change_info + assert permissions_dict["can_invite_users"] == chat_permissions.can_invite_users + assert permissions_dict["can_pin_messages"] == chat_permissions.can_pin_messages + assert permissions_dict["can_manage_topics"] == chat_permissions.can_manage_topics + assert permissions_dict["can_send_audios"] == chat_permissions.can_send_audios + assert permissions_dict["can_send_documents"] == chat_permissions.can_send_documents + assert permissions_dict["can_send_photos"] == chat_permissions.can_send_photos + assert permissions_dict["can_send_videos"] == chat_permissions.can_send_videos + assert permissions_dict["can_send_video_notes"] == chat_permissions.can_send_video_notes + assert permissions_dict["can_send_voice_notes"] == chat_permissions.can_send_voice_notes + + def test_equality(self): + a = ChatPermissions( + can_send_messages=True, + can_send_polls=True, + can_send_other_messages=False, + ) + b = ChatPermissions( + can_send_polls=True, + can_send_other_messages=False, + can_send_messages=True, + ) + c = ChatPermissions( + can_send_messages=False, + can_send_polls=True, + can_send_other_messages=False, + ) + d = User(123, "", False) + e = ChatPermissions( + can_send_messages=True, + can_send_polls=True, + can_send_other_messages=False, + can_send_audios=True, + can_send_documents=True, + can_send_photos=True, + can_send_videos=True, + can_send_video_notes=True, + can_send_voice_notes=True, + ) + f = ChatPermissions( + can_send_messages=True, + can_send_polls=True, + can_send_other_messages=False, + can_send_audios=True, + can_send_documents=True, + can_send_photos=True, + can_send_videos=True, + can_send_video_notes=True, + can_send_voice_notes=True, + ) + + assert a == b + assert hash(a) == hash(b) + assert a is not b + + assert a != c + assert hash(a) != hash(c) + + assert a != d + assert hash(a) != hash(d) + + assert a != e + assert hash(a) != hash(e) + + assert e == f + assert hash(e) == hash(f) + + def test_all_permissions(self): + f = ChatPermissions() + t = ChatPermissions.all_permissions() + # if the dirs are the same, the attributes will all be there + assert dir(f) == dir(t) + # now we just need to check that all attributes are True. _id_attrs returns all values, + # if a new one is added without defaulting to True, this will fail + for key in t.__slots__: + assert t[key] is True + # and as a finisher, make sure the default is different. + assert f != t + + def test_no_permissions(self): + f = ChatPermissions() + t = ChatPermissions.no_permissions() + # if the dirs are the same, the attributes will all be there + assert dir(f) == dir(t) + # now we just need to check that all attributes are True. _id_attrs returns all values, + # if a new one is added without defaulting to False, this will fail + for key in t.__slots__: + assert t[key] is False + # and as a finisher, make sure the default is different. + assert f != t diff --git a/test_choseninlineresult.py b/test_choseninlineresult.py new file mode 100644 index 0000000000000000000000000000000000000000..2b53b6847708def3da003e4d664ac2631d83229f --- /dev/null +++ b/test_choseninlineresult.py @@ -0,0 +1,105 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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/]. + +import pytest + +from telegram import ChosenInlineResult, Location, User, Voice +from tests.auxil.slots import mro_slots + + +@pytest.fixture(scope="module") +def user(): + user = User(1, "First name", False) + user._unfreeze() + return user + + +@pytest.fixture(scope="module") +def chosen_inline_result(user): + return ChosenInlineResult( + ChosenInlineResultTestBase.result_id, user, ChosenInlineResultTestBase.query + ) + + +class ChosenInlineResultTestBase: + result_id = "result id" + query = "query text" + + +class TestChosenInlineResultWithoutRequest(ChosenInlineResultTestBase): + def test_slot_behaviour(self, chosen_inline_result): + inst = chosen_inline_result + for attr in inst.__slots__: + assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" + + def test_de_json_required(self, bot, user): + json_dict = {"result_id": self.result_id, "from": user.to_dict(), "query": self.query} + result = ChosenInlineResult.de_json(json_dict, bot) + assert result.api_kwargs == {} + + assert result.result_id == self.result_id + assert result.from_user == user + assert result.query == self.query + + def test_de_json_all(self, bot, user): + loc = Location(-42.003, 34.004) + json_dict = { + "result_id": self.result_id, + "from": user.to_dict(), + "query": self.query, + "location": loc.to_dict(), + "inline_message_id": "a random id", + } + result = ChosenInlineResult.de_json(json_dict, bot) + assert result.api_kwargs == {} + + assert result.result_id == self.result_id + assert result.from_user == user + assert result.query == self.query + assert result.location == loc + assert result.inline_message_id == "a random id" + + def test_to_dict(self, chosen_inline_result): + chosen_inline_result_dict = chosen_inline_result.to_dict() + + assert isinstance(chosen_inline_result_dict, dict) + assert chosen_inline_result_dict["result_id"] == chosen_inline_result.result_id + assert chosen_inline_result_dict["from"] == chosen_inline_result.from_user.to_dict() + assert chosen_inline_result_dict["query"] == chosen_inline_result.query + + def test_equality(self, user): + a = ChosenInlineResult(self.result_id, user, "Query", "") + b = ChosenInlineResult(self.result_id, user, "Query", "") + c = ChosenInlineResult(self.result_id, user, "", "") + d = ChosenInlineResult("", user, "Query", "") + e = Voice(self.result_id, "unique_id", 0) + + assert a == b + assert hash(a) == hash(b) + assert a is not b + + assert a == c + assert hash(a) == hash(c) + + assert a != d + assert hash(a) != hash(d) + + assert a != e + assert hash(a) != hash(e) diff --git a/test_constants.py b/test_constants.py new file mode 100644 index 0000000000000000000000000000000000000000..dc76bea3aeffa77069daeaf329b8543f0f79541a --- /dev/null +++ b/test_constants.py @@ -0,0 +1,262 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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/]. +import asyncio +import inspect +import json +import re + +import pytest + +from telegram import Message, constants +from telegram._utils.enum import IntEnum, StringEnum +from telegram.error import BadRequest +from tests.auxil.build_messages import make_message +from tests.auxil.files import data_file +from tests.auxil.string_manipulation import to_snake_case + + +class StrEnumTest(StringEnum): + FOO = "foo" + BAR = "bar" + + +class IntEnumTest(IntEnum): + FOO = 1 + BAR = 2 + + +class TestConstantsWithoutRequest: + """Also test _utils.enum.StringEnum on the fly because tg.constants is currently the only + place where that class is used.""" + + def test__all__(self): + expected = { + key + for key, member in constants.__dict__.items() + if ( + not key.startswith("_") + # exclude imported stuff + and getattr(member, "__module__", "telegram.constants") == "telegram.constants" + and key not in ("sys", "datetime") + ) + } + actual = set(constants.__all__) + assert ( + actual == expected + ), f"Members {expected - actual} were not listed in constants.__all__" + + def test_message_attachment_type(self): + assert all( + getattr(constants.MessageType, x.name, False) for x in constants.MessageAttachmentType + ), "All MessageAttachmentType members should be in MessageType" + + def test_to_json(self): + assert json.dumps(StrEnumTest.FOO) == json.dumps("foo") + assert json.dumps(IntEnumTest.FOO) == json.dumps(1) + + def test_string_representation(self): + # test __repr__ + assert repr(StrEnumTest.FOO) == "" + + # test __format__ + assert f"{StrEnumTest.FOO} this {StrEnumTest.BAR}" == "foo this bar" + assert f"{StrEnumTest.FOO:*^10}" == "***foo****" + + # test __str__ + assert str(StrEnumTest.FOO) == "foo" + + def test_int_representation(self): + # test __repr__ + assert repr(IntEnumTest.FOO) == "" + # test __format__ + assert f"{IntEnumTest.FOO}/0 is undefined!" == "1/0 is undefined!" + assert f"{IntEnumTest.FOO:*^10}" == "****1*****" + # test __str__ + assert str(IntEnumTest.FOO) == "1" + + def test_string_inheritance(self): + assert isinstance(StrEnumTest.FOO, str) + assert StrEnumTest.FOO + StrEnumTest.BAR == "foobar" + assert StrEnumTest.FOO.replace("o", "a") == "faa" + + assert StrEnumTest.FOO == StrEnumTest.FOO + assert StrEnumTest.FOO == "foo" + assert StrEnumTest.FOO != StrEnumTest.BAR + assert StrEnumTest.FOO != "bar" + assert object() != StrEnumTest.FOO + + assert hash(StrEnumTest.FOO) == hash("foo") + + def test_int_inheritance(self): + assert isinstance(IntEnumTest.FOO, int) + assert IntEnumTest.FOO + IntEnumTest.BAR == 3 + + assert IntEnumTest.FOO == IntEnumTest.FOO + assert IntEnumTest.FOO == 1 + assert IntEnumTest.FOO != IntEnumTest.BAR + assert IntEnumTest.FOO != 2 + assert object() != IntEnumTest.FOO + + assert hash(IntEnumTest.FOO) == hash(1) + + def test_bot_api_version_and_info(self): + assert str(constants.BOT_API_VERSION_INFO) == constants.BOT_API_VERSION + assert ( + tuple(int(x) for x in constants.BOT_API_VERSION.split(".")) + == constants.BOT_API_VERSION_INFO + ) + + def test_bot_api_version_info(self): + vi = constants.BOT_API_VERSION_INFO + assert isinstance(vi, tuple) + assert repr(vi) == f"BotAPIVersion(major={vi[0]}, minor={vi[1]})" + assert vi == (vi[0], vi[1]) + assert not (vi < (vi[0], vi[1])) + assert vi < (vi[0], vi[1] + 1) + assert vi < (vi[0] + 1, vi[1]) + assert vi < (vi[0] + 1, vi[1] + 1) + assert vi[0] == vi.major + assert vi[1] == vi.minor + + @staticmethod + def is_type_attribute(name: str) -> bool: + # Return False if the attribute doesn't generate a message type, i.e. only message + # metadata. Manually excluding a lot of attributes here is a bit of work, but it makes + # sure that we don't miss any new message types in the future. + patters = { + "(text|caption)_(markdown|html)", + "caption_(entities|html|markdown)", + "(edit_)?date", + "forward_", + "has_", + } + + if any(re.match(pattern, name) for pattern in patters): + return False + return name not in { + "author_signature", + "api_kwargs", + "caption", + "chat", + "chat_id", + "effective_attachment", + "entities", + "from_user", + "id", + "is_automatic_forward", + "is_topic_message", + "link", + "link_preview_options", + "media_group_id", + "message_id", + "message_thread_id", + "migrate_from_chat_id", + "reply_markup", + "reply_to_message", + "sender_chat", + "is_accessible", + "quote", + "external_reply", + "via_bot", + "is_from_offline", + "show_caption_above_media", + } + + @pytest.mark.parametrize( + "attribute", + [ + name + for name, _ in inspect.getmembers( + make_message("test"), lambda x: not inspect.isroutine(x) + ) + ], + ) + def test_message_type_completeness(self, attribute): + if attribute.startswith("_") or not self.is_type_attribute(attribute): + return + + assert hasattr(constants.MessageType, attribute.upper()), ( + f"Missing MessageType.{attribute}. Please also check if this should be present in " + f"MessageAttachmentType." + ) + + @pytest.mark.parametrize("member", constants.MessageType) + def test_message_type_completeness_reverse(self, member): + assert self.is_type_attribute( + member.value + ), f"Additional member {member} in MessageType that should not be a message type" + + @pytest.mark.parametrize("member", constants.MessageAttachmentType) + def test_message_attachment_type_completeness(self, member): + try: + constants.MessageType(member) + except ValueError: + pytest.fail(f"Missing MessageType for {member}") + + def test_message_attachment_type_completeness_reverse(self): + # Getting the type hints of a property is a bit tricky, so we instead parse the docstring + # for now + for match in re.finditer(r"`telegram.(\w+)`", Message.effective_attachment.__doc__): + name = to_snake_case(match.group(1)) + if name == "photo_size": + name = "photo" + if name == "paid_media_info": + name = "paid_media" + try: + constants.MessageAttachmentType(name) + except ValueError: + pytest.fail(f"Missing MessageAttachmentType for {match.group(1)}") + + +class TestConstantsWithRequest: + async def test_max_message_length(self, bot, chat_id): + good_text = "a" * constants.MessageLimit.MAX_TEXT_LENGTH + bad_text = good_text + "Z" + tasks = asyncio.gather( + bot.send_message(chat_id, text=good_text), + bot.send_message(chat_id, text=bad_text), + return_exceptions=True, + ) + good_msg, bad_msg = await tasks + + if isinstance(good_msg, BaseException): + # handling xfails + raise good_msg + + assert good_msg.text == good_text + assert isinstance(bad_msg, BadRequest) + assert "Message is too long" in str(bad_msg) + + async def test_max_caption_length(self, bot, chat_id): + good_caption = "a" * constants.MessageLimit.CAPTION_LENGTH + bad_caption = good_caption + "Z" + tasks = asyncio.gather( + bot.send_photo(chat_id, data_file("telegram.png").read_bytes(), good_caption), + bot.send_photo(chat_id, data_file("telegram.png").read_bytes(), bad_caption), + return_exceptions=True, + ) + good_msg, bad_msg = await tasks + + if isinstance(good_msg, BaseException): + # handling xfails + raise good_msg + + assert good_msg.caption == good_caption + assert isinstance(bad_msg, BadRequest) + assert "Message caption is too long" in str(bad_msg) diff --git a/test_dice.py b/test_dice.py new file mode 100644 index 0000000000000000000000000000000000000000..df3c349d4d70319a512576b3104bb0108cdf1826 --- /dev/null +++ b/test_dice.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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/]. + +import pytest + +from telegram import BotCommand, Dice +from tests.auxil.slots import mro_slots + + +@pytest.fixture(scope="module", params=Dice.ALL_EMOJI) +def dice(request): + return Dice(value=5, emoji=request.param) + + +class DiceTestBase: + value = 4 + + +class TestDiceWithoutRequest(DiceTestBase): + def test_slot_behaviour(self, dice): + for attr in dice.__slots__: + assert getattr(dice, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(dice)) == len(set(mro_slots(dice))), "duplicate slot" + + @pytest.mark.parametrize("emoji", Dice.ALL_EMOJI) + def test_de_json(self, bot, emoji): + json_dict = {"value": self.value, "emoji": emoji} + dice = Dice.de_json(json_dict, bot) + assert dice.api_kwargs == {} + + assert dice.value == self.value + assert dice.emoji == emoji + assert Dice.de_json(None, bot) is None + + def test_to_dict(self, dice): + dice_dict = dice.to_dict() + + assert isinstance(dice_dict, dict) + assert dice_dict["value"] == dice.value + assert dice_dict["emoji"] == dice.emoji + + def test_equality(self): + a = Dice(3, "🎯") + b = Dice(3, "🎯") + c = Dice(3, "🎲") + d = Dice(4, "🎯") + e = BotCommand("start", "description") + + assert a == b + assert hash(a) == hash(b) + + assert a != c + assert hash(a) != hash(c) + + assert a != d + assert hash(a) != hash(d) + + assert a != e + assert hash(a) != hash(e) diff --git a/test_enum_types.py b/test_enum_types.py new file mode 100644 index 0000000000000000000000000000000000000000..947d5fd065563e96365b973b030dfd61c56da064 --- /dev/null +++ b/test_enum_types.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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/]. +import re +from pathlib import Path + +from telegram._utils.strings import TextEncoding + +telegram_root = Path(__file__).parent.parent / "telegram" +telegram_ext_root = telegram_root / "ext" +exclude_dirs = { + # We touch passport stuff only if strictly necessary. + telegram_root + / "_passport", +} + +exclude_patterns = { + re.compile(re.escape("self.type: ReactionType = type")), + re.compile(re.escape("self.type: BackgroundType = type")), +} + + +def test_types_are_converted_to_enum(): + """We want to convert all attributes of name "type" to an enum from telegram.constants. + Since we don't necessarily document this as type hint, we simply check this with a regex. + """ + pattern = re.compile(r"self\.type: [^=]+ = ([^\n]+)\n", re.MULTILINE) + + for path in telegram_root.rglob("*.py"): + if telegram_ext_root in path.parents or any( + exclude_dir in path.parents for exclude_dir in exclude_dirs + ): + # We don't check tg.ext. + continue + + text = path.read_text(encoding=TextEncoding.UTF_8) + for match in re.finditer(pattern, text): + if any(exclude_pattern.match(match.group(0)) for exclude_pattern in exclude_patterns): + continue + + assert match.group(1).startswith("enum.get_member") or match.group(1).startswith( + "get_member" + ), ( + f"`{match.group(1)}` in `{path}` does not seem to convert the type to an enum. " + f"Please fix this and also make sure to add a separate test to the classes test " + f"file." + ) diff --git a/test_error.py b/test_error.py new file mode 100644 index 0000000000000000000000000000000000000000..ef6d2ed61bdfd96480e5119e256495ff550a2097 --- /dev/null +++ b/test_error.py @@ -0,0 +1,196 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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/]. +import pickle +from collections import defaultdict + +import pytest + +from telegram.error import ( + BadRequest, + ChatMigrated, + Conflict, + EndPointNotFound, + Forbidden, + InvalidToken, + NetworkError, + PassportDecryptionError, + RetryAfter, + TelegramError, + TimedOut, +) +from telegram.ext import InvalidCallbackData +from tests.auxil.slots import mro_slots + + +class TestErrors: + def test_telegram_error(self): + with pytest.raises(TelegramError, match="^test message$"): + raise TelegramError("test message") + with pytest.raises(TelegramError, match="^Test message$"): + raise TelegramError("Error: test message") + with pytest.raises(TelegramError, match="^Test message$"): + raise TelegramError("[Error]: test message") + with pytest.raises(TelegramError, match="^Test message$"): + raise TelegramError("Bad Request: test message") + + def test_unauthorized(self): + with pytest.raises(Forbidden, match="test message"): + raise Forbidden("test message") + with pytest.raises(Forbidden, match="^Test message$"): + raise Forbidden("Error: test message") + with pytest.raises(Forbidden, match="^Test message$"): + raise Forbidden("[Error]: test message") + with pytest.raises(Forbidden, match="^Test message$"): + raise Forbidden("Bad Request: test message") + + def test_invalid_token(self): + with pytest.raises(InvalidToken, match="Invalid token"): + raise InvalidToken + + def test_network_error(self): + with pytest.raises(NetworkError, match="test message"): + raise NetworkError("test message") + with pytest.raises(NetworkError, match="^Test message$"): + raise NetworkError("Error: test message") + with pytest.raises(NetworkError, match="^Test message$"): + raise NetworkError("[Error]: test message") + with pytest.raises(NetworkError, match="^Test message$"): + raise NetworkError("Bad Request: test message") + + def test_bad_request(self): + with pytest.raises(BadRequest, match="test message"): + raise BadRequest("test message") + with pytest.raises(BadRequest, match="^Test message$"): + raise BadRequest("Error: test message") + with pytest.raises(BadRequest, match="^Test message$"): + raise BadRequest("[Error]: test message") + with pytest.raises(BadRequest, match="^Test message$"): + raise BadRequest("Bad Request: test message") + + def test_timed_out(self): + with pytest.raises(TimedOut, match="^Timed out$"): + raise TimedOut + + def test_chat_migrated(self): + with pytest.raises(ChatMigrated, match="New chat id: 1234") as e: + raise ChatMigrated(1234) + assert e.value.new_chat_id == 1234 + + def test_retry_after(self): + with pytest.raises(RetryAfter, match="Flood control exceeded. Retry in 12 seconds"): + raise RetryAfter(12) + + def test_conflict(self): + with pytest.raises(Conflict, match="Something something."): + raise Conflict("Something something.") + + @pytest.mark.parametrize( + ("exception", "attributes"), + [ + (TelegramError("test message"), ["message"]), + (Forbidden("test message"), ["message"]), + (InvalidToken(), ["message"]), + (NetworkError("test message"), ["message"]), + (BadRequest("test message"), ["message"]), + (TimedOut(), ["message"]), + (ChatMigrated(1234), ["message", "new_chat_id"]), + (RetryAfter(12), ["message", "retry_after"]), + (Conflict("test message"), ["message"]), + (PassportDecryptionError("test message"), ["message"]), + (InvalidCallbackData("test data"), ["callback_data"]), + (EndPointNotFound("endPoint"), ["message"]), + ], + ) + def test_errors_pickling(self, exception, attributes): + pickled = pickle.dumps(exception) + unpickled = pickle.loads(pickled) + assert type(unpickled) is type(exception) + assert str(unpickled) == str(exception) + + for attribute in attributes: + assert getattr(unpickled, attribute) == getattr(exception, attribute) + + @pytest.mark.parametrize( + "inst", + [ + (TelegramError("test message")), + (Forbidden("test message")), + (InvalidToken()), + (NetworkError("test message")), + (BadRequest("test message")), + (TimedOut()), + (ChatMigrated(1234)), + (RetryAfter(12)), + (Conflict("test message")), + (PassportDecryptionError("test message")), + (InvalidCallbackData("test data")), + (EndPointNotFound("test message")), + ], + ) + def test_slot_behaviour(self, inst): + for attr in inst.__slots__: + assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" + + def test_coverage(self): + """ + This test is only here to make sure that new errors will override __reduce__ and set + __slots__ properly. + Add the new error class to the below covered_subclasses dict, if it's covered in the above + test_errors_pickling and test_slots_behavior tests. + """ + + def make_assertion(cls): + assert set(cls.__subclasses__()) == covered_subclasses[cls] + for subcls in cls.__subclasses__(): + make_assertion(subcls) + + covered_subclasses = defaultdict(set) + covered_subclasses.update( + { + TelegramError: { + Forbidden, + InvalidToken, + NetworkError, + ChatMigrated, + RetryAfter, + Conflict, + PassportDecryptionError, + InvalidCallbackData, + EndPointNotFound, + }, + NetworkError: {BadRequest, TimedOut}, + } + ) + + make_assertion(TelegramError) + + def test_string_representations(self): + """We just randomly test a few of the subclasses - should suffice""" + e = TelegramError("This is a message") + assert repr(e) == "TelegramError('This is a message')" + assert str(e) == "This is a message" + + e = RetryAfter(42) + assert repr(e) == "RetryAfter('Flood control exceeded. Retry in 42 seconds')" + assert str(e) == "Flood control exceeded. Retry in 42 seconds" + + e = BadRequest("This is a message") + assert repr(e) == "BadRequest('This is a message')" + assert str(e) == "This is a message" diff --git a/test_forcereply.py b/test_forcereply.py new file mode 100644 index 0000000000000000000000000000000000000000..e4e2b5b5ddaf8a5382c779e6be367f03a7d07b36 --- /dev/null +++ b/test_forcereply.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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/]. + +import pytest + +from telegram import ForceReply, ReplyKeyboardRemove +from tests.auxil.slots import mro_slots + + +@pytest.fixture(scope="module") +def force_reply(): + return ForceReply(ForceReplyTestBase.selective, ForceReplyTestBase.input_field_placeholder) + + +class ForceReplyTestBase: + force_reply = True + selective = True + input_field_placeholder = "force replies can be annoying if not used properly" + + +class TestForceReplyWithoutRequest(ForceReplyTestBase): + def test_slot_behaviour(self, force_reply): + for attr in force_reply.__slots__: + assert getattr(force_reply, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(force_reply)) == len(set(mro_slots(force_reply))), "duplicate slot" + + def test_expected(self, force_reply): + assert force_reply.force_reply == self.force_reply + assert force_reply.selective == self.selective + assert force_reply.input_field_placeholder == self.input_field_placeholder + + def test_to_dict(self, force_reply): + force_reply_dict = force_reply.to_dict() + + assert isinstance(force_reply_dict, dict) + assert force_reply_dict["force_reply"] == force_reply.force_reply + assert force_reply_dict["selective"] == force_reply.selective + assert force_reply_dict["input_field_placeholder"] == force_reply.input_field_placeholder + + def test_equality(self): + a = ForceReply(True, "test") + b = ForceReply(False, "pass") + c = ForceReply(True) + d = ReplyKeyboardRemove() + + assert a != b + assert hash(a) != hash(b) + + assert a == c + assert hash(a) == hash(c) + + assert a != d + assert hash(a) != hash(d) + + +class TestForceReplyWithRequest(ForceReplyTestBase): + async def test_send_message_with_force_reply(self, bot, chat_id, force_reply): + message = await bot.send_message(chat_id, "text", reply_markup=force_reply) + assert message.text == "text" diff --git a/test_forum.py b/test_forum.py new file mode 100644 index 0000000000000000000000000000000000000000..1f143616ee91aa20a1d80095a981c33f19f454e3 --- /dev/null +++ b/test_forum.py @@ -0,0 +1,498 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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/]. +import asyncio +import datetime + +import pytest + +from telegram import ( + ForumTopic, + ForumTopicClosed, + ForumTopicCreated, + ForumTopicEdited, + ForumTopicReopened, + GeneralForumTopicHidden, + GeneralForumTopicUnhidden, + Sticker, +) +from telegram.error import BadRequest +from tests.auxil.slots import mro_slots + +TEST_MSG_TEXT = "Topics are forever" +TEST_TOPIC_ICON_COLOR = 0x6FB9F0 +TEST_TOPIC_NAME = "Sad bot true: real stories" + + +@pytest.fixture(scope="module") +async def emoji_id(bot): + emoji_sticker_list = await bot.get_forum_topic_icon_stickers() + first_sticker = emoji_sticker_list[0] + return first_sticker.custom_emoji_id + + +@pytest.fixture(scope="module") +async def forum_topic_object(forum_group_id, emoji_id): + return ForumTopic( + message_thread_id=forum_group_id, + name=TEST_TOPIC_NAME, + icon_color=TEST_TOPIC_ICON_COLOR, + icon_custom_emoji_id=emoji_id, + ) + + +@pytest.fixture +async def real_topic(bot, emoji_id, forum_group_id): + result = await bot.create_forum_topic( + chat_id=forum_group_id, + name=TEST_TOPIC_NAME, + icon_color=TEST_TOPIC_ICON_COLOR, + icon_custom_emoji_id=emoji_id, + ) + + yield result + + result = await bot.delete_forum_topic( + chat_id=forum_group_id, message_thread_id=result.message_thread_id + ) + assert result is True, "Topic was not deleted" + + +class TestForumTopicWithoutRequest: + def test_slot_behaviour(self, forum_topic_object): + inst = forum_topic_object + for attr in inst.__slots__: + assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" + + async def test_expected_values(self, emoji_id, forum_group_id, forum_topic_object): + assert forum_topic_object.message_thread_id == forum_group_id + assert forum_topic_object.icon_color == TEST_TOPIC_ICON_COLOR + assert forum_topic_object.name == TEST_TOPIC_NAME + assert forum_topic_object.icon_custom_emoji_id == emoji_id + + def test_de_json(self, bot, emoji_id, forum_group_id): + assert ForumTopic.de_json(None, bot=bot) is None + + json_dict = { + "message_thread_id": forum_group_id, + "name": TEST_TOPIC_NAME, + "icon_color": TEST_TOPIC_ICON_COLOR, + "icon_custom_emoji_id": emoji_id, + } + topic = ForumTopic.de_json(json_dict, bot) + assert topic.api_kwargs == {} + + assert topic.message_thread_id == forum_group_id + assert topic.icon_color == TEST_TOPIC_ICON_COLOR + assert topic.name == TEST_TOPIC_NAME + assert topic.icon_custom_emoji_id == emoji_id + + def test_to_dict(self, emoji_id, forum_group_id, forum_topic_object): + topic_dict = forum_topic_object.to_dict() + + assert isinstance(topic_dict, dict) + assert topic_dict["message_thread_id"] == forum_group_id + assert topic_dict["name"] == TEST_TOPIC_NAME + assert topic_dict["icon_color"] == TEST_TOPIC_ICON_COLOR + assert topic_dict["icon_custom_emoji_id"] == emoji_id + + def test_equality(self, emoji_id, forum_group_id): + a = ForumTopic( + message_thread_id=forum_group_id, + name=TEST_TOPIC_NAME, + icon_color=TEST_TOPIC_ICON_COLOR, + ) + b = ForumTopic( + message_thread_id=forum_group_id, + name=TEST_TOPIC_NAME, + icon_color=TEST_TOPIC_ICON_COLOR, + icon_custom_emoji_id=emoji_id, + ) + c = ForumTopic( + message_thread_id=forum_group_id, + name=f"{TEST_TOPIC_NAME}!", + icon_color=TEST_TOPIC_ICON_COLOR, + ) + d = ForumTopic( + message_thread_id=forum_group_id + 1, + name=TEST_TOPIC_NAME, + icon_color=TEST_TOPIC_ICON_COLOR, + ) + e = ForumTopic( + message_thread_id=forum_group_id, + name=TEST_TOPIC_NAME, + icon_color=0xFFD67E, + ) + + assert a == b + assert hash(a) == hash(b) + + assert a != c + assert hash(a) != hash(c) + + assert a != d + assert hash(a) != hash(d) + + assert a != e + assert hash(a) != hash(e) + + +class TestForumMethodsWithRequest: + async def test_create_forum_topic(self, real_topic): + result = real_topic + assert isinstance(result, ForumTopic) + assert result.name == TEST_TOPIC_NAME + assert result.message_thread_id + assert isinstance(result.icon_color, int) + assert isinstance(result.icon_custom_emoji_id, str) + + async def test_create_forum_topic_with_only_required_args(self, bot, forum_group_id): + result = await bot.create_forum_topic(chat_id=forum_group_id, name=TEST_TOPIC_NAME) + assert isinstance(result, ForumTopic) + assert result.name == TEST_TOPIC_NAME + assert result.message_thread_id + assert isinstance(result.icon_color, int) # color is still there though it was not passed + assert result.icon_custom_emoji_id is None + + result = await bot.delete_forum_topic( + chat_id=forum_group_id, message_thread_id=result.message_thread_id + ) + assert result is True, "Failed to delete forum topic" + + async def test_get_forum_topic_icon_stickers(self, bot): + emoji_sticker_list = await bot.get_forum_topic_icon_stickers() + first_sticker = emoji_sticker_list[0] + + assert first_sticker.emoji == "📰" + assert first_sticker.height == 512 + assert first_sticker.width == 512 + assert first_sticker.is_animated + assert not first_sticker.is_video + assert first_sticker.set_name == "Topics" + assert first_sticker.type == Sticker.CUSTOM_EMOJI + assert first_sticker.thumbnail.width == 128 + assert first_sticker.thumbnail.height == 128 + + # The following data of first item returned has changed in the past already, + # so check sizes loosely and ID's only by length of string + assert first_sticker.thumbnail.file_size in range(2000, 7000) + assert first_sticker.file_size in range(20000, 70000) + assert len(first_sticker.custom_emoji_id) == 19 + assert len(first_sticker.thumbnail.file_unique_id) == 16 + assert len(first_sticker.file_unique_id) == 15 + + async def test_edit_forum_topic(self, emoji_id, forum_group_id, bot, real_topic): + result = await bot.edit_forum_topic( + chat_id=forum_group_id, + message_thread_id=real_topic.message_thread_id, + name=f"{TEST_TOPIC_NAME}_EDITED", + icon_custom_emoji_id=emoji_id, + ) + assert result is True, "Failed to edit forum topic" + # no way of checking the edited name, just the boolean result + + async def test_send_message_to_topic(self, bot, forum_group_id, real_topic): + message_thread_id = real_topic.message_thread_id + + message = await bot.send_message( + chat_id=forum_group_id, text=TEST_MSG_TEXT, message_thread_id=message_thread_id + ) + + assert message.text == TEST_MSG_TEXT + assert message.is_topic_message is True + assert message.message_thread_id == message_thread_id + + async def test_close_and_reopen_forum_topic(self, bot, forum_group_id, real_topic): + message_thread_id = real_topic.message_thread_id + + result = await bot.close_forum_topic( + chat_id=forum_group_id, + message_thread_id=message_thread_id, + ) + assert result is True, "Failed to close forum topic" + # bot will still be able to send a message to a closed topic, so can't test anything like + # the inability to post to the topic + + result = await bot.reopen_forum_topic( + chat_id=forum_group_id, + message_thread_id=message_thread_id, + ) + assert result is True, "Failed to reopen forum topic" + + async def test_unpin_all_forum_topic_messages(self, bot, forum_group_id, real_topic): + # We need 2 or more pinned msgs for this to work, else we get Chat_not_modified error + message_thread_id = real_topic.message_thread_id + pin_msg_tasks = set() + + awaitables = { + bot.send_message(forum_group_id, TEST_MSG_TEXT, message_thread_id=message_thread_id) + for _ in range(2) + } + for coro in asyncio.as_completed(awaitables): + msg = await coro + pin_msg_tasks.add(asyncio.create_task(msg.pin())) + + assert all([await task for task in pin_msg_tasks]) is True, "Message(s) were not pinned" + + result = await bot.unpin_all_forum_topic_messages(forum_group_id, message_thread_id) + assert result is True, "Failed to unpin all the messages in forum topic" + + async def test_unpin_all_general_forum_topic_messages(self, bot, forum_group_id): + # We need 2 or more pinned msgs for this to work, else we get Chat_not_modified error + pin_msg_tasks = set() + + awaitables = {bot.send_message(forum_group_id, TEST_MSG_TEXT) for _ in range(2)} + for coro in asyncio.as_completed(awaitables): + msg = await coro + pin_msg_tasks.add(asyncio.create_task(msg.pin())) + + assert all([await task for task in pin_msg_tasks]) is True, "Message(s) were not pinned" + + result = await bot.unpin_all_general_forum_topic_messages(forum_group_id) + assert result is True, "Failed to unpin all the messages in forum topic" + + async def test_edit_general_forum_topic(self, bot, forum_group_id): + result = await bot.edit_general_forum_topic( + chat_id=forum_group_id, + name=f"GENERAL_{datetime.datetime.now().timestamp()}", + ) + assert result is True, "Failed to edit general forum topic" + # no way of checking the edited name, just the boolean result + + async def test_close_reopen_hide_unhide_general_forum_topic(self, bot, forum_group_id): + """Since reopening also unhides and hiding also closes, testing (un)hiding and + closing/reopening in different tests would mean that the tests have to be executed in + a specific order. For stability, we instead test all of them in one test.""" + + # We first ensure that the topic is open and visible + # Otherwise the tests below will fail + try: + await bot.reopen_general_forum_topic(chat_id=forum_group_id) + except BadRequest as exc: + # If the topic is already open, we get BadRequest: Topic_not_modified + if "Topic_not_modified" not in exc.message: + raise exc + + # first just close, bot don't hide + result = await bot.close_general_forum_topic( + chat_id=forum_group_id, + ) + assert result is True, "Failed to close general forum topic" + + # then hide + result = await bot.hide_general_forum_topic( + chat_id=forum_group_id, + ) + assert result is True, "Failed to hide general forum topic" + + # then unhide, but don't reopen + result = await bot.unhide_general_forum_topic( + chat_id=forum_group_id, + ) + assert result is True, "Failed to unhide general forum topic" + + # finally, reopen + # as this also unhides, this should ensure that the topic is open and visible + # for the next test run + result = await bot.reopen_general_forum_topic( + chat_id=forum_group_id, + ) + assert result is True, "Failed to reopen general forum topic" + + +@pytest.fixture(scope="module") +def topic_created(): + return ForumTopicCreated(name=TEST_TOPIC_NAME, icon_color=TEST_TOPIC_ICON_COLOR) + + +class TestForumTopicCreatedWithoutRequest: + def test_slot_behaviour(self, topic_created): + for attr in topic_created.__slots__: + assert getattr(topic_created, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(topic_created)) == len( + set(mro_slots(topic_created)) + ), "duplicate slot" + + def test_expected_values(self, topic_created): + assert topic_created.icon_color == TEST_TOPIC_ICON_COLOR + assert topic_created.name == TEST_TOPIC_NAME + + def test_de_json(self, bot): + assert ForumTopicCreated.de_json(None, bot=bot) is None + + json_dict = {"icon_color": TEST_TOPIC_ICON_COLOR, "name": TEST_TOPIC_NAME} + action = ForumTopicCreated.de_json(json_dict, bot) + assert action.api_kwargs == {} + + assert action.icon_color == TEST_TOPIC_ICON_COLOR + assert action.name == TEST_TOPIC_NAME + + def test_to_dict(self, topic_created): + action_dict = topic_created.to_dict() + + assert isinstance(action_dict, dict) + assert action_dict["name"] == TEST_TOPIC_NAME + assert action_dict["icon_color"] == TEST_TOPIC_ICON_COLOR + + def test_equality(self, emoji_id): + a = ForumTopicCreated(name=TEST_TOPIC_NAME, icon_color=TEST_TOPIC_ICON_COLOR) + b = ForumTopicCreated( + name=TEST_TOPIC_NAME, + icon_color=TEST_TOPIC_ICON_COLOR, + icon_custom_emoji_id=emoji_id, + ) + c = ForumTopicCreated(name=f"{TEST_TOPIC_NAME}!", icon_color=TEST_TOPIC_ICON_COLOR) + d = ForumTopicCreated(name=TEST_TOPIC_NAME, icon_color=0xFFD67E) + + assert a == b + assert hash(a) == hash(b) + + assert a != c + assert hash(a) != hash(c) + + assert a != d + assert hash(a) != hash(d) + + +class TestForumTopicClosedWithoutRequest: + def test_slot_behaviour(self): + action = ForumTopicClosed() + for attr in action.__slots__: + assert getattr(action, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(action)) == len(set(mro_slots(action))), "duplicate slot" + + def test_de_json(self): + action = ForumTopicClosed.de_json({}, None) + assert action.api_kwargs == {} + assert isinstance(action, ForumTopicClosed) + + def test_to_dict(self): + action = ForumTopicClosed() + action_dict = action.to_dict() + assert action_dict == {} + + +class TestForumTopicReopenedWithoutRequest: + def test_slot_behaviour(self): + action = ForumTopicReopened() + for attr in action.__slots__: + assert getattr(action, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(action)) == len(set(mro_slots(action))), "duplicate slot" + + def test_de_json(self): + action = ForumTopicReopened.de_json({}, None) + assert action.api_kwargs == {} + assert isinstance(action, ForumTopicReopened) + + def test_to_dict(self): + action = ForumTopicReopened() + action_dict = action.to_dict() + assert action_dict == {} + + +@pytest.fixture(scope="module") +def topic_edited(emoji_id): + return ForumTopicEdited(name=TEST_TOPIC_NAME, icon_custom_emoji_id=emoji_id) + + +class TestForumTopicEdited: + def test_slot_behaviour(self, topic_edited): + for attr in topic_edited.__slots__: + assert getattr(topic_edited, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(topic_edited)) == len(set(mro_slots(topic_edited))), "duplicate slot" + + def test_expected_values(self, topic_edited, emoji_id): + assert topic_edited.name == TEST_TOPIC_NAME + assert topic_edited.icon_custom_emoji_id == emoji_id + + def test_de_json(self, bot, emoji_id): + assert ForumTopicEdited.de_json(None, bot=bot) is None + + json_dict = {"name": TEST_TOPIC_NAME, "icon_custom_emoji_id": emoji_id} + action = ForumTopicEdited.de_json(json_dict, bot) + assert action.api_kwargs == {} + + assert action.name == TEST_TOPIC_NAME + assert action.icon_custom_emoji_id == emoji_id + # special test since it is mentioned in the docs that icon_custom_emoji_id can be an + # empty string + json_dict = {"icon_custom_emoji_id": ""} + action = ForumTopicEdited.de_json(json_dict, bot) + assert not action.icon_custom_emoji_id + + def test_to_dict(self, topic_edited, emoji_id): + action_dict = topic_edited.to_dict() + + assert isinstance(action_dict, dict) + assert action_dict["name"] == TEST_TOPIC_NAME + assert action_dict["icon_custom_emoji_id"] == emoji_id + + def test_equality(self, emoji_id): + a = ForumTopicEdited(name=TEST_TOPIC_NAME, icon_custom_emoji_id="") + b = ForumTopicEdited( + name=TEST_TOPIC_NAME, + icon_custom_emoji_id="", + ) + c = ForumTopicEdited(name=f"{TEST_TOPIC_NAME}!", icon_custom_emoji_id=emoji_id) + d = ForumTopicEdited(icon_custom_emoji_id="") + + assert a == b + assert hash(a) == hash(b) + + assert a != c + assert hash(a) != hash(c) + + assert a != d + assert hash(a) != hash(d) + + +class TestGeneralForumTopicHidden: + def test_slot_behaviour(self): + action = GeneralForumTopicHidden() + for attr in action.__slots__: + assert getattr(action, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(action)) == len(set(mro_slots(action))), "duplicate slot" + + def test_de_json(self): + action = GeneralForumTopicHidden.de_json({}, None) + assert action.api_kwargs == {} + assert isinstance(action, GeneralForumTopicHidden) + + def test_to_dict(self): + action = GeneralForumTopicHidden() + action_dict = action.to_dict() + assert action_dict == {} + + +class TestGeneralForumTopicUnhidden: + def test_slot_behaviour(self): + action = GeneralForumTopicUnhidden() + for attr in action.__slots__: + assert getattr(action, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(action)) == len(set(mro_slots(action))), "duplicate slot" + + def test_de_json(self): + action = GeneralForumTopicUnhidden.de_json({}, None) + assert action.api_kwargs == {} + assert isinstance(action, GeneralForumTopicUnhidden) + + def test_to_dict(self): + action = GeneralForumTopicUnhidden() + action_dict = action.to_dict() + assert action_dict == {} diff --git a/test_game.py b/test_game.py new file mode 100644 index 0000000000000000000000000000000000000000..23e0d5e9cea623d169f96193c18ff6d3e51d63de --- /dev/null +++ b/test_game.py @@ -0,0 +1,138 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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/]. + +import pytest + +from telegram import Animation, Game, MessageEntity, PhotoSize +from tests.auxil.slots import mro_slots + + +@pytest.fixture(scope="module") +def game(): + game = Game( + GameTestBase.title, + GameTestBase.description, + GameTestBase.photo, + text=GameTestBase.text, + text_entities=GameTestBase.text_entities, + animation=GameTestBase.animation, + ) + game._unfreeze() + return game + + +class GameTestBase: + title = "Python-telegram-bot Test Game" + description = "description" + photo = [PhotoSize("Blah", "ElseBlah", 640, 360, file_size=0)] + text = ( + b"\\U0001f469\\u200d\\U0001f469\\u200d\\U0001f467" + b"\\u200d\\U0001f467\\U0001f431http://google.com" + ).decode("unicode-escape") + text_entities = [MessageEntity(13, 17, MessageEntity.URL)] + animation = Animation("blah", "unique_id", 320, 180, 1) + + +class TestGameWithoutRequest(GameTestBase): + def test_slot_behaviour(self, game): + for attr in game.__slots__: + assert getattr(game, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(game)) == len(set(mro_slots(game))), "duplicate slot" + + def test_de_json_required(self, bot): + json_dict = { + "title": self.title, + "description": self.description, + "photo": [self.photo[0].to_dict()], + } + game = Game.de_json(json_dict, bot) + assert game.api_kwargs == {} + + assert game.title == self.title + assert game.description == self.description + assert game.photo == tuple(self.photo) + + def test_de_json_all(self, bot): + json_dict = { + "title": self.title, + "description": self.description, + "photo": [self.photo[0].to_dict()], + "text": self.text, + "text_entities": [self.text_entities[0].to_dict()], + "animation": self.animation.to_dict(), + } + game = Game.de_json(json_dict, bot) + assert game.api_kwargs == {} + + assert game.title == self.title + assert game.description == self.description + assert game.photo == tuple(self.photo) + assert game.text == self.text + assert game.text_entities == tuple(self.text_entities) + assert game.animation == self.animation + + def test_to_dict(self, game): + game_dict = game.to_dict() + + assert isinstance(game_dict, dict) + assert game_dict["title"] == game.title + assert game_dict["description"] == game.description + assert game_dict["photo"] == [game.photo[0].to_dict()] + assert game_dict["text"] == game.text + assert game_dict["text_entities"] == [game.text_entities[0].to_dict()] + assert game_dict["animation"] == game.animation.to_dict() + + def test_equality(self): + a = Game("title", "description", [PhotoSize("Blah", "unique_id", 640, 360, file_size=0)]) + b = Game( + "title", + "description", + [PhotoSize("Blah", "unique_id", 640, 360, file_size=0)], + text="Here is a text", + ) + c = Game( + "eltit", + "description", + [PhotoSize("Blah", "unique_id", 640, 360, file_size=0)], + animation=Animation("blah", "unique_id", 320, 180, 1), + ) + d = Animation("blah", "unique_id", 320, 180, 1) + + assert a == b + assert hash(a) == hash(b) + + assert a != c + assert hash(a) != hash(c) + + assert a != d + assert hash(a) != hash(d) + + def test_parse_entity(self, game): + entity = MessageEntity(type=MessageEntity.URL, offset=13, length=17) + game.text_entities = [entity] + + assert game.parse_text_entity(entity) == "http://google.com" + + def test_parse_entities(self, game): + entity = MessageEntity(type=MessageEntity.URL, offset=13, length=17) + entity_2 = MessageEntity(type=MessageEntity.BOLD, offset=13, length=1) + game.text_entities = [entity_2, entity] + + assert game.parse_text_entities(MessageEntity.URL) == {entity: "http://google.com"} + assert game.parse_text_entities() == {entity: "http://google.com", entity_2: "h"} diff --git a/test_gamehighscore.py b/test_gamehighscore.py new file mode 100644 index 0000000000000000000000000000000000000000..fc76867b49957d709eb1ed48ba72ace6a7da10cf --- /dev/null +++ b/test_gamehighscore.py @@ -0,0 +1,85 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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/]. + +import pytest + +from telegram import GameHighScore, User +from tests.auxil.slots import mro_slots + + +@pytest.fixture(scope="module") +def game_highscore(): + return GameHighScore( + GameHighScoreTestBase.position, GameHighScoreTestBase.user, GameHighScoreTestBase.score + ) + + +class GameHighScoreTestBase: + position = 12 + user = User(2, "test user", False) + score = 42 + + +class TestGameHighScoreWithoutRequest(GameHighScoreTestBase): + def test_slot_behaviour(self, game_highscore): + for attr in game_highscore.__slots__: + assert getattr(game_highscore, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(game_highscore)) == len(set(mro_slots(game_highscore))), "same slot" + + def test_de_json(self, bot): + json_dict = { + "position": self.position, + "user": self.user.to_dict(), + "score": self.score, + } + highscore = GameHighScore.de_json(json_dict, bot) + assert highscore.api_kwargs == {} + + assert highscore.position == self.position + assert highscore.user == self.user + assert highscore.score == self.score + + assert GameHighScore.de_json(None, bot) is None + + def test_to_dict(self, game_highscore): + game_highscore_dict = game_highscore.to_dict() + + assert isinstance(game_highscore_dict, dict) + assert game_highscore_dict["position"] == game_highscore.position + assert game_highscore_dict["user"] == game_highscore.user.to_dict() + assert game_highscore_dict["score"] == game_highscore.score + + def test_equality(self): + a = GameHighScore(1, User(2, "test user", False), 42) + b = GameHighScore(1, User(2, "test user", False), 42) + c = GameHighScore(2, User(2, "test user", False), 42) + d = GameHighScore(1, User(3, "test user", False), 42) + e = User(3, "test user", False) + + assert a == b + assert hash(a) == hash(b) + + assert a != c + assert hash(a) != hash(c) + + assert a != d + assert hash(a) != hash(d) + + assert a != e + assert hash(a) != hash(e) diff --git a/test_giveaway.py b/test_giveaway.py new file mode 100644 index 0000000000000000000000000000000000000000..359451183630a74bfbc295a071a3cf1adb3ae0f9 --- /dev/null +++ b/test_giveaway.py @@ -0,0 +1,419 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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/]. + +import datetime as dtm + +import pytest + +from telegram import ( + BotCommand, + Chat, + Giveaway, + GiveawayCompleted, + GiveawayCreated, + GiveawayWinners, + Message, + User, +) +from telegram._utils.datetime import UTC, to_timestamp +from tests.auxil.slots import mro_slots + + +@pytest.fixture(scope="module") +def giveaway(): + return Giveaway( + chats=[Chat(1, Chat.CHANNEL), Chat(2, Chat.SUPERGROUP)], + winners_selection_date=TestGiveawayWithoutRequest.winners_selection_date, + winner_count=TestGiveawayWithoutRequest.winner_count, + only_new_members=TestGiveawayWithoutRequest.only_new_members, + has_public_winners=TestGiveawayWithoutRequest.has_public_winners, + prize_description=TestGiveawayWithoutRequest.prize_description, + country_codes=TestGiveawayWithoutRequest.country_codes, + premium_subscription_month_count=( + TestGiveawayWithoutRequest.premium_subscription_month_count + ), + ) + + +class TestGiveawayWithoutRequest: + chats = [Chat(1, Chat.CHANNEL), Chat(2, Chat.SUPERGROUP)] + winners_selection_date = dtm.datetime.now(dtm.timezone.utc).replace(microsecond=0) + winner_count = 42 + only_new_members = True + has_public_winners = True + prize_description = "prize_description" + country_codes = ["DE", "US"] + premium_subscription_month_count = 3 + + def test_slot_behaviour(self, giveaway): + for attr in giveaway.__slots__: + assert getattr(giveaway, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(giveaway)) == len(set(mro_slots(giveaway))), "duplicate slot" + + def test_de_json(self, bot): + json_dict = { + "chats": [chat.to_dict() for chat in self.chats], + "winners_selection_date": to_timestamp(self.winners_selection_date), + "winner_count": self.winner_count, + "only_new_members": self.only_new_members, + "has_public_winners": self.has_public_winners, + "prize_description": self.prize_description, + "country_codes": self.country_codes, + "premium_subscription_month_count": self.premium_subscription_month_count, + } + + giveaway = Giveaway.de_json(json_dict, bot) + assert giveaway.api_kwargs == {} + + assert giveaway.chats == tuple(self.chats) + assert giveaway.winners_selection_date == self.winners_selection_date + assert giveaway.winner_count == self.winner_count + assert giveaway.only_new_members == self.only_new_members + assert giveaway.has_public_winners == self.has_public_winners + assert giveaway.prize_description == self.prize_description + assert giveaway.country_codes == tuple(self.country_codes) + assert giveaway.premium_subscription_month_count == self.premium_subscription_month_count + + assert Giveaway.de_json(None, bot) is None + + def test_de_json_localization(self, tz_bot, bot, raw_bot): + json_dict = { + "chats": [chat.to_dict() for chat in self.chats], + "winners_selection_date": to_timestamp(self.winners_selection_date), + "winner_count": self.winner_count, + "only_new_members": self.only_new_members, + "has_public_winners": self.has_public_winners, + "prize_description": self.prize_description, + "country_codes": self.country_codes, + "premium_subscription_month_count": self.premium_subscription_month_count, + } + + giveaway_raw = Giveaway.de_json(json_dict, raw_bot) + giveaway_bot = Giveaway.de_json(json_dict, bot) + giveaway_bot_tz = Giveaway.de_json(json_dict, tz_bot) + + # comparing utcoffsets because comparing timezones is unpredicatable + giveaway_bot_tz_offset = giveaway_bot_tz.winners_selection_date.utcoffset() + tz_bot_offset = tz_bot.defaults.tzinfo.utcoffset( + giveaway_bot_tz.winners_selection_date.replace(tzinfo=None) + ) + + assert giveaway_raw.winners_selection_date.tzinfo == UTC + assert giveaway_bot.winners_selection_date.tzinfo == UTC + assert giveaway_bot_tz_offset == tz_bot_offset + + def test_to_dict(self, giveaway): + giveaway_dict = giveaway.to_dict() + + assert isinstance(giveaway_dict, dict) + assert giveaway_dict["chats"] == [chat.to_dict() for chat in self.chats] + assert giveaway_dict["winners_selection_date"] == to_timestamp(self.winners_selection_date) + assert giveaway_dict["winner_count"] == self.winner_count + assert giveaway_dict["only_new_members"] == self.only_new_members + assert giveaway_dict["has_public_winners"] == self.has_public_winners + assert giveaway_dict["prize_description"] == self.prize_description + assert giveaway_dict["country_codes"] == self.country_codes + assert ( + giveaway_dict["premium_subscription_month_count"] + == self.premium_subscription_month_count + ) + + def test_equality(self, giveaway): + a = giveaway + b = Giveaway( + chats=self.chats, + winners_selection_date=self.winners_selection_date, + winner_count=self.winner_count, + ) + c = Giveaway( + chats=self.chats, + winners_selection_date=self.winners_selection_date + dtm.timedelta(seconds=100), + winner_count=self.winner_count, + ) + d = Giveaway( + chats=self.chats, winners_selection_date=self.winners_selection_date, winner_count=17 + ) + e = BotCommand("start", "description") + + assert a == b + assert hash(a) == hash(b) + + assert a != c + assert hash(a) != hash(c) + + assert a != d + assert hash(a) != hash(d) + + assert a != e + assert hash(a) != hash(e) + + +class TestGiveawayCreatedWithoutRequest: + def test_slot_behaviour(self): + giveaway_created = GiveawayCreated() + for attr in giveaway_created.__slots__: + assert getattr(giveaway_created, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(giveaway_created)) == len( + set(mro_slots(giveaway_created)) + ), "duplicate slot" + + +@pytest.fixture(scope="module") +def giveaway_winners(): + return GiveawayWinners( + chat=TestGiveawayWinnersWithoutRequest.chat, + giveaway_message_id=TestGiveawayWinnersWithoutRequest.giveaway_message_id, + winners_selection_date=TestGiveawayWinnersWithoutRequest.winners_selection_date, + winner_count=TestGiveawayWinnersWithoutRequest.winner_count, + winners=TestGiveawayWinnersWithoutRequest.winners, + only_new_members=TestGiveawayWinnersWithoutRequest.only_new_members, + prize_description=TestGiveawayWinnersWithoutRequest.prize_description, + premium_subscription_month_count=( + TestGiveawayWinnersWithoutRequest.premium_subscription_month_count + ), + additional_chat_count=TestGiveawayWinnersWithoutRequest.additional_chat_count, + unclaimed_prize_count=TestGiveawayWinnersWithoutRequest.unclaimed_prize_count, + was_refunded=TestGiveawayWinnersWithoutRequest.was_refunded, + ) + + +class TestGiveawayWinnersWithoutRequest: + chat = Chat(1, Chat.CHANNEL) + giveaway_message_id = 123456789 + winners_selection_date = dtm.datetime.now(dtm.timezone.utc).replace(microsecond=0) + winner_count = 42 + winners = [User(1, "user1", False), User(2, "user2", False)] + additional_chat_count = 2 + premium_subscription_month_count = 3 + unclaimed_prize_count = 4 + only_new_members = True + was_refunded = True + prize_description = "prize_description" + + def test_slot_behaviour(self, giveaway_winners): + for attr in giveaway_winners.__slots__: + assert getattr(giveaway_winners, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(giveaway_winners)) == len( + set(mro_slots(giveaway_winners)) + ), "duplicate slot" + + def test_de_json(self, bot): + json_dict = { + "chat": self.chat.to_dict(), + "giveaway_message_id": self.giveaway_message_id, + "winners_selection_date": to_timestamp(self.winners_selection_date), + "winner_count": self.winner_count, + "winners": [winner.to_dict() for winner in self.winners], + "additional_chat_count": self.additional_chat_count, + "premium_subscription_month_count": self.premium_subscription_month_count, + "unclaimed_prize_count": self.unclaimed_prize_count, + "only_new_members": self.only_new_members, + "was_refunded": self.was_refunded, + "prize_description": self.prize_description, + } + + giveaway_winners = GiveawayWinners.de_json(json_dict, bot) + assert giveaway_winners.api_kwargs == {} + + assert giveaway_winners.chat == self.chat + assert giveaway_winners.giveaway_message_id == self.giveaway_message_id + assert giveaway_winners.winners_selection_date == self.winners_selection_date + assert giveaway_winners.winner_count == self.winner_count + assert giveaway_winners.winners == tuple(self.winners) + assert giveaway_winners.additional_chat_count == self.additional_chat_count + assert ( + giveaway_winners.premium_subscription_month_count + == self.premium_subscription_month_count + ) + assert giveaway_winners.unclaimed_prize_count == self.unclaimed_prize_count + assert giveaway_winners.only_new_members == self.only_new_members + assert giveaway_winners.was_refunded == self.was_refunded + assert giveaway_winners.prize_description == self.prize_description + + assert GiveawayWinners.de_json(None, bot) is None + + def test_de_json_localization(self, tz_bot, bot, raw_bot): + json_dict = { + "chat": self.chat.to_dict(), + "giveaway_message_id": self.giveaway_message_id, + "winners_selection_date": to_timestamp(self.winners_selection_date), + "winner_count": self.winner_count, + "winners": [winner.to_dict() for winner in self.winners], + } + + giveaway_winners_raw = GiveawayWinners.de_json(json_dict, raw_bot) + giveaway_winners_bot = GiveawayWinners.de_json(json_dict, bot) + giveaway_winners_bot_tz = GiveawayWinners.de_json(json_dict, tz_bot) + + # comparing utcoffsets because comparing timezones is unpredicatable + giveaway_winners_bot_tz_offset = giveaway_winners_bot_tz.winners_selection_date.utcoffset() + tz_bot_offset = tz_bot.defaults.tzinfo.utcoffset( + giveaway_winners_bot_tz.winners_selection_date.replace(tzinfo=None) + ) + + assert giveaway_winners_raw.winners_selection_date.tzinfo == UTC + assert giveaway_winners_bot.winners_selection_date.tzinfo == UTC + assert giveaway_winners_bot_tz_offset == tz_bot_offset + + def test_to_dict(self, giveaway_winners): + giveaway_winners_dict = giveaway_winners.to_dict() + + assert isinstance(giveaway_winners_dict, dict) + assert giveaway_winners_dict["chat"] == self.chat.to_dict() + assert giveaway_winners_dict["giveaway_message_id"] == self.giveaway_message_id + assert giveaway_winners_dict["winners_selection_date"] == to_timestamp( + self.winners_selection_date + ) + assert giveaway_winners_dict["winner_count"] == self.winner_count + assert giveaway_winners_dict["winners"] == [winner.to_dict() for winner in self.winners] + assert giveaway_winners_dict["additional_chat_count"] == self.additional_chat_count + assert ( + giveaway_winners_dict["premium_subscription_month_count"] + == self.premium_subscription_month_count + ) + assert giveaway_winners_dict["unclaimed_prize_count"] == self.unclaimed_prize_count + assert giveaway_winners_dict["only_new_members"] == self.only_new_members + assert giveaway_winners_dict["was_refunded"] == self.was_refunded + assert giveaway_winners_dict["prize_description"] == self.prize_description + + def test_equality(self, giveaway_winners): + a = giveaway_winners + b = GiveawayWinners( + chat=self.chat, + giveaway_message_id=self.giveaway_message_id, + winners_selection_date=self.winners_selection_date, + winner_count=self.winner_count, + winners=self.winners, + ) + c = GiveawayWinners( + chat=self.chat, + giveaway_message_id=self.giveaway_message_id, + winners_selection_date=self.winners_selection_date + dtm.timedelta(seconds=100), + winner_count=self.winner_count, + winners=self.winners, + ) + d = GiveawayWinners( + chat=self.chat, + giveaway_message_id=self.giveaway_message_id, + winners_selection_date=self.winners_selection_date, + winner_count=17, + winners=self.winners, + ) + e = BotCommand("start", "description") + + assert a == b + assert hash(a) == hash(b) + + assert a != c + assert hash(a) != hash(c) + + assert a != d + assert hash(a) != hash(d) + + assert a != e + assert hash(a) != hash(e) + + +@pytest.fixture(scope="module") +def giveaway_completed(): + return GiveawayCompleted( + winner_count=TestGiveawayCompletedWithoutRequest.winner_count, + unclaimed_prize_count=TestGiveawayCompletedWithoutRequest.unclaimed_prize_count, + giveaway_message=TestGiveawayCompletedWithoutRequest.giveaway_message, + ) + + +class TestGiveawayCompletedWithoutRequest: + winner_count = 42 + unclaimed_prize_count = 4 + giveaway_message = Message( + message_id=1, + date=dtm.datetime.now(dtm.timezone.utc), + text="giveaway_message", + chat=Chat(1, Chat.CHANNEL), + from_user=User(1, "user1", False), + ) + + def test_slot_behaviour(self, giveaway_completed): + for attr in giveaway_completed.__slots__: + assert getattr(giveaway_completed, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(giveaway_completed)) == len( + set(mro_slots(giveaway_completed)) + ), "duplicate slot" + + def test_de_json(self, bot): + json_dict = { + "winner_count": self.winner_count, + "unclaimed_prize_count": self.unclaimed_prize_count, + "giveaway_message": self.giveaway_message.to_dict(), + } + + giveaway_completed = GiveawayCompleted.de_json(json_dict, bot) + assert giveaway_completed.api_kwargs == {} + + assert giveaway_completed.winner_count == self.winner_count + assert giveaway_completed.unclaimed_prize_count == self.unclaimed_prize_count + assert giveaway_completed.giveaway_message == self.giveaway_message + + assert GiveawayCompleted.de_json(None, bot) is None + + def test_to_dict(self, giveaway_completed): + giveaway_completed_dict = giveaway_completed.to_dict() + + assert isinstance(giveaway_completed_dict, dict) + assert giveaway_completed_dict["winner_count"] == self.winner_count + assert giveaway_completed_dict["unclaimed_prize_count"] == self.unclaimed_prize_count + assert giveaway_completed_dict["giveaway_message"] == self.giveaway_message.to_dict() + + def test_equality(self, giveaway_completed): + a = giveaway_completed + b = GiveawayCompleted( + winner_count=self.winner_count, + unclaimed_prize_count=self.unclaimed_prize_count, + giveaway_message=self.giveaway_message, + ) + c = GiveawayCompleted( + winner_count=self.winner_count + 30, + unclaimed_prize_count=self.unclaimed_prize_count, + ) + d = GiveawayCompleted( + winner_count=self.winner_count, + unclaimed_prize_count=17, + giveaway_message=self.giveaway_message, + ) + e = GiveawayCompleted( + winner_count=self.winner_count + 1, + unclaimed_prize_count=self.unclaimed_prize_count, + ) + f = BotCommand("start", "description") + + assert a == b + assert hash(a) == hash(b) + + assert a != c + assert hash(a) != hash(c) + + assert a != d + assert hash(a) != hash(d) + + assert a != e + assert hash(a) != hash(e) + + assert a != f + assert hash(a) != hash(f) diff --git a/test_helpers.py b/test_helpers.py new file mode 100644 index 0000000000000000000000000000000000000000..5a453da716da8a2d5b3e047b9a45a532991710d0 --- /dev/null +++ b/test_helpers.py @@ -0,0 +1,166 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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/]. +import re + +import pytest + +from telegram import Message, MessageEntity, Update, helpers +from telegram.constants import MessageType + + +class TestHelpers: + @pytest.mark.parametrize( + ("test_str", "expected"), + [ + ("*bold*", r"\*bold\*"), + ("_italic_", r"\_italic\_"), + ("`code`", r"\`code\`"), + ("[text_link](https://github.com/)", r"\[text\_link](https://github.com/)"), + ("![👍](tg://emoji?id=1)", r"!\[👍](tg://emoji?id=1)"), + ], + ids=["bold", "italic", "code", "text_link", "custom_emoji_id"], + ) + def test_escape_markdown(self, test_str, expected): + assert expected == helpers.escape_markdown(test_str) + + @pytest.mark.parametrize( + ("test_str", "expected"), + [ + (r"a_b*c[d]e", r"a\_b\*c\[d\]e"), + (r"(fg) ", r"\(fg\) "), + (r"h~I`>JK#L+MN", r"h\~I\`\>JK\#L\+MN"), + (r"-O=|p{qr}s.t!\ ", r"\-O\=\|p\{qr\}s\.t\!\\ "), + (r"\u", r"\\u"), + ], + ) + def test_escape_markdown_v2(self, test_str, expected): + assert expected == helpers.escape_markdown(test_str, version=2) + + @pytest.mark.parametrize( + ("test_str", "expected"), + [ + (r"mono/pre:", r"mono/pre:"), + ("`abc`", r"\`abc\`"), + (r"\int", r"\\int"), + (r"(`\some \` stuff)", r"(\`\\some \\\` stuff)"), + ], + ) + def test_escape_markdown_v2_monospaced(self, test_str, expected): + assert expected == helpers.escape_markdown( + test_str, version=2, entity_type=MessageEntity.PRE + ) + assert expected == helpers.escape_markdown( + test_str, version=2, entity_type=MessageEntity.CODE + ) + + def test_escape_markdown_v2_links(self): + test_str = "https://url.containing/funny)cha)\\ra\\)cter\\s" + expected_str = "https://url.containing/funny\\)cha\\)\\\\ra\\\\\\)cter\\\\s" + + assert expected_str == helpers.escape_markdown( + test_str, version=2, entity_type=MessageEntity.TEXT_LINK + ) + assert expected_str == helpers.escape_markdown( + test_str, version=2, entity_type=MessageEntity.CUSTOM_EMOJI + ) + + def test_markdown_invalid_version(self): + with pytest.raises(ValueError, match="Markdown version must be either"): + helpers.escape_markdown("abc", version=-1) + with pytest.raises(ValueError, match="Markdown version must be either"): + helpers.mention_markdown(1, "abc", version=-1) + + def test_create_deep_linked_url(self): + username = "JamesTheMock" + + payload = "hello" + expected = f"https://t.me/{username}?start={payload}" + actual = helpers.create_deep_linked_url(username, payload) + assert expected == actual + + expected = f"https://t.me/{username}?startgroup={payload}" + actual = helpers.create_deep_linked_url(username, payload, group=True) + assert expected == actual + + payload = "" + expected = f"https://t.me/{username}" + assert expected == helpers.create_deep_linked_url(username) + assert expected == helpers.create_deep_linked_url(username, payload) + payload = None + assert expected == helpers.create_deep_linked_url(username, payload) + + with pytest.raises(ValueError, match="Only the following characters"): + helpers.create_deep_linked_url(username, "text with spaces") + + with pytest.raises(ValueError, match="must not exceed 64"): + helpers.create_deep_linked_url(username, "0" * 65) + + with pytest.raises(ValueError, match="valid bot_username"): + helpers.create_deep_linked_url(None, None) + with pytest.raises(ValueError, match="valid bot_username"): # too short username, 4 is min + helpers.create_deep_linked_url("abc", None) + + @pytest.mark.parametrize("message_type", list(MessageType)) + @pytest.mark.parametrize("entity_type", [Update, Message]) + def test_effective_message_type(self, message_type, entity_type): + def build_test_message(kwargs): + config = { + "message_id": 1, + "from_user": None, + "date": None, + "chat": None, + } + config.update(**kwargs) + return Message(**config) + + message = build_test_message({message_type: (True,)}) # tuple for array-type args + entity = message if entity_type is Message else Update(1, message=message) + assert helpers.effective_message_type(entity) == message_type + + empty_update = Update(2) + assert helpers.effective_message_type(empty_update) is None + + def test_effective_message_type_wrong_type(self): + with pytest.raises( + TypeError, match=re.escape(f"neither Message nor Update (got: {type(entity := {})})") + ): + helpers.effective_message_type(entity) + + def test_mention_html(self): + expected = 'the name' + + assert expected == helpers.mention_html(1, "the name") + + @pytest.mark.parametrize( + ("test_str", "expected"), + [ + ("the name", "[the name](tg://user?id=1)"), + ("under_score", "[under_score](tg://user?id=1)"), + ("starred*text", "[starred*text](tg://user?id=1)"), + ("`backtick`", "[`backtick`](tg://user?id=1)"), + ("[square brackets", "[[square brackets](tg://user?id=1)"), + ], + ) + def test_mention_markdown(self, test_str, expected): + assert expected == helpers.mention_markdown(1, test_str) + + def test_mention_markdown_2(self): + expected = r"[the\_name](tg://user?id=1)" + + assert expected == helpers.mention_markdown(1, "the_name", 2) diff --git a/test_inlinequeryresultsbutton.py b/test_inlinequeryresultsbutton.py new file mode 100644 index 0000000000000000000000000000000000000000..90ce2c235acb5b1b8df9989457c285dfde3ef9a9 --- /dev/null +++ b/test_inlinequeryresultsbutton.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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/]. +import pytest + +from telegram import InlineQueryResultsButton, WebAppInfo +from tests.auxil.slots import mro_slots + + +@pytest.fixture(scope="module") +def inline_query_results_button(): + return InlineQueryResultsButton( + text=InlineQueryResultsButtonTestBase.text, + start_parameter=InlineQueryResultsButtonTestBase.start_parameter, + web_app=InlineQueryResultsButtonTestBase.web_app, + ) + + +class InlineQueryResultsButtonTestBase: + text = "text" + start_parameter = "start_parameter" + web_app = WebAppInfo(url="https://python-telegram-bot.org") + + +class TestInlineQueryResultsButtonWithoutRequest(InlineQueryResultsButtonTestBase): + def test_slot_behaviour(self, inline_query_results_button): + inst = inline_query_results_button + for attr in inst.__slots__: + assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" + + def test_to_dict(self, inline_query_results_button): + inline_query_results_button_dict = inline_query_results_button.to_dict() + assert isinstance(inline_query_results_button_dict, dict) + assert inline_query_results_button_dict["text"] == self.text + assert inline_query_results_button_dict["start_parameter"] == self.start_parameter + assert inline_query_results_button_dict["web_app"] == self.web_app.to_dict() + + def test_de_json(self, bot): + assert InlineQueryResultsButton.de_json(None, bot) is None + assert InlineQueryResultsButton.de_json({}, bot) is None + + json_dict = { + "text": self.text, + "start_parameter": self.start_parameter, + "web_app": self.web_app.to_dict(), + } + inline_query_results_button = InlineQueryResultsButton.de_json(json_dict, bot) + + assert inline_query_results_button.text == self.text + assert inline_query_results_button.start_parameter == self.start_parameter + assert inline_query_results_button.web_app == self.web_app + + def test_equality(self): + a = InlineQueryResultsButton(self.text, self.start_parameter, self.web_app) + b = InlineQueryResultsButton(self.text, self.start_parameter, self.web_app) + c = InlineQueryResultsButton(self.text, "", self.web_app) + d = InlineQueryResultsButton(self.text, self.start_parameter, None) + + assert a == b + assert hash(a) == hash(b) + assert a is not b + + assert a != c + assert hash(a) != hash(c) + + assert a != d + assert hash(a) != hash(d) diff --git a/test_keyboardbutton.py b/test_keyboardbutton.py new file mode 100644 index 0000000000000000000000000000000000000000..4493ed22320c80cdad66e37b6849d4dfcfb1ec71 --- /dev/null +++ b/test_keyboardbutton.py @@ -0,0 +1,149 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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/]. +import pytest + +from telegram import ( + InlineKeyboardButton, + KeyboardButton, + KeyboardButtonPollType, + KeyboardButtonRequestChat, + KeyboardButtonRequestUsers, + WebAppInfo, +) +from tests.auxil.slots import mro_slots + + +@pytest.fixture(scope="module") +def keyboard_button(): + return KeyboardButton( + KeyboardButtonTestBase.text, + request_location=KeyboardButtonTestBase.request_location, + request_contact=KeyboardButtonTestBase.request_contact, + request_poll=KeyboardButtonTestBase.request_poll, + web_app=KeyboardButtonTestBase.web_app, + request_chat=KeyboardButtonTestBase.request_chat, + request_users=KeyboardButtonTestBase.request_users, + ) + + +class KeyboardButtonTestBase: + text = "text" + request_location = True + request_contact = True + request_poll = KeyboardButtonPollType("quiz") + web_app = WebAppInfo(url="https://example.com") + request_chat = KeyboardButtonRequestChat(1, True) + request_users = KeyboardButtonRequestUsers(2) + + +class TestKeyboardButtonWithoutRequest(KeyboardButtonTestBase): + def test_slot_behaviour(self, keyboard_button): + inst = keyboard_button + for attr in inst.__slots__: + assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" + + def test_expected_values(self, keyboard_button): + assert keyboard_button.text == self.text + assert keyboard_button.request_location == self.request_location + assert keyboard_button.request_contact == self.request_contact + assert keyboard_button.request_poll == self.request_poll + assert keyboard_button.web_app == self.web_app + assert keyboard_button.request_chat == self.request_chat + assert keyboard_button.request_users == self.request_users + + def test_to_dict(self, keyboard_button): + keyboard_button_dict = keyboard_button.to_dict() + + assert isinstance(keyboard_button_dict, dict) + assert keyboard_button_dict["text"] == keyboard_button.text + assert keyboard_button_dict["request_location"] == keyboard_button.request_location + assert keyboard_button_dict["request_contact"] == keyboard_button.request_contact + assert keyboard_button_dict["request_poll"] == keyboard_button.request_poll.to_dict() + assert keyboard_button_dict["web_app"] == keyboard_button.web_app.to_dict() + assert keyboard_button_dict["request_chat"] == keyboard_button.request_chat.to_dict() + assert keyboard_button_dict["request_users"] == keyboard_button.request_users.to_dict() + + @pytest.mark.parametrize("request_user", [True, False]) + def test_de_json(self, bot, request_user): + json_dict = { + "text": self.text, + "request_location": self.request_location, + "request_contact": self.request_contact, + "request_poll": self.request_poll.to_dict(), + "web_app": self.web_app.to_dict(), + "request_chat": self.request_chat.to_dict(), + "request_users": self.request_users.to_dict(), + } + if request_user: + json_dict["request_user"] = {"request_id": 2} + + keyboard_button = KeyboardButton.de_json(json_dict, None) + if request_user: + assert keyboard_button.api_kwargs == {"request_user": {"request_id": 2}} + else: + assert keyboard_button.api_kwargs == {} + + assert keyboard_button.text == self.text + assert keyboard_button.request_location == self.request_location + assert keyboard_button.request_contact == self.request_contact + assert keyboard_button.request_poll == self.request_poll + assert keyboard_button.web_app == self.web_app + assert keyboard_button.request_chat == self.request_chat + assert keyboard_button.request_users == self.request_users + + none = KeyboardButton.de_json({}, None) + assert none is None + + def test_equality(self): + a = KeyboardButton("test", request_contact=True) + b = KeyboardButton("test", request_contact=True) + c = KeyboardButton("Test", request_location=True) + d = KeyboardButton("Test", web_app=WebAppInfo(url="https://ptb.org")) + e = InlineKeyboardButton("test", callback_data="test") + f = KeyboardButton( + "test", + request_contact=True, + request_chat=KeyboardButtonRequestChat(1, False), + request_users=KeyboardButtonRequestUsers(2), + ) + g = KeyboardButton( + "test", + request_contact=True, + request_chat=KeyboardButtonRequestChat(1, False), + request_users=KeyboardButtonRequestUsers(2), + ) + + assert a == b + assert hash(a) == hash(b) + + assert a != c + assert hash(a) != hash(c) + + assert a != d + assert hash(a) != hash(d) + + assert a != e + assert hash(a) != hash(e) + + assert a != f + assert hash(a) != hash(f) + + assert f == g + assert hash(f) == hash(g) diff --git a/test_keyboardbuttonpolltype.py b/test_keyboardbuttonpolltype.py new file mode 100644 index 0000000000000000000000000000000000000000..6c4e92bf2ca43443c8b610c3ca33dccd5aefb9b6 --- /dev/null +++ b/test_keyboardbuttonpolltype.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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/]. +import pytest + +from telegram import KeyboardButtonPollType, Poll +from telegram.constants import PollType +from tests.auxil.slots import mro_slots + + +@pytest.fixture(scope="module") +def keyboard_button_poll_type(): + return KeyboardButtonPollType(KeyboardButtonPollTypeTestBase.type) + + +class KeyboardButtonPollTypeTestBase: + type = Poll.QUIZ + + +class TestKeyboardButtonPollTypeWithoutRequest(KeyboardButtonPollTypeTestBase): + def test_slot_behaviour(self, keyboard_button_poll_type): + inst = keyboard_button_poll_type + for attr in inst.__slots__: + assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" + + def test_to_dict(self, keyboard_button_poll_type): + keyboard_button_poll_type_dict = keyboard_button_poll_type.to_dict() + assert isinstance(keyboard_button_poll_type_dict, dict) + assert keyboard_button_poll_type_dict["type"] == self.type + + def test_type_enum_conversion(self): + assert ( + type( + KeyboardButtonPollType( + type="quiz", + ).type + ) + is PollType + ) + assert ( + KeyboardButtonPollType( + type="unknown", + ).type + == "unknown" + ) + + def test_equality(self): + a = KeyboardButtonPollType(Poll.QUIZ) + b = KeyboardButtonPollType(Poll.QUIZ) + c = KeyboardButtonPollType(Poll.REGULAR) + + assert a == b + assert hash(a) == hash(b) + assert a is not b + + assert a != c + assert hash(a) != hash(c) diff --git a/test_keyboardbuttonrequest.py b/test_keyboardbuttonrequest.py new file mode 100644 index 0000000000000000000000000000000000000000..8e42b1cd374e113b70c71f21291a6a76fa569ad7 --- /dev/null +++ b/test_keyboardbuttonrequest.py @@ -0,0 +1,195 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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/]. + +import pytest + +from telegram import ChatAdministratorRights, KeyboardButtonRequestChat, KeyboardButtonRequestUsers +from tests.auxil.slots import mro_slots + + +@pytest.fixture(scope="class") +def request_users(): + return KeyboardButtonRequestUsers( + KeyboardButtonRequestUsersTestBase.request_id, + KeyboardButtonRequestUsersTestBase.user_is_bot, + KeyboardButtonRequestUsersTestBase.user_is_premium, + KeyboardButtonRequestUsersTestBase.max_quantity, + ) + + +class KeyboardButtonRequestUsersTestBase: + request_id = 123 + user_is_bot = True + user_is_premium = False + max_quantity = 10 + + +class TestKeyboardButtonRequestUsersWithoutRequest(KeyboardButtonRequestUsersTestBase): + def test_slot_behaviour(self, request_users): + for attr in request_users.__slots__: + assert getattr(request_users, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(request_users)) == len( + set(mro_slots(request_users)) + ), "duplicate slot" + + def test_to_dict(self, request_users): + request_users_dict = request_users.to_dict() + + assert isinstance(request_users_dict, dict) + assert request_users_dict["request_id"] == self.request_id + assert request_users_dict["user_is_bot"] == self.user_is_bot + assert request_users_dict["user_is_premium"] == self.user_is_premium + assert request_users_dict["max_quantity"] == self.max_quantity + + def test_de_json(self, bot): + json_dict = { + "request_id": self.request_id, + "user_is_bot": self.user_is_bot, + "user_is_premium": self.user_is_premium, + "max_quantity": self.max_quantity, + } + request_users = KeyboardButtonRequestUsers.de_json(json_dict, bot) + assert request_users.api_kwargs == {} + + assert request_users.request_id == self.request_id + assert request_users.user_is_bot == self.user_is_bot + assert request_users.user_is_premium == self.user_is_premium + assert request_users.max_quantity == self.max_quantity + + def test_equality(self): + a = KeyboardButtonRequestUsers(self.request_id) + b = KeyboardButtonRequestUsers(self.request_id) + c = KeyboardButtonRequestUsers(1) + + assert a == b + assert hash(a) == hash(b) + assert a is not b + + assert a != c + assert hash(a) != hash(c) + + +@pytest.fixture(scope="class") +def request_chat(): + return KeyboardButtonRequestChat( + KeyboardButtonRequestChatTestBase.request_id, + KeyboardButtonRequestChatTestBase.chat_is_channel, + KeyboardButtonRequestChatTestBase.chat_is_forum, + KeyboardButtonRequestChatTestBase.chat_has_username, + KeyboardButtonRequestChatTestBase.chat_is_created, + KeyboardButtonRequestChatTestBase.user_administrator_rights, + KeyboardButtonRequestChatTestBase.bot_administrator_rights, + KeyboardButtonRequestChatTestBase.bot_is_member, + ) + + +class KeyboardButtonRequestChatTestBase: + request_id = 456 + chat_is_channel = True + chat_is_forum = False + chat_has_username = True + chat_is_created = False + user_administrator_rights = ChatAdministratorRights( + True, + False, + True, + False, + True, + False, + True, + False, + can_post_stories=False, + can_edit_stories=False, + can_delete_stories=False, + ) + bot_administrator_rights = ChatAdministratorRights( + True, + False, + True, + False, + True, + False, + True, + False, + can_post_stories=False, + can_edit_stories=False, + can_delete_stories=False, + ) + bot_is_member = True + + +class TestKeyboardButtonRequestChatWithoutRequest(KeyboardButtonRequestChatTestBase): + def test_slot_behaviour(self, request_chat): + for attr in request_chat.__slots__: + assert getattr(request_chat, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(request_chat)) == len(set(mro_slots(request_chat))), "duplicate slot" + + def test_to_dict(self, request_chat): + request_chat_dict = request_chat.to_dict() + + assert isinstance(request_chat_dict, dict) + assert request_chat_dict["request_id"] == self.request_id + assert request_chat_dict["chat_is_channel"] == self.chat_is_channel + assert request_chat_dict["chat_is_forum"] == self.chat_is_forum + assert request_chat_dict["chat_has_username"] == self.chat_has_username + assert ( + request_chat_dict["user_administrator_rights"] + == self.user_administrator_rights.to_dict() + ) + assert ( + request_chat_dict["bot_administrator_rights"] + == self.bot_administrator_rights.to_dict() + ) + assert request_chat_dict["bot_is_member"] == self.bot_is_member + + def test_de_json(self, bot): + json_dict = { + "request_id": self.request_id, + "chat_is_channel": self.chat_is_channel, + "chat_is_forum": self.chat_is_forum, + "chat_has_username": self.chat_has_username, + "user_administrator_rights": self.user_administrator_rights.to_dict(), + "bot_administrator_rights": self.bot_administrator_rights.to_dict(), + "bot_is_member": self.bot_is_member, + } + request_chat = KeyboardButtonRequestChat.de_json(json_dict, bot) + assert request_chat.api_kwargs == {} + + assert request_chat.request_id == self.request_id + assert request_chat.chat_is_channel == self.chat_is_channel + assert request_chat.chat_is_forum == self.chat_is_forum + assert request_chat.chat_has_username == self.chat_has_username + assert request_chat.user_administrator_rights == self.user_administrator_rights + assert request_chat.bot_administrator_rights == self.bot_administrator_rights + assert request_chat.bot_is_member == self.bot_is_member + + empty_chat = KeyboardButtonRequestChat.de_json({}, bot) + assert empty_chat is None + + def test_equality(self): + a = KeyboardButtonRequestChat(self.request_id, True) + b = KeyboardButtonRequestChat(self.request_id, True) + c = KeyboardButtonRequestChat(1, True) + + assert a == b + assert hash(a) == hash(b) + assert a is not b + + assert a != c + assert hash(a) != hash(c) diff --git a/test_linkpreviewoptions.py b/test_linkpreviewoptions.py new file mode 100644 index 0000000000000000000000000000000000000000..a4fa5aea455137d4a50fd4baf8e9107d838baafe --- /dev/null +++ b/test_linkpreviewoptions.py @@ -0,0 +1,106 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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/]. +import pytest + +from telegram import LinkPreviewOptions +from tests.auxil.slots import mro_slots + + +@pytest.fixture(scope="module") +def link_preview_options(): + return LinkPreviewOptions( + is_disabled=LinkPreviewOptionsTestBase.is_disabled, + url=LinkPreviewOptionsTestBase.url, + prefer_small_media=LinkPreviewOptionsTestBase.prefer_small_media, + prefer_large_media=LinkPreviewOptionsTestBase.prefer_large_media, + show_above_text=LinkPreviewOptionsTestBase.show_above_text, + ) + + +class LinkPreviewOptionsTestBase: + is_disabled = True + url = "https://www.example.com" + prefer_small_media = True + prefer_large_media = False + show_above_text = True + + +class TestLinkPreviewOptionsWithoutRequest(LinkPreviewOptionsTestBase): + def test_slot_behaviour(self, link_preview_options): + a = link_preview_options + for attr in a.__slots__: + assert getattr(a, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(a)) == len(set(mro_slots(a))), "duplicate slot" + + def test_to_dict(self, link_preview_options): + link_preview_options_dict = link_preview_options.to_dict() + + assert isinstance(link_preview_options_dict, dict) + assert link_preview_options_dict["is_disabled"] == self.is_disabled + assert link_preview_options_dict["url"] == self.url + assert link_preview_options_dict["prefer_small_media"] == self.prefer_small_media + assert link_preview_options_dict["prefer_large_media"] == self.prefer_large_media + assert link_preview_options_dict["show_above_text"] == self.show_above_text + + def test_de_json(self, link_preview_options): + link_preview_options_dict = { + "is_disabled": self.is_disabled, + "url": self.url, + "prefer_small_media": self.prefer_small_media, + "prefer_large_media": self.prefer_large_media, + "show_above_text": self.show_above_text, + } + + link_preview_options = LinkPreviewOptions.de_json(link_preview_options_dict, bot=None) + assert link_preview_options.api_kwargs == {} + + assert link_preview_options.is_disabled == self.is_disabled + assert link_preview_options.url == self.url + assert link_preview_options.prefer_small_media == self.prefer_small_media + assert link_preview_options.prefer_large_media == self.prefer_large_media + assert link_preview_options.show_above_text == self.show_above_text + + def test_equality(self): + a = LinkPreviewOptions( + self.is_disabled, + self.url, + self.prefer_small_media, + self.prefer_large_media, + self.show_above_text, + ) + b = LinkPreviewOptions( + self.is_disabled, + self.url, + self.prefer_small_media, + self.prefer_large_media, + self.show_above_text, + ) + c = LinkPreviewOptions(self.is_disabled) + d = LinkPreviewOptions( + False, self.url, self.prefer_small_media, self.prefer_large_media, self.show_above_text + ) + + assert a == b + assert hash(a) == hash(b) + + assert a != c + assert hash(a) != hash(c) + + assert a != d + assert hash(a) != hash(d) diff --git a/test_loginurl.py b/test_loginurl.py new file mode 100644 index 0000000000000000000000000000000000000000..2fd84e97e23f64cbe49f66b03e54891039104eff --- /dev/null +++ b/test_loginurl.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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/]. +import pytest + +from telegram import LoginUrl +from tests.auxil.slots import mro_slots + + +@pytest.fixture(scope="module") +def login_url(): + return LoginUrl( + url=LoginUrlTestBase.url, + forward_text=LoginUrlTestBase.forward_text, + bot_username=LoginUrlTestBase.bot_username, + request_write_access=LoginUrlTestBase.request_write_access, + ) + + +class LoginUrlTestBase: + url = "http://www.google.com" + forward_text = "Send me forward!" + bot_username = "botname" + request_write_access = True + + +class TestLoginUrlWithoutRequest(LoginUrlTestBase): + def test_slot_behaviour(self, login_url): + for attr in login_url.__slots__: + assert getattr(login_url, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(login_url)) == len(set(mro_slots(login_url))), "duplicate slot" + + def test_to_dict(self, login_url): + login_url_dict = login_url.to_dict() + + assert isinstance(login_url_dict, dict) + assert login_url_dict["url"] == self.url + assert login_url_dict["forward_text"] == self.forward_text + assert login_url_dict["bot_username"] == self.bot_username + assert login_url_dict["request_write_access"] == self.request_write_access + + def test_equality(self): + a = LoginUrl(self.url, self.forward_text, self.bot_username, self.request_write_access) + b = LoginUrl(self.url, self.forward_text, self.bot_username, self.request_write_access) + c = LoginUrl(self.url) + d = LoginUrl("text.com", self.forward_text, self.bot_username, self.request_write_access) + + assert a == b + assert hash(a) == hash(b) + assert a is not b + + assert a == c + assert hash(a) == hash(c) + + assert a != d + assert hash(a) != hash(d) diff --git a/test_maybeinaccessiblemessage.py b/test_maybeinaccessiblemessage.py new file mode 100644 index 0000000000000000000000000000000000000000..da7db43ce0e58d9e69e0d06533f34e79186d2141 --- /dev/null +++ b/test_maybeinaccessiblemessage.py @@ -0,0 +1,131 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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/]. +import datetime as dtm + +import pytest + +from telegram import Chat, MaybeInaccessibleMessage +from telegram._utils.datetime import UTC, to_timestamp +from telegram.constants import ZERO_DATE +from tests.auxil.slots import mro_slots + + +@pytest.fixture(scope="class") +def maybe_inaccessible_message(): + return MaybeInaccessibleMessage( + MaybeInaccessibleMessageTestBase.chat, + MaybeInaccessibleMessageTestBase.message_id, + MaybeInaccessibleMessageTestBase.date, + ) + + +class MaybeInaccessibleMessageTestBase: + chat = Chat(1, "title") + message_id = 123 + date = dtm.datetime.now(dtm.timezone.utc).replace(microsecond=0) + + +class TestMaybeInaccessibleMessageWithoutRequest(MaybeInaccessibleMessageTestBase): + def test_slot_behaviour(self, maybe_inaccessible_message): + for attr in maybe_inaccessible_message.__slots__: + assert ( + getattr(maybe_inaccessible_message, attr, "err") != "err" + ), f"got extra slot '{attr}'" + assert len(mro_slots(maybe_inaccessible_message)) == len( + set(mro_slots(maybe_inaccessible_message)) + ), "duplicate slot" + + def test_to_dict(self, maybe_inaccessible_message): + maybe_inaccessible_message_dict = maybe_inaccessible_message.to_dict() + + assert isinstance(maybe_inaccessible_message_dict, dict) + assert maybe_inaccessible_message_dict["chat"] == self.chat.to_dict() + assert maybe_inaccessible_message_dict["message_id"] == self.message_id + assert maybe_inaccessible_message_dict["date"] == to_timestamp(self.date) + + def test_de_json(self, bot): + json_dict = { + "chat": self.chat.to_dict(), + "message_id": self.message_id, + "date": to_timestamp(self.date), + } + maybe_inaccessible_message = MaybeInaccessibleMessage.de_json(json_dict, bot) + assert maybe_inaccessible_message.api_kwargs == {} + + assert maybe_inaccessible_message.chat == self.chat + assert maybe_inaccessible_message.message_id == self.message_id + assert maybe_inaccessible_message.date == self.date + + def test_de_json_localization(self, tz_bot, bot, raw_bot): + json_dict = { + "chat": self.chat.to_dict(), + "message_id": self.message_id, + "date": to_timestamp(self.date), + } + + maybe_inaccessible_message_raw = MaybeInaccessibleMessage.de_json(json_dict, raw_bot) + maybe_inaccessible_message_bot = MaybeInaccessibleMessage.de_json(json_dict, bot) + maybe_inaccessible_message_bot_tz = MaybeInaccessibleMessage.de_json(json_dict, tz_bot) + + # comparing utcoffsets because comparing timezones is unpredicatable + maybe_inaccessible_message_bot_tz_offset = ( + maybe_inaccessible_message_bot_tz.date.utcoffset() + ) + tz_bot_offset = tz_bot.defaults.tzinfo.utcoffset( + maybe_inaccessible_message_bot_tz.date.replace(tzinfo=None) + ) + + assert maybe_inaccessible_message_raw.date.tzinfo == UTC + assert maybe_inaccessible_message_bot.date.tzinfo == UTC + assert maybe_inaccessible_message_bot_tz_offset == tz_bot_offset + + def test_de_json_zero_date(self, bot): + json_dict = { + "chat": self.chat.to_dict(), + "message_id": self.message_id, + "date": 0, + } + + maybe_inaccessible_message = MaybeInaccessibleMessage.de_json(json_dict, bot) + assert maybe_inaccessible_message.date == ZERO_DATE + assert maybe_inaccessible_message.date is ZERO_DATE + + def test_is_accessible(self): + assert MaybeInaccessibleMessage(self.chat, self.message_id, self.date).is_accessible + assert not MaybeInaccessibleMessage(self.chat, self.message_id, ZERO_DATE).is_accessible + + def test_equality(self, maybe_inaccessible_message): + a = maybe_inaccessible_message + b = MaybeInaccessibleMessage( + self.chat, self.message_id, self.date + dtm.timedelta(seconds=1) + ) + c = MaybeInaccessibleMessage(self.chat, self.message_id + 1, self.date) + d = MaybeInaccessibleMessage(Chat(2, "title"), self.message_id, self.date) + + assert a == b + assert hash(a) == hash(b) + assert a is not b + + assert a != c + assert hash(a) != hash(c) + assert a is not c + + assert a != d + assert hash(a) != hash(d) + assert a is not d diff --git a/test_menubutton.py b/test_menubutton.py new file mode 100644 index 0000000000000000000000000000000000000000..d5930f805c257305494f66203a1d8087bc2df76b --- /dev/null +++ b/test_menubutton.py @@ -0,0 +1,199 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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/]. +from copy import deepcopy + +import pytest + +from telegram import ( + Dice, + MenuButton, + MenuButtonCommands, + MenuButtonDefault, + MenuButtonWebApp, + WebAppInfo, +) +from telegram.constants import MenuButtonType +from tests.auxil.slots import mro_slots + + +@pytest.fixture( + scope="module", + params=[ + MenuButton.DEFAULT, + MenuButton.WEB_APP, + MenuButton.COMMANDS, + ], +) +def scope_type(request): + return request.param + + +@pytest.fixture( + scope="module", + params=[ + MenuButtonDefault, + MenuButtonCommands, + MenuButtonWebApp, + ], + ids=[ + MenuButton.DEFAULT, + MenuButton.COMMANDS, + MenuButton.WEB_APP, + ], +) +def scope_class(request): + return request.param + + +@pytest.fixture( + scope="module", + params=[ + (MenuButtonDefault, MenuButton.DEFAULT), + (MenuButtonCommands, MenuButton.COMMANDS), + (MenuButtonWebApp, MenuButton.WEB_APP), + ], + ids=[ + MenuButton.DEFAULT, + MenuButton.COMMANDS, + MenuButton.WEB_APP, + ], +) +def scope_class_and_type(request): + return request.param + + +@pytest.fixture(scope="module") +def menu_button(scope_class_and_type): + # We use de_json here so that we don't have to worry about which class gets which arguments + return scope_class_and_type[0].de_json( + { + "type": scope_class_and_type[1], + "text": MenuButtonTestBase.text, + "web_app": MenuButtonTestBase.web_app.to_dict(), + }, + bot=None, + ) + + +class MenuButtonTestBase: + text = "button_text" + web_app = WebAppInfo(url="https://python-telegram-bot.org/web_app") + + +# All the scope types are very similar, so we test everything via parametrization +class TestMenuButtonWithoutRequest(MenuButtonTestBase): + def test_slot_behaviour(self, menu_button): + for attr in menu_button.__slots__: + assert getattr(menu_button, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(menu_button)) == len(set(mro_slots(menu_button))), "duplicate slot" + + def test_de_json(self, bot, scope_class_and_type): + cls = scope_class_and_type[0] + type_ = scope_class_and_type[1] + + json_dict = {"type": type_, "text": self.text, "web_app": self.web_app.to_dict()} + menu_button = MenuButton.de_json(json_dict, bot) + assert set(menu_button.api_kwargs.keys()) == {"text", "web_app"} - set(cls.__slots__) + + assert isinstance(menu_button, MenuButton) + assert type(menu_button) is cls + assert menu_button.type == type_ + if "web_app" in cls.__slots__: + assert menu_button.web_app == self.web_app + if "text" in cls.__slots__: + assert menu_button.text == self.text + + assert cls.de_json(None, bot) is None + assert MenuButton.de_json({}, bot) is None + + def test_de_json_invalid_type(self, bot): + json_dict = {"type": "invalid", "text": self.text, "web_app": self.web_app.to_dict()} + menu_button = MenuButton.de_json(json_dict, bot) + assert menu_button.api_kwargs == {"text": self.text, "web_app": self.web_app.to_dict()} + + assert type(menu_button) is MenuButton + assert menu_button.type == "invalid" + + def test_de_json_subclass(self, scope_class, bot): + """This makes sure that e.g. MenuButtonDefault(data) never returns a + MenuButtonChat instance.""" + json_dict = {"type": "invalid", "text": self.text, "web_app": self.web_app.to_dict()} + assert type(scope_class.de_json(json_dict, bot)) is scope_class + + def test_de_json_empty_data(self, scope_class): + if scope_class in (MenuButtonWebApp,): + pytest.skip( + "This test is not relevant for subclasses that have more attributes than just type" + ) + assert isinstance(scope_class.de_json({}, None), scope_class) + + def test_to_dict(self, menu_button): + menu_button_dict = menu_button.to_dict() + + assert isinstance(menu_button_dict, dict) + assert menu_button_dict["type"] == menu_button.type + if hasattr(menu_button, "web_app"): + assert menu_button_dict["web_app"] == menu_button.web_app.to_dict() + if hasattr(menu_button, "text"): + assert menu_button_dict["text"] == menu_button.text + + def test_type_enum_conversion(self): + assert type(MenuButton("commands").type) is MenuButtonType + assert MenuButton("unknown").type == "unknown" + + def test_equality(self, menu_button, bot): + a = MenuButton("base_type") + b = MenuButton("base_type") + c = menu_button + d = deepcopy(menu_button) + e = Dice(4, "emoji") + + assert a == b + assert hash(a) == hash(b) + + assert a != c + assert hash(a) != hash(c) + + assert a != d + assert hash(a) != hash(d) + + assert a != e + assert hash(a) != hash(e) + + assert c == d + assert hash(c) == hash(d) + + assert c != e + assert hash(c) != hash(e) + + if hasattr(c, "web_app"): + json_dict = c.to_dict() + json_dict["web_app"] = WebAppInfo("https://foo.bar/web_app").to_dict() + f = c.__class__.de_json(json_dict, bot) + + assert c != f + assert hash(c) != hash(f) + + if hasattr(c, "text"): + json_dict = c.to_dict() + json_dict["text"] = "other text" + g = c.__class__.de_json(json_dict, bot) + + assert c != g + assert hash(c) != hash(g) diff --git a/test_message.py b/test_message.py new file mode 100644 index 0000000000000000000000000000000000000000..84b2b7c3929c656f25995ba5c74907f8e862a3a2 --- /dev/null +++ b/test_message.py @@ -0,0 +1,2803 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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/]. +from copy import copy +from datetime import datetime + +import pytest + +from telegram import ( + Animation, + Audio, + BackgroundTypeChatTheme, + Bot, + Chat, + ChatBackground, + ChatBoostAdded, + ChatShared, + Contact, + Dice, + Document, + ExternalReplyInfo, + Game, + Giveaway, + GiveawayCompleted, + GiveawayCreated, + GiveawayWinners, + Invoice, + LinkPreviewOptions, + Location, + Message, + MessageAutoDeleteTimerChanged, + MessageEntity, + MessageOriginChat, + PaidMediaInfo, + PaidMediaPreview, + PassportData, + PhotoSize, + Poll, + PollOption, + ProximityAlertTriggered, + RefundedPayment, + ReplyParameters, + SharedUser, + Sticker, + Story, + SuccessfulPayment, + TextQuote, + Update, + User, + UsersShared, + Venue, + Video, + VideoChatEnded, + VideoChatParticipantsInvited, + VideoChatScheduled, + VideoChatStarted, + VideoNote, + Voice, + WebAppData, +) +from telegram._utils.datetime import UTC +from telegram._utils.defaultvalue import DEFAULT_NONE +from telegram._utils.types import ODVInput +from telegram.constants import ChatAction, ParseMode +from telegram.ext import Defaults +from telegram.warnings import PTBDeprecationWarning +from tests._passport.test_passport import RAW_PASSPORT_DATA +from tests.auxil.bot_method_checks import ( + check_defaults_handling, + check_shortcut_call, + check_shortcut_signature, +) +from tests.auxil.build_messages import make_message +from tests.auxil.pytest_classes import PytestExtBot, PytestMessage +from tests.auxil.slots import mro_slots + + +@pytest.fixture +def message(bot): + message = PytestMessage( + message_id=MessageTestBase.id_, + date=MessageTestBase.date, + chat=copy(MessageTestBase.chat), + from_user=copy(MessageTestBase.from_user), + business_connection_id="123456789", + ) + message.set_bot(bot) + message._unfreeze() + message.chat._unfreeze() + message.from_user._unfreeze() + return message + + +@pytest.fixture( + params=[ + { + "reply_to_message": Message( + 50, datetime.utcnow(), Chat(13, "channel"), User(9, "i", False) + ) + }, + {"edit_date": datetime.utcnow()}, + { + "text": "a text message", + "entities": [MessageEntity("bold", 10, 4), MessageEntity("italic", 16, 7)], + }, + { + "caption": "A message caption", + "caption_entities": [MessageEntity("bold", 1, 1), MessageEntity("text_link", 4, 3)], + }, + {"audio": Audio("audio_id", "unique_id", 12), "caption": "audio_file"}, + {"document": Document("document_id", "unique_id"), "caption": "document_file"}, + { + "animation": Animation("animation_id", "unique_id", 30, 30, 1), + "caption": "animation_file", + }, + { + "game": Game( + "my_game", + "just my game", + [ + PhotoSize("game_photo_id", "unique_id", 30, 30), + ], + ) + }, + {"photo": [PhotoSize("photo_id", "unique_id", 50, 50)], "caption": "photo_file"}, + {"sticker": Sticker("sticker_id", "unique_id", 50, 50, True, False, Sticker.REGULAR)}, + {"story": Story(Chat(1, Chat.PRIVATE), 0)}, + {"video": Video("video_id", "unique_id", 12, 12, 12), "caption": "video_file"}, + {"voice": Voice("voice_id", "unique_id", 5)}, + {"video_note": VideoNote("video_note_id", "unique_id", 20, 12)}, + {"new_chat_members": [User(55, "new_user", False)]}, + {"contact": Contact("phone_numner", "contact_name")}, + {"location": Location(-23.691288, 46.788279)}, + {"venue": Venue(Location(-23.691288, 46.788279), "some place", "right here")}, + {"left_chat_member": User(33, "kicked", False)}, + {"new_chat_title": "new title"}, + {"new_chat_photo": [PhotoSize("photo_id", "unique_id", 50, 50)]}, + {"delete_chat_photo": True}, + {"group_chat_created": True}, + {"supergroup_chat_created": True}, + {"channel_chat_created": True}, + {"message_auto_delete_timer_changed": MessageAutoDeleteTimerChanged(42)}, + {"migrate_to_chat_id": -12345}, + {"migrate_from_chat_id": -54321}, + { + "pinned_message": Message( + 7, datetime.utcnow(), Chat(13, "channel"), User(9, "i", False) + ) + }, + {"invoice": Invoice("my invoice", "invoice", "start", "EUR", 243)}, + { + "successful_payment": SuccessfulPayment( + "EUR", 243, "payload", "charge_id", "provider_id", order_info={} + ) + }, + {"connected_website": "http://example.com/"}, + {"author_signature": "some_author_sign"}, + { + "photo": [PhotoSize("photo_id", "unique_id", 50, 50)], + "caption": "photo_file", + "media_group_id": 1234443322222, + }, + {"passport_data": PassportData.de_json(RAW_PASSPORT_DATA, None)}, + { + "poll": Poll( + id="abc", + question="What is this?", + options=[PollOption(text="a", voter_count=1), PollOption(text="b", voter_count=2)], + is_closed=False, + total_voter_count=0, + is_anonymous=False, + type=Poll.REGULAR, + allows_multiple_answers=True, + explanation_entities=[], + ) + }, + { + "text": "a text message", + "reply_markup": { + "inline_keyboard": [ + [ + {"text": "start", "url": "http://google.com"}, + {"text": "next", "callback_data": "abcd"}, + ], + [{"text": "Cancel", "callback_data": "Cancel"}], + ] + }, + }, + {"dice": Dice(4, "🎲")}, + {"via_bot": User(9, "A_Bot", True)}, + { + "proximity_alert_triggered": ProximityAlertTriggered( + User(1, "John", False), User(2, "Doe", False), 42 + ) + }, + {"video_chat_scheduled": VideoChatScheduled(datetime.utcnow())}, + {"video_chat_started": VideoChatStarted()}, + {"video_chat_ended": VideoChatEnded(100)}, + { + "video_chat_participants_invited": VideoChatParticipantsInvited( + [User(1, "Rem", False), User(2, "Emilia", False)] + ) + }, + {"sender_chat": Chat(-123, "discussion_channel")}, + {"is_automatic_forward": True}, + {"has_protected_content": True}, + { + "entities": [ + MessageEntity(MessageEntity.BOLD, 0, 1), + MessageEntity(MessageEntity.TEXT_LINK, 2, 3, url="https://ptb.org"), + ] + }, + {"web_app_data": WebAppData("some_data", "some_button_text")}, + {"message_thread_id": 123}, + {"users_shared": UsersShared(1, users=[SharedUser(2, "user2"), SharedUser(3, "user3")])}, + {"chat_shared": ChatShared(3, 4)}, + { + "giveaway": Giveaway( + chats=[Chat(1, Chat.SUPERGROUP)], + winners_selection_date=datetime.utcnow().replace(microsecond=0), + winner_count=5, + ) + }, + {"giveaway_created": GiveawayCreated()}, + { + "giveaway_winners": GiveawayWinners( + chat=Chat(1, Chat.CHANNEL), + giveaway_message_id=123456789, + winners_selection_date=datetime.utcnow().replace(microsecond=0), + winner_count=42, + winners=[User(1, "user1", False), User(2, "user2", False)], + ) + }, + { + "giveaway_completed": GiveawayCompleted( + winner_count=42, + unclaimed_prize_count=4, + giveaway_message=make_message(text="giveaway_message"), + ) + }, + { + "link_preview_options": LinkPreviewOptions( + is_disabled=True, + url="https://python-telegram-bot.org", + prefer_small_media=True, + prefer_large_media=True, + show_above_text=True, + ) + }, + { + "external_reply": ExternalReplyInfo( + MessageOriginChat(datetime.utcnow(), Chat(1, Chat.PRIVATE)) + ) + }, + {"quote": TextQuote("a text quote", 1)}, + {"forward_origin": MessageOriginChat(datetime.utcnow(), Chat(1, Chat.PRIVATE))}, + {"reply_to_story": Story(Chat(1, Chat.PRIVATE), 0)}, + {"boost_added": ChatBoostAdded(100)}, + {"sender_boost_count": 1}, + {"is_from_offline": True}, + {"sender_business_bot": User(1, "BusinessBot", True)}, + {"business_connection_id": "123456789"}, + {"chat_background_set": ChatBackground(type=BackgroundTypeChatTheme("ice"))}, + {"effect_id": "123456789"}, + {"show_caption_above_media": True}, + {"paid_media": PaidMediaInfo(5, [PaidMediaPreview(10, 10, 10)])}, + {"refunded_payment": RefundedPayment("EUR", 243, "payload", "charge_id", "provider_id")}, + ], + ids=[ + "reply", + "edited", + "text", + "caption_entities", + "audio", + "document", + "animation", + "game", + "photo", + "sticker", + "story", + "video", + "voice", + "video_note", + "new_members", + "contact", + "location", + "venue", + "left_member", + "new_title", + "new_photo", + "delete_photo", + "group_created", + "supergroup_created", + "channel_created", + "message_auto_delete_timer_changed", + "migrated_to", + "migrated_from", + "pinned", + "invoice", + "successful_payment", + "connected_website", + "author_signature", + "photo_from_media_group", + "passport_data", + "poll", + "reply_markup", + "dice", + "via_bot", + "proximity_alert_triggered", + "video_chat_scheduled", + "video_chat_started", + "video_chat_ended", + "video_chat_participants_invited", + "sender_chat", + "is_automatic_forward", + "has_protected_content", + "entities", + "web_app_data", + "message_thread_id", + "users_shared", + "chat_shared", + "giveaway", + "giveaway_created", + "giveaway_winners", + "giveaway_completed", + "link_preview_options", + "external_reply", + "quote", + "forward_origin", + "reply_to_story", + "boost_added", + "sender_boost_count", + "sender_business_bot", + "business_connection_id", + "is_from_offline", + "chat_background_set", + "effect_id", + "show_caption_above_media", + "paid_media", + "refunded_payment", + ], +) +def message_params(bot, request): + message = Message( + message_id=MessageTestBase.id_, + from_user=MessageTestBase.from_user, + date=MessageTestBase.date, + chat=MessageTestBase.chat, + **request.param, + ) + message.set_bot(bot) + return message + + +class MessageTestBase: + id_ = 1 + from_user = User(2, "testuser", False) + date = datetime.utcnow() + chat = Chat(3, "private") + test_entities = [ + {"length": 4, "offset": 10, "type": "bold"}, + {"length": 3, "offset": 16, "type": "italic"}, + {"length": 3, "offset": 20, "type": "italic"}, + {"length": 4, "offset": 25, "type": "code"}, + {"length": 5, "offset": 31, "type": "text_link", "url": "http://github.com/ab_"}, + { + "length": 12, + "offset": 38, + "type": "text_mention", + "user": User(123456789, "mentioned user", False), + }, + {"length": 3, "offset": 55, "type": "pre", "language": "python"}, + {"length": 21, "offset": 60, "type": "url"}, + ] + test_text = "Test for trgh nested in italic. Python pre. Spoiled. " + "👍.\nMultiline\nblock quote\nwith nested.\n\nMultiline\nexpandable\nblock quote." + ) + test_message = Message( + message_id=1, + from_user=None, + date=None, + chat=None, + text=test_text, + entities=[MessageEntity(**e) for e in test_entities], + caption=test_text, + caption_entities=[MessageEntity(**e) for e in test_entities], + ) + test_message_v2 = Message( + message_id=1, + from_user=None, + date=None, + chat=None, + text=test_text_v2, + entities=[MessageEntity(**e) for e in test_entities_v2], + caption=test_text_v2, + caption_entities=[MessageEntity(**e) for e in test_entities_v2], + ) + + +class TestMessageWithoutRequest(MessageTestBase): + @staticmethod + async def check_quote_parsing( + message: Message, method, bot_method_name: str, args, monkeypatch + ): + """Used in testing reply_* below. Makes sure that quote and do_quote are handled + correctly + """ + with pytest.raises(ValueError, match="`quote` and `do_quote` are mutually exclusive"): + await method(*args, quote=True, do_quote=True) + + with pytest.warns(PTBDeprecationWarning, match="`quote` parameter is deprecated"): + await method(*args, quote=True) + + with pytest.raises( + ValueError, + match="`reply_to_message_id` and `reply_parameters` are mutually exclusive.", + ): + await method(*args, reply_to_message_id=42, reply_parameters=42) + + async def make_assertion(*args, **kwargs): + return kwargs.get("chat_id"), kwargs.get("reply_parameters") + + monkeypatch.setattr(message.get_bot(), bot_method_name, make_assertion) + + for param in ("quote", "do_quote"): + chat_id, reply_parameters = await method(*args, **{param: True}) + if chat_id != message.chat.id: + pytest.fail(f"chat_id is {chat_id} but should be {message.chat.id}") + if reply_parameters is None or reply_parameters.message_id != message.message_id: + pytest.fail( + f"reply_parameters is {reply_parameters} but should be {message.message_id}" + ) + + input_chat_id = object() + input_reply_parameters = ReplyParameters(message_id=1, chat_id=42) + chat_id, reply_parameters = await method( + *args, do_quote={"chat_id": input_chat_id, "reply_parameters": input_reply_parameters} + ) + if chat_id is not input_chat_id: + pytest.fail(f"chat_id is {chat_id} but should be {chat_id}") + if reply_parameters is not input_reply_parameters: + pytest.fail(f"reply_parameters is {reply_parameters} but should be {reply_parameters}") + + input_parameters_2 = ReplyParameters(message_id=2, chat_id=43) + chat_id, reply_parameters = await method( + *args, + reply_parameters=input_parameters_2, + # passing these here to make sure that `reply_parameters` has higher priority + do_quote={"chat_id": input_chat_id, "reply_parameters": input_reply_parameters}, + ) + if chat_id is not message.chat.id: + pytest.fail(f"chat_id is {chat_id} but should be {message.chat.id}") + if reply_parameters is not input_parameters_2: + pytest.fail( + f"reply_parameters is {reply_parameters} but should be {input_parameters_2}" + ) + + chat_id, reply_parameters = await method( + *args, + reply_to_message_id=42, + # passing these here to make sure that `reply_to_message_id` has higher priority + do_quote={"chat_id": input_chat_id, "reply_parameters": input_reply_parameters}, + ) + if chat_id != message.chat.id: + pytest.fail(f"chat_id is {chat_id} but should be {message.chat.id}") + if reply_parameters is None or reply_parameters.message_id != 42: + pytest.fail(f"reply_parameters is {reply_parameters} but should be 42") + + @staticmethod + async def check_thread_id_parsing( + message: Message, method, bot_method_name: str, args, monkeypatch + ): + """Used in testing reply_* below. Makes sure that meassage_thread_id is parsed + correctly.""" + + async def extract_message_thread_id(*args, **kwargs): + return kwargs.get("message_thread_id") + + monkeypatch.setattr(message.get_bot(), bot_method_name, extract_message_thread_id) + + for is_topic_message in (True, False): + message.is_topic_message = is_topic_message + + message.message_thread_id = None + message_thread_id = await method(*args) + assert message_thread_id is None + + message.message_thread_id = 99 + message_thread_id = await method(*args) + assert message_thread_id == (99 if is_topic_message else None) + + message_thread_id = await method(*args, message_thread_id=50) + assert message_thread_id == 50 + + message_thread_id = await method(*args, message_thread_id=None) + assert message_thread_id is None + + if bot_method_name == "send_chat_action": + return + + message_thread_id = await method( + *args, + do_quote=message.build_reply_arguments( + target_chat_id=123, + ), + ) + assert message_thread_id is None + + for target_chat_id in (message.chat_id, message.chat.username): + message_thread_id = await method( + *args, + do_quote=message.build_reply_arguments( + target_chat_id=target_chat_id, + ), + ) + assert message_thread_id == (message.message_thread_id if is_topic_message else None) + + def test_slot_behaviour(self): + message = Message( + message_id=MessageTestBase.id_, + date=MessageTestBase.date, + chat=copy(MessageTestBase.chat), + from_user=copy(MessageTestBase.from_user), + ) + for attr in message.__slots__: + assert getattr(message, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(message)) == len(set(mro_slots(message))), "duplicate slot" + + def test_all_possibilities_de_json_and_to_dict(self, bot, message_params): + new = Message.de_json(message_params.to_dict(), bot) + assert new.api_kwargs == {} + assert new.to_dict() == message_params.to_dict() + + # Checking that none of the attributes are dicts is a best effort approach to ensure that + # de_json converts everything to proper classes without having to write special tests for + # every single case + for slot in new.__slots__: + assert not isinstance(new[slot], dict) + + def test_de_json_localization(self, bot, raw_bot, tz_bot): + json_dict = { + "message_id": 12, + "from_user": None, + "date": int(datetime.now().timestamp()), + "chat": None, + "edit_date": int(datetime.now().timestamp()), + } + + message_raw = Message.de_json(json_dict, raw_bot) + message_bot = Message.de_json(json_dict, bot) + message_tz = Message.de_json(json_dict, tz_bot) + + # comparing utcoffsets because comparing timezones is unpredicatable + date_offset = message_tz.date.utcoffset() + date_tz_bot_offset = tz_bot.defaults.tzinfo.utcoffset(message_tz.date.replace(tzinfo=None)) + + edit_date_offset = message_tz.edit_date.utcoffset() + edit_date_tz_bot_offset = tz_bot.defaults.tzinfo.utcoffset( + message_tz.edit_date.replace(tzinfo=None) + ) + + assert message_raw.date.tzinfo == UTC + assert message_bot.date.tzinfo == UTC + assert date_offset == date_tz_bot_offset + + assert message_raw.edit_date.tzinfo == UTC + assert message_bot.edit_date.tzinfo == UTC + assert edit_date_offset == edit_date_tz_bot_offset + + def test_de_json_api_kwargs_backward_compatibility(self, bot, message_params): + message_dict = message_params.to_dict() + keys = ( + "user_shared", + "forward_from", + "forward_from_chat", + "forward_from_message_id", + "forward_signature", + "forward_sender_name", + "forward_date", + ) + for key in keys: + message_dict[key] = key + message = Message.de_json(message_dict, bot) + assert message.api_kwargs == {key: key for key in keys} + + def test_equality(self): + id_ = 1 + a = Message(id_, self.date, self.chat, from_user=self.from_user) + b = Message(id_, self.date, self.chat, from_user=self.from_user) + c = Message(id_, self.date, Chat(123, Chat.GROUP), from_user=User(0, "", False)) + d = Message(0, self.date, self.chat, from_user=self.from_user) + e = Update(id_) + + assert a == b + assert hash(a) == hash(b) + assert a is not b + + assert a != c + assert hash(a) != hash(c) + + assert a != d + assert hash(a) != hash(d) + + assert a != e + assert hash(a) != hash(e) + + def test_bool(self, message, recwarn): + # Relevant as long as we override MaybeInaccessibleMessage.__bool__ + # Can be removed once that's removed + assert bool(message) is True + assert len(recwarn) == 0 + + async def test_parse_entity(self): + text = ( + b"\\U0001f469\\u200d\\U0001f469\\u200d\\U0001f467" + b"\\u200d\\U0001f467\\U0001f431http://google.com" + ).decode("unicode-escape") + entity = MessageEntity(type=MessageEntity.URL, offset=13, length=17) + message = Message( + 1, + from_user=self.from_user, + date=self.date, + chat=self.chat, + text=text, + entities=[entity], + ) + assert message.parse_entity(entity) == "http://google.com" + + with pytest.raises(RuntimeError, match="Message has no"): + Message(message_id=1, date=self.date, chat=self.chat).parse_entity(entity) + + async def test_parse_caption_entity(self): + caption = ( + b"\\U0001f469\\u200d\\U0001f469\\u200d\\U0001f467" + b"\\u200d\\U0001f467\\U0001f431http://google.com" + ).decode("unicode-escape") + entity = MessageEntity(type=MessageEntity.URL, offset=13, length=17) + message = Message( + 1, + from_user=self.from_user, + date=self.date, + chat=self.chat, + caption=caption, + caption_entities=[entity], + ) + assert message.parse_caption_entity(entity) == "http://google.com" + + with pytest.raises(RuntimeError, match="Message has no"): + Message(message_id=1, date=self.date, chat=self.chat).parse_entity(entity) + + async def test_parse_entities(self): + text = ( + b"\\U0001f469\\u200d\\U0001f469\\u200d\\U0001f467" + b"\\u200d\\U0001f467\\U0001f431http://google.com" + ).decode("unicode-escape") + entity = MessageEntity(type=MessageEntity.URL, offset=13, length=17) + entity_2 = MessageEntity(type=MessageEntity.BOLD, offset=13, length=1) + message = Message( + 1, + from_user=self.from_user, + date=self.date, + chat=self.chat, + text=text, + entities=[entity_2, entity], + ) + assert message.parse_entities(MessageEntity.URL) == {entity: "http://google.com"} + assert message.parse_entities() == {entity: "http://google.com", entity_2: "h"} + + async def test_parse_caption_entities(self): + text = ( + b"\\U0001f469\\u200d\\U0001f469\\u200d\\U0001f467" + b"\\u200d\\U0001f467\\U0001f431http://google.com" + ).decode("unicode-escape") + entity = MessageEntity(type=MessageEntity.URL, offset=13, length=17) + entity_2 = MessageEntity(type=MessageEntity.BOLD, offset=13, length=1) + message = Message( + 1, + from_user=self.from_user, + date=self.date, + chat=self.chat, + caption=text, + caption_entities=[entity_2, entity], + ) + assert message.parse_caption_entities(MessageEntity.URL) == {entity: "http://google.com"} + assert message.parse_caption_entities() == { + entity: "http://google.com", + entity_2: "h", + } + + def test_text_html_simple(self): + test_html_string = ( + "Test for <bold, ita_lic, " + r"\`code, " + r'links, ' + 'text-mention and ' + r"
`\pre
. http://google.com " + "and bold nested in strk>trgh nested in italic. " + '
Python pre
. ' + 'Spoiled. ' + '👍.\n' + "
Multiline\nblock quote\nwith nested.
\n\n" + "
Multiline\nexpandable\nblock quote.
" + ) + text_html = self.test_message_v2.text_html + assert text_html == test_html_string + + def test_text_html_empty(self, message): + message.text = None + message.caption = "test" + assert message.text_html is None + + def test_text_html_urled(self): + test_html_string = ( + "Test for <bold, ita_lic, " + r"\`code, " + r'links, ' + 'text-mention and ' + r'
`\pre
. http://google.com ' + "and bold nested in strk>trgh nested in italic. " + '
Python pre
. ' + 'Spoiled. ' + '👍.\n' + "
Multiline\nblock quote\nwith nested.
\n\n" + "
Multiline\nexpandable\nblock quote.
" + ) + text_html = self.test_message_v2.text_html_urled + assert text_html == test_html_string + + def test_text_markdown_simple(self): + test_md_string = ( + r"Test for <*bold*, _ita_\__lic_, `code`, " + "[links](http://github.com/ab_), " + "[text-mention](tg://user?id=123456789) and ```python\npre```. " + r"http://google.com/ab\_" + ) + text_markdown = self.test_message.text_markdown + assert text_markdown == test_md_string + + def test_text_markdown_v2_simple(self): + test_md_string = ( + r"__Test__ for <*bold*, _ita\_lic_, `\\\`code`, " + "[links](http://github.com/abc\\\\\\)def), " + "[text\\-mention](tg://user?id=123456789) and ```\\`\\\\pre```\\. " + r"http://google\.com and _bold *nested in ~strk\>trgh~ nested in* italic_\. " + "```python\nPython pre```\\. ||Spoiled||\\. ![👍](tg://emoji?id=1)\\.\n" + ">Multiline\n" + ">block quote\n" + r">with *nested*\." + "\n\n>Multiline\n" + ">expandable\n" + r">block quote\.||" + ) + text_markdown = self.test_message_v2.text_markdown_v2 + assert text_markdown == test_md_string + + @pytest.mark.parametrize( + "entity_type", + [ + MessageEntity.UNDERLINE, + MessageEntity.STRIKETHROUGH, + MessageEntity.SPOILER, + MessageEntity.BLOCKQUOTE, + MessageEntity.CUSTOM_EMOJI, + ], + ) + def test_text_markdown_new_in_v2(self, message, entity_type): + message.text = "test" + message.entities = [ + MessageEntity(MessageEntity.BOLD, offset=0, length=4), + MessageEntity(MessageEntity.ITALIC, offset=0, length=4), + ] + with pytest.raises(ValueError, match="Nested entities are not supported for"): + assert message.text_markdown + + message.entities = [MessageEntity(entity_type, offset=0, length=4)] + with pytest.raises(ValueError, match="entities are not supported for"): + message.text_markdown + + message.entities = [] + + def test_text_markdown_empty(self, message): + message.text = None + message.caption = "test" + assert message.text_markdown is None + assert message.text_markdown_v2 is None + + def test_text_markdown_urled(self): + test_md_string = ( + r"Test for <*bold*, _ita_\__lic_, `code`, " + "[links](http://github.com/ab_), " + "[text-mention](tg://user?id=123456789) and ```python\npre```. " + "[http://google.com/ab_](http://google.com/ab_)" + ) + text_markdown = self.test_message.text_markdown_urled + assert text_markdown == test_md_string + + def test_text_markdown_v2_urled(self): + test_md_string = ( + r"__Test__ for <*bold*, _ita\_lic_, `\\\`code`, " + "[links](http://github.com/abc\\\\\\)def), " + "[text\\-mention](tg://user?id=123456789) and ```\\`\\\\pre```\\. " + r"[http://google\.com](http://google.com) and _bold *nested in ~strk\>trgh~ " + "nested in* italic_\\. ```python\nPython pre```\\. ||Spoiled||\\. " + "![👍](tg://emoji?id=1)\\.\n" + ">Multiline\n" + ">block quote\n" + r">with *nested*\." + "\n\n>Multiline\n" + ">expandable\n" + r">block quote\.||" + ) + text_markdown = self.test_message_v2.text_markdown_v2_urled + assert text_markdown == test_md_string + + def test_text_html_emoji(self): + text = b"\\U0001f469\\u200d\\U0001f469\\u200d ABC".decode("unicode-escape") + expected = b"\\U0001f469\\u200d\\U0001f469\\u200d ABC".decode("unicode-escape") + bold_entity = MessageEntity(type=MessageEntity.BOLD, offset=7, length=3) + message = Message( + 1, + from_user=self.from_user, + date=self.date, + chat=self.chat, + text=text, + entities=[bold_entity], + ) + assert expected == message.text_html + + def test_text_markdown_emoji(self): + text = b"\\U0001f469\\u200d\\U0001f469\\u200d ABC".decode("unicode-escape") + expected = b"\\U0001f469\\u200d\\U0001f469\\u200d *ABC*".decode("unicode-escape") + bold_entity = MessageEntity(type=MessageEntity.BOLD, offset=7, length=3) + message = Message( + 1, self.date, self.chat, self.from_user, text=text, entities=[bold_entity] + ) + assert expected == message.text_markdown + + @pytest.mark.parametrize( + "type_", + argvalues=[ + "text_markdown", + "text_markdown_urled", + ], + ) + def test_text_custom_emoji_md_v1(self, type_, recwarn): + text = "Look a custom emoji: 😎" + emoji_entity = MessageEntity( + type=MessageEntity.CUSTOM_EMOJI, + offset=21, + length=2, + custom_emoji_id="5472409228461217725", + ) + message = Message( + 1, + from_user=self.from_user, + date=self.date, + chat=self.chat, + text=text, + entities=[emoji_entity], + ) + with pytest.raises(ValueError, match="Custom Emoji entities are not supported for"): + getattr(message, type_) + + @pytest.mark.parametrize( + "type_", + argvalues=[ + "text_markdown_v2", + "text_markdown_v2_urled", + ], + ) + def test_text_custom_emoji_md_v2(self, type_): + text = "Look a custom emoji: 😎" + expected = "Look a custom emoji: ![😎](tg://emoji?id=5472409228461217725)" + emoji_entity = MessageEntity( + type=MessageEntity.CUSTOM_EMOJI, + offset=21, + length=2, + custom_emoji_id="5472409228461217725", + ) + message = Message( + 1, + from_user=self.from_user, + date=self.date, + chat=self.chat, + text=text, + entities=[emoji_entity], + ) + assert expected == message[type_] + + @pytest.mark.parametrize( + "type_", + argvalues=[ + "text_html", + "text_html_urled", + ], + ) + def test_text_custom_emoji_html(self, type_): + text = "Look a custom emoji: 😎" + expected = 'Look a custom emoji: 😎' + emoji_entity = MessageEntity( + type=MessageEntity.CUSTOM_EMOJI, + offset=21, + length=2, + custom_emoji_id="5472409228461217725", + ) + message = Message( + 1, + from_user=self.from_user, + date=self.date, + chat=self.chat, + text=text, + entities=[emoji_entity], + ) + assert expected == message[type_] + + def test_caption_html_simple(self): + test_html_string = ( + "Test for <bold, ita_lic, " + r"\`code, " + r'links, ' + 'text-mention and ' + r"
`\pre
. http://google.com " + "and bold nested in strk>trgh nested in italic. " + '
Python pre
. ' + 'Spoiled. ' + '👍.\n' + "
Multiline\nblock quote\nwith nested.
\n\n" + "
Multiline\nexpandable\nblock quote.
" + ) + caption_html = self.test_message_v2.caption_html + assert caption_html == test_html_string + + def test_caption_html_empty(self, message): + message.text = "test" + message.caption = None + assert message.caption_html is None + + def test_caption_html_urled(self): + test_html_string = ( + "Test for <bold, ita_lic, " + r"\`code, " + r'links, ' + 'text-mention and ' + r'
`\pre
. http://google.com ' + "and bold nested in strk>trgh nested in italic. " + '
Python pre
. ' + 'Spoiled. ' + '👍.\n' + "
Multiline\nblock quote\nwith nested.
\n\n" + "
Multiline\nexpandable\nblock quote.
" + ) + caption_html = self.test_message_v2.caption_html_urled + assert caption_html == test_html_string + + def test_caption_markdown_simple(self): + test_md_string = ( + r"Test for <*bold*, _ita_\__lic_, `code`, " + "[links](http://github.com/ab_), " + "[text-mention](tg://user?id=123456789) and ```python\npre```. " + r"http://google.com/ab\_" + ) + caption_markdown = self.test_message.caption_markdown + assert caption_markdown == test_md_string + + def test_caption_markdown_v2_simple(self): + test_md_string = ( + r"__Test__ for <*bold*, _ita\_lic_, `\\\`code`, " + "[links](http://github.com/abc\\\\\\)def), " + "[text\\-mention](tg://user?id=123456789) and ```\\`\\\\pre```\\. " + r"http://google\.com and _bold *nested in ~strk\>trgh~ nested in* italic_\. " + "```python\nPython pre```\\. ||Spoiled||\\. ![👍](tg://emoji?id=1)\\.\n" + ">Multiline\n" + ">block quote\n" + r">with *nested*\." + "\n\n>Multiline\n" + ">expandable\n" + r">block quote\.||" + ) + caption_markdown = self.test_message_v2.caption_markdown_v2 + assert caption_markdown == test_md_string + + def test_caption_markdown_empty(self, message): + message.text = "test" + message.caption = None + assert message.caption_markdown is None + assert message.caption_markdown_v2 is None + + def test_caption_markdown_urled(self): + test_md_string = ( + r"Test for <*bold*, _ita_\__lic_, `code`, " + "[links](http://github.com/ab_), " + "[text-mention](tg://user?id=123456789) and ```python\npre```. " + "[http://google.com/ab_](http://google.com/ab_)" + ) + caption_markdown = self.test_message.caption_markdown_urled + assert caption_markdown == test_md_string + + def test_caption_markdown_v2_urled(self): + test_md_string = ( + r"__Test__ for <*bold*, _ita\_lic_, `\\\`code`, " + "[links](http://github.com/abc\\\\\\)def), " + "[text\\-mention](tg://user?id=123456789) and ```\\`\\\\pre```\\. " + r"[http://google\.com](http://google.com) and _bold *nested in ~strk\>trgh~ " + "nested in* italic_\\. ```python\nPython pre```\\. ||Spoiled||\\. " + "![👍](tg://emoji?id=1)\\.\n" + ">Multiline\n" + ">block quote\n" + r">with *nested*\." + "\n\n>Multiline\n" + ">expandable\n" + r">block quote\.||" + ) + caption_markdown = self.test_message_v2.caption_markdown_v2_urled + assert caption_markdown == test_md_string + + def test_caption_html_emoji(self): + caption = b"\\U0001f469\\u200d\\U0001f469\\u200d ABC".decode("unicode-escape") + expected = b"\\U0001f469\\u200d\\U0001f469\\u200d ABC".decode("unicode-escape") + bold_entity = MessageEntity(type=MessageEntity.BOLD, offset=7, length=3) + message = Message( + 1, + from_user=self.from_user, + date=self.date, + chat=self.chat, + caption=caption, + caption_entities=[bold_entity], + ) + assert expected == message.caption_html + + def test_caption_markdown_emoji(self): + caption = b"\\U0001f469\\u200d\\U0001f469\\u200d ABC".decode("unicode-escape") + expected = b"\\U0001f469\\u200d\\U0001f469\\u200d *ABC*".decode("unicode-escape") + bold_entity = MessageEntity(type=MessageEntity.BOLD, offset=7, length=3) + message = Message( + 1, + from_user=self.from_user, + date=self.date, + chat=self.chat, + caption=caption, + caption_entities=[bold_entity], + ) + assert expected == message.caption_markdown + + @pytest.mark.parametrize( + "type_", + argvalues=[ + "caption_markdown", + "caption_markdown_urled", + ], + ) + def test_caption_custom_emoji_md_v1(self, type_, recwarn): + caption = "Look a custom emoji: 😎" + emoji_entity = MessageEntity( + type=MessageEntity.CUSTOM_EMOJI, + offset=21, + length=2, + custom_emoji_id="5472409228461217725", + ) + message = Message( + 1, + from_user=self.from_user, + date=self.date, + chat=self.chat, + caption=caption, + caption_entities=[emoji_entity], + ) + with pytest.raises(ValueError, match="Custom Emoji entities are not supported for"): + getattr(message, type_) + + @pytest.mark.parametrize( + "type_", + argvalues=[ + "caption_markdown_v2", + "caption_markdown_v2_urled", + ], + ) + def test_caption_custom_emoji_md_v2(self, type_): + caption = "Look a custom emoji: 😎" + expected = "Look a custom emoji: ![😎](tg://emoji?id=5472409228461217725)" + emoji_entity = MessageEntity( + type=MessageEntity.CUSTOM_EMOJI, + offset=21, + length=2, + custom_emoji_id="5472409228461217725", + ) + message = Message( + 1, + from_user=self.from_user, + date=self.date, + chat=self.chat, + caption=caption, + caption_entities=[emoji_entity], + ) + assert expected == message[type_] + + @pytest.mark.parametrize( + "type_", + argvalues=[ + "caption_html", + "caption_html_urled", + ], + ) + def test_caption_custom_emoji_html(self, type_): + caption = "Look a custom emoji: 😎" + expected = 'Look a custom emoji: 😎' + emoji_entity = MessageEntity( + type=MessageEntity.CUSTOM_EMOJI, + offset=21, + length=2, + custom_emoji_id="5472409228461217725", + ) + message = Message( + 1, + from_user=self.from_user, + date=self.date, + chat=self.chat, + caption=caption, + caption_entities=[emoji_entity], + ) + assert expected == message[type_] + + async def test_parse_entities_url_emoji(self): + url = b"http://github.com/?unicode=\\u2713\\U0001f469".decode("unicode-escape") + text = "some url" + link_entity = MessageEntity(type=MessageEntity.URL, offset=0, length=8, url=url) + message = Message( + 1, self.from_user, self.date, self.chat, text=text, entities=[link_entity] + ) + assert message.parse_entities() == {link_entity: text} + assert next(iter(message.parse_entities())).url == url + + def test_chat_id(self, message): + assert message.chat_id == message.chat.id + + def test_id(self, message): + assert message.message_id == message.id + + @pytest.mark.parametrize("type_", argvalues=[Chat.SUPERGROUP, Chat.CHANNEL]) + def test_link_with_username(self, message, type_): + message.chat.username = "username" + message.chat.type = type_ + assert message.link == f"https://t.me/{message.chat.username}/{message.message_id}" + + @pytest.mark.parametrize( + ("type_", "id_"), argvalues=[(Chat.CHANNEL, -1003), (Chat.SUPERGROUP, -1003)] + ) + def test_link_with_id(self, message, type_, id_): + message.chat.username = None + message.chat.id = id_ + message.chat.type = type_ + # The leading - for group ids/ -100 for supergroup ids isn't supposed to be in the link + assert message.link == f"https://t.me/c/{3}/{message.message_id}" + + @pytest.mark.parametrize("type_", argvalues=[Chat.SUPERGROUP, Chat.CHANNEL]) + def test_link_with_topics(self, message, type_): + message.chat.username = None + message.chat.id = -1003 + message.chat.type = type_ + message.is_topic_message = True + message.message_thread_id = 123 + assert message.link == f"https://t.me/c/3/{message.message_id}?thread=123" + + @pytest.mark.parametrize("type_", argvalues=[Chat.SUPERGROUP, Chat.CHANNEL]) + def test_link_with_reply(self, message, type_): + message.chat.username = None + message.chat.id = -1003 + message.chat.type = type_ + message.reply_to_message = Message(7, self.from_user, self.date, self.chat, text="Reply") + message.message_thread_id = 123 + assert message.link == f"https://t.me/c/3/{message.message_id}?thread=123" + + @pytest.mark.parametrize(("id_", "username"), argvalues=[(None, "username"), (-3, None)]) + def test_link_private_chats(self, message, id_, username): + message.chat.type = Chat.PRIVATE + message.chat.id = id_ + message.chat.username = username + assert message.link is None + message.chat.type = Chat.GROUP + assert message.link is None + + def test_effective_attachment(self, message_params): + # This list is hard coded on purpose because just using constants.MessageAttachmentType + # (which is used in Message.effective_message) wouldn't find any mistakes + expected_attachment_types = [ + "animation", + "audio", + "contact", + "dice", + "document", + "game", + "invoice", + "location", + "paid_media", + "passport_data", + "photo", + "poll", + "sticker", + "story", + "successful_payment", + "video", + "video_note", + "voice", + "venue", + ] + + for _ in range(3): + # We run the same test multiple times to make sure that the caching is tested + + attachment = message_params.effective_attachment + if attachment: + condition = any( + message_params[message_type] is attachment + for message_type in expected_attachment_types + ) + assert condition, "Got effective_attachment for unexpected type" + else: + condition = any( + message_params[message_type] for message_type in expected_attachment_types + ) + assert not condition, "effective_attachment was None even though it should not be" + + def test_compute_quote_position_and_entities_false_index(self, message): + message.text = "AA" + with pytest.raises( + ValueError, + match="You requested the 5-th occurrence of 'A', " + "but this text appears only 2 times.", + ): + message.compute_quote_position_and_entities("A", 5) + + def test_compute_quote_position_and_entities_no_text_or_caption(self, message): + message.text = None + message.caption = None + with pytest.raises( + RuntimeError, + match="This message has neither text nor caption.", + ): + message.compute_quote_position_and_entities("A", 5) + + @pytest.mark.parametrize( + ("text", "quote", "index", "expected"), + argvalues=[ + ("AA", "A", None, 0), + ("AA", "A", 0, 0), + ("AA", "A", 1, 1), + ("ABC ABC ABC ABC", "ABC", None, 0), + ("ABC ABC ABC ABC", "ABC", 0, 0), + ("ABC ABC ABC ABC", "ABC", 3, 12), + ("👨‍👨‍👧👨‍👨‍👧👨‍👨‍👧👨‍👨‍👧", "👨‍👨‍👧", 0, 0), + ("👨‍👨‍👧👨‍👨‍👧👨‍👨‍👧👨‍👨‍👧", "👨‍👨‍👧", 3, 24), + ("👨‍👨‍👧👨‍👨‍👧👨‍👨‍👧👨‍👨‍👧", "👨", 1, 3), + ("👨‍👨‍👧👨‍👨‍👧👨‍👨‍👧👨‍👨‍👧", "👧", 2, 22), + ], + ) + @pytest.mark.parametrize("caption", [True, False]) + def test_compute_quote_position_and_entities_position( + self, message, text, quote, index, expected, caption + ): + if caption: + message.caption = text + message.text = None + else: + message.text = text + message.caption = None + + assert message.compute_quote_position_and_entities(quote, index)[0] == expected + + def test_compute_quote_position_and_entities_entities(self, message): + message.text = "A A A" + message.entities = () + assert message.compute_quote_position_and_entities("A", 0)[1] is None + + message.entities = ( + # covers complete string + MessageEntity(type=MessageEntity.BOLD, offset=0, length=6), + # covers first 2 As only + MessageEntity(type=MessageEntity.ITALIC, offset=0, length=3), + # covers second 2 As only + MessageEntity(type=MessageEntity.UNDERLINE, offset=2, length=3), + # covers middle A only + MessageEntity(type=MessageEntity.STRIKETHROUGH, offset=2, length=1), + # covers only whitespace, should be ignored + MessageEntity(type=MessageEntity.CODE, offset=1, length=1), + ) + + assert message.compute_quote_position_and_entities("A", 0)[1] == ( + MessageEntity(type=MessageEntity.BOLD, offset=0, length=1), + MessageEntity(type=MessageEntity.ITALIC, offset=0, length=1), + ) + + assert message.compute_quote_position_and_entities("A", 1)[1] == ( + MessageEntity(type=MessageEntity.BOLD, offset=0, length=1), + MessageEntity(type=MessageEntity.ITALIC, offset=0, length=1), + MessageEntity(type=MessageEntity.UNDERLINE, offset=0, length=1), + MessageEntity(type=MessageEntity.STRIKETHROUGH, offset=0, length=1), + ) + + assert message.compute_quote_position_and_entities("A", 2)[1] == ( + MessageEntity(type=MessageEntity.BOLD, offset=0, length=1), + MessageEntity(type=MessageEntity.UNDERLINE, offset=0, length=1), + ) + + @pytest.mark.parametrize( + ("target_chat_id", "expected"), + argvalues=[ + (None, 3), + (3, 3), + (-1003, -1003), + ("@username", "@username"), + ], + ) + def test_build_reply_arguments_chat_id_and_message_id(self, message, target_chat_id, expected): + message.chat.id = 3 + reply_kwargs = message.build_reply_arguments(target_chat_id=target_chat_id) + assert reply_kwargs["chat_id"] == expected + assert reply_kwargs["reply_parameters"].chat_id == (None if expected == 3 else 3) + assert reply_kwargs["reply_parameters"].message_id == message.message_id + + @pytest.mark.parametrize( + ("target_chat_id", "message_thread_id", "expected"), + argvalues=[ + (None, None, True), + (None, 123, True), + (None, 0, False), + (None, -1, False), + (3, None, True), + (3, 123, True), + (3, 0, False), + (3, -1, False), + (-1003, None, False), + (-1003, 123, False), + (-1003, 0, False), + (-1003, -1, False), + ("@username", None, True), + ("@username", 123, True), + ("@username", 0, False), + ("@username", -1, False), + ("@other_username", None, False), + ("@other_username", 123, False), + ("@other_username", 0, False), + ("@other_username", -1, False), + ], + ) + def test_build_reply_arguments_aswr( + self, message, target_chat_id, message_thread_id, expected + ): + message.chat.id = 3 + message.chat.username = "username" + message.message_thread_id = 123 + assert ( + message.build_reply_arguments( + target_chat_id=target_chat_id, message_thread_id=message_thread_id + )["reply_parameters"].allow_sending_without_reply + is not None + ) == expected + + assert ( + message.build_reply_arguments( + target_chat_id=target_chat_id, + message_thread_id=message_thread_id, + allow_sending_without_reply="custom", + )["reply_parameters"].allow_sending_without_reply + ) == ("custom" if expected else None) + + def test_build_reply_arguments_quote(self, message, monkeypatch): + reply_parameters = message.build_reply_arguments()["reply_parameters"] + assert reply_parameters.quote is None + assert reply_parameters.quote_entities == () + assert reply_parameters.quote_position is None + assert not reply_parameters.quote_parse_mode + + quote_obj = object() + quote_index = object() + quote_entities = (object(), object()) + quote_position = object() + + def mock_compute(quote, index): + if quote is quote_obj and index is quote_index: + return quote_position, quote_entities + return False, False + + monkeypatch.setattr(message, "compute_quote_position_and_entities", mock_compute) + reply_parameters = message.build_reply_arguments(quote=quote_obj, quote_index=quote_index)[ + "reply_parameters" + ] + + assert reply_parameters.quote is quote_obj + assert reply_parameters.quote_entities is quote_entities + assert reply_parameters.quote_position is quote_position + assert not reply_parameters.quote_parse_mode + + async def test_reply_text(self, monkeypatch, message): + async def make_assertion(*_, **kwargs): + id_ = kwargs["chat_id"] == message.chat_id + text = kwargs["text"] == "test" + return id_ and text + + assert check_shortcut_signature( + Message.reply_text, + Bot.send_message, + ["chat_id", "reply_to_message_id", "business_connection_id"], + ["quote", "do_quote", "reply_to_message_id"], + annotation_overrides={"message_thread_id": (ODVInput[int], DEFAULT_NONE)}, + ) + assert await check_shortcut_call( + message.reply_text, + message.get_bot(), + "send_message", + skip_params=["reply_to_message_id"], + shortcut_kwargs=["business_connection_id"], + ) + assert await check_defaults_handling( + message.reply_text, message.get_bot(), no_default_kwargs={"message_thread_id"} + ) + + monkeypatch.setattr(message.get_bot(), "send_message", make_assertion) + assert await message.reply_text("test") + await self.check_quote_parsing( + message, message.reply_text, "send_message", ["test"], monkeypatch + ) + + await self.check_thread_id_parsing( + message, message.reply_text, "send_message", ["test"], monkeypatch + ) + + async def test_reply_markdown(self, monkeypatch, message): + test_md_string = ( + r"Test for <*bold*, _ita_\__lic_, `code`, " + "[links](http://github.com/ab_), " + "[text-mention](tg://user?id=123456789) and ```python\npre```. " + r"http://google.com/ab\_" + ) + + async def make_assertion(*_, **kwargs): + cid = kwargs["chat_id"] == message.chat_id + markdown_text = kwargs["text"] == test_md_string + markdown_enabled = kwargs["parse_mode"] == ParseMode.MARKDOWN + return all([cid, markdown_text, markdown_enabled]) + + assert check_shortcut_signature( + Message.reply_markdown, + Bot.send_message, + ["chat_id", "parse_mode", "reply_to_message_id", "business_connection_id"], + ["quote", "do_quote", "reply_to_message_id"], + annotation_overrides={"message_thread_id": (ODVInput[int], DEFAULT_NONE)}, + ) + assert await check_shortcut_call( + message.reply_text, + message.get_bot(), + "send_message", + skip_params=["reply_to_message_id"], + shortcut_kwargs=["business_connection_id"], + ) + assert await check_defaults_handling( + message.reply_text, message.get_bot(), no_default_kwargs={"message_thread_id"} + ) + + text_markdown = self.test_message.text_markdown + assert text_markdown == test_md_string + + monkeypatch.setattr(message.get_bot(), "send_message", make_assertion) + assert await message.reply_markdown(self.test_message.text_markdown) + + await self.check_thread_id_parsing( + message, message.reply_markdown, "send_message", ["test"], monkeypatch + ) + + async def test_reply_markdown_v2(self, monkeypatch, message): + test_md_string = ( + r"__Test__ for <*bold*, _ita\_lic_, `\\\`code`, " + "[links](http://github.com/abc\\\\\\)def), " + "[text\\-mention](tg://user?id=123456789) and ```\\`\\\\pre```\\. " + r"http://google\.com and _bold *nested in ~strk\>trgh~ nested in* italic_\. " + "```python\nPython pre```\\. ||Spoiled||\\. ![👍](tg://emoji?id=1)\\.\n" + ">Multiline\n" + ">block quote\n" + r">with *nested*\." + "\n\n>Multiline\n" + ">expandable\n" + r">block quote\.||" + ) + + async def make_assertion(*_, **kwargs): + cid = kwargs["chat_id"] == message.chat_id + markdown_text = kwargs["text"] == test_md_string + markdown_enabled = kwargs["parse_mode"] == ParseMode.MARKDOWN_V2 + return all([cid, markdown_text, markdown_enabled]) + + assert check_shortcut_signature( + Message.reply_markdown_v2, + Bot.send_message, + ["chat_id", "parse_mode", "reply_to_message_id", "business_connection_id"], + ["quote", "do_quote", "reply_to_message_id"], + annotation_overrides={"message_thread_id": (ODVInput[int], DEFAULT_NONE)}, + ) + assert await check_shortcut_call( + message.reply_text, + message.get_bot(), + "send_message", + skip_params=["reply_to_message_id"], + shortcut_kwargs=["business_connection_id"], + ) + assert await check_defaults_handling( + message.reply_text, message.get_bot(), no_default_kwargs={"message_thread_id"} + ) + + text_markdown = self.test_message_v2.text_markdown_v2 + assert text_markdown == test_md_string + + monkeypatch.setattr(message.get_bot(), "send_message", make_assertion) + assert await message.reply_markdown_v2(self.test_message_v2.text_markdown_v2) + await self.check_quote_parsing( + message, message.reply_markdown_v2, "send_message", [test_md_string], monkeypatch + ) + + await self.check_thread_id_parsing( + message, message.reply_markdown_v2, "send_message", ["test"], monkeypatch + ) + + async def test_reply_html(self, monkeypatch, message): + test_html_string = ( + "Test for <bold, ita_lic, " + r"\`code, " + r'links, ' + 'text-mention and ' + r"
`\pre
. http://google.com " + "and bold nested in strk>trgh nested in italic. " + '
Python pre
. ' + 'Spoiled. ' + '👍.\n' + "
Multiline\nblock quote\nwith nested.
\n\n" + "
Multiline\nexpandable\nblock quote.
" + ) + + async def make_assertion(*_, **kwargs): + cid = kwargs["chat_id"] == message.chat_id + html_text = kwargs["text"] == test_html_string + html_enabled = kwargs["parse_mode"] == ParseMode.HTML + return all([cid, html_text, html_enabled]) + + assert check_shortcut_signature( + Message.reply_html, + Bot.send_message, + ["chat_id", "parse_mode", "reply_to_message_id", "business_connection_id"], + ["quote", "do_quote", "reply_to_message_id"], + annotation_overrides={"message_thread_id": (ODVInput[int], DEFAULT_NONE)}, + ) + assert await check_shortcut_call( + message.reply_text, + message.get_bot(), + "send_message", + skip_params=["reply_to_message_id"], + shortcut_kwargs=["business_connection_id"], + ) + assert await check_defaults_handling( + message.reply_text, message.get_bot(), no_default_kwargs={"message_thread_id"} + ) + + text_html = self.test_message_v2.text_html + assert text_html == test_html_string + + monkeypatch.setattr(message.get_bot(), "send_message", make_assertion) + assert await message.reply_html(self.test_message_v2.text_html) + await self.check_quote_parsing( + message, message.reply_html, "send_message", [test_html_string], monkeypatch + ) + + await self.check_thread_id_parsing( + message, message.reply_html, "send_message", ["test"], monkeypatch + ) + + async def test_reply_media_group(self, monkeypatch, message): + async def make_assertion(*_, **kwargs): + id_ = kwargs["chat_id"] == message.chat_id + media = kwargs["media"] == "reply_media_group" + return id_ and media + + assert check_shortcut_signature( + Message.reply_media_group, + Bot.send_media_group, + ["chat_id", "reply_to_message_id", "business_connection_id"], + ["quote", "do_quote", "reply_to_message_id"], + annotation_overrides={"message_thread_id": (ODVInput[int], DEFAULT_NONE)}, + ) + assert await check_shortcut_call( + message.reply_media_group, + message.get_bot(), + "send_media_group", + skip_params=["reply_to_message_id"], + shortcut_kwargs=["business_connection_id"], + ) + assert await check_defaults_handling( + message.reply_media_group, message.get_bot(), no_default_kwargs={"message_thread_id"} + ) + + monkeypatch.setattr(message.get_bot(), "send_media_group", make_assertion) + assert await message.reply_media_group(media="reply_media_group") + await self.check_quote_parsing( + message, + message.reply_media_group, + "send_media_group", + ["reply_media_group"], + monkeypatch, + ) + + await self.check_thread_id_parsing( + message, + message.reply_media_group, + "send_media_group", + ["reply_media_group"], + monkeypatch, + ) + + async def test_reply_photo(self, monkeypatch, message): + async def make_assertion(*_, **kwargs): + id_ = kwargs["chat_id"] == message.chat_id + photo = kwargs["photo"] == "test_photo" + return id_ and photo + + assert check_shortcut_signature( + Message.reply_photo, + Bot.send_photo, + ["chat_id", "reply_to_message_id", "business_connection_id"], + ["quote", "do_quote", "reply_to_message_id"], + annotation_overrides={"message_thread_id": (ODVInput[int], DEFAULT_NONE)}, + ) + assert await check_shortcut_call( + message.reply_photo, + message.get_bot(), + "send_photo", + skip_params=["reply_to_message_id"], + shortcut_kwargs=["business_connection_id"], + ) + assert await check_defaults_handling( + message.reply_photo, message.get_bot(), no_default_kwargs={"message_thread_id"} + ) + + monkeypatch.setattr(message.get_bot(), "send_photo", make_assertion) + assert await message.reply_photo(photo="test_photo") + await self.check_quote_parsing( + message, message.reply_photo, "send_photo", ["test_photo"], monkeypatch + ) + + await self.check_thread_id_parsing( + message, message.reply_photo, "send_photo", ["test_photo"], monkeypatch + ) + + async def test_reply_audio(self, monkeypatch, message): + async def make_assertion(*_, **kwargs): + id_ = kwargs["chat_id"] == message.chat_id + audio = kwargs["audio"] == "test_audio" + return id_ and audio + + assert check_shortcut_signature( + Message.reply_audio, + Bot.send_audio, + ["chat_id", "reply_to_message_id", "business_connection_id"], + ["quote", "do_quote", "reply_to_message_id"], + annotation_overrides={"message_thread_id": (ODVInput[int], DEFAULT_NONE)}, + ) + assert await check_shortcut_call( + message.reply_audio, + message.get_bot(), + "send_audio", + skip_params=["reply_to_message_id"], + shortcut_kwargs=["business_connection_id"], + ) + assert await check_defaults_handling( + message.reply_audio, message.get_bot(), no_default_kwargs={"message_thread_id"} + ) + + monkeypatch.setattr(message.get_bot(), "send_audio", make_assertion) + assert await message.reply_audio(audio="test_audio") + await self.check_quote_parsing( + message, message.reply_audio, "send_audio", ["test_audio"], monkeypatch + ) + + await self.check_thread_id_parsing( + message, message.reply_audio, "send_audio", ["test_audio"], monkeypatch + ) + + async def test_reply_document(self, monkeypatch, message): + async def make_assertion(*_, **kwargs): + id_ = kwargs["chat_id"] == message.chat_id + document = kwargs["document"] == "test_document" + return id_ and document + + assert check_shortcut_signature( + Message.reply_document, + Bot.send_document, + ["chat_id", "reply_to_message_id", "business_connection_id"], + ["quote", "do_quote", "reply_to_message_id"], + annotation_overrides={"message_thread_id": (ODVInput[int], DEFAULT_NONE)}, + ) + assert await check_shortcut_call( + message.reply_document, + message.get_bot(), + "send_document", + skip_params=["reply_to_message_id"], + shortcut_kwargs=["business_connection_id"], + ) + assert await check_defaults_handling( + message.reply_document, message.get_bot(), no_default_kwargs={"message_thread_id"} + ) + + monkeypatch.setattr(message.get_bot(), "send_document", make_assertion) + assert await message.reply_document(document="test_document") + await self.check_quote_parsing( + message, message.reply_document, "send_document", ["test_document"], monkeypatch + ) + + await self.check_thread_id_parsing( + message, message.reply_document, "send_document", ["test_document"], monkeypatch + ) + + async def test_reply_animation(self, monkeypatch, message): + async def make_assertion(*_, **kwargs): + id_ = kwargs["chat_id"] == message.chat_id + animation = kwargs["animation"] == "test_animation" + return id_ and animation + + assert check_shortcut_signature( + Message.reply_animation, + Bot.send_animation, + ["chat_id", "reply_to_message_id", "business_connection_id"], + ["quote", "do_quote", "reply_to_message_id"], + annotation_overrides={"message_thread_id": (ODVInput[int], DEFAULT_NONE)}, + ) + assert await check_shortcut_call( + message.reply_animation, + message.get_bot(), + "send_animation", + skip_params=["reply_to_message_id"], + shortcut_kwargs=["business_connection_id"], + ) + assert await check_defaults_handling( + message.reply_animation, message.get_bot(), no_default_kwargs={"message_thread_id"} + ) + + monkeypatch.setattr(message.get_bot(), "send_animation", make_assertion) + assert await message.reply_animation(animation="test_animation") + await self.check_quote_parsing( + message, message.reply_animation, "send_animation", ["test_animation"], monkeypatch + ) + + await self.check_thread_id_parsing( + message, message.reply_animation, "send_animation", ["test_animation"], monkeypatch + ) + + async def test_reply_sticker(self, monkeypatch, message): + async def make_assertion(*_, **kwargs): + id_ = kwargs["chat_id"] == message.chat_id + sticker = kwargs["sticker"] == "test_sticker" + return id_ and sticker + + assert check_shortcut_signature( + Message.reply_sticker, + Bot.send_sticker, + ["chat_id", "reply_to_message_id", "business_connection_id"], + ["quote", "do_quote", "reply_to_message_id"], + annotation_overrides={"message_thread_id": (ODVInput[int], DEFAULT_NONE)}, + ) + assert await check_shortcut_call( + message.reply_sticker, + message.get_bot(), + "send_sticker", + skip_params=["reply_to_message_id"], + shortcut_kwargs=["business_connection_id"], + ) + assert await check_defaults_handling( + message.reply_sticker, message.get_bot(), no_default_kwargs={"message_thread_id"} + ) + + monkeypatch.setattr(message.get_bot(), "send_sticker", make_assertion) + assert await message.reply_sticker(sticker="test_sticker") + await self.check_quote_parsing( + message, message.reply_sticker, "send_sticker", ["test_sticker"], monkeypatch + ) + + await self.check_thread_id_parsing( + message, message.reply_sticker, "send_sticker", ["test_sticker"], monkeypatch + ) + + async def test_reply_video(self, monkeypatch, message): + async def make_assertion(*_, **kwargs): + id_ = kwargs["chat_id"] == message.chat_id + video = kwargs["video"] == "test_video" + return id_ and video + + assert check_shortcut_signature( + Message.reply_video, + Bot.send_video, + ["chat_id", "reply_to_message_id", "business_connection_id"], + ["quote", "do_quote", "reply_to_message_id"], + annotation_overrides={"message_thread_id": (ODVInput[int], DEFAULT_NONE)}, + ) + assert await check_shortcut_call( + message.reply_video, + message.get_bot(), + "send_video", + skip_params=["reply_to_message_id"], + shortcut_kwargs=["business_connection_id"], + ) + assert await check_defaults_handling( + message.reply_video, message.get_bot(), no_default_kwargs={"message_thread_id"} + ) + + monkeypatch.setattr(message.get_bot(), "send_video", make_assertion) + assert await message.reply_video(video="test_video") + await self.check_quote_parsing( + message, message.reply_video, "send_video", ["test_video"], monkeypatch + ) + + await self.check_thread_id_parsing( + message, message.reply_video, "send_video", ["test_video"], monkeypatch + ) + + async def test_reply_video_note(self, monkeypatch, message): + async def make_assertion(*_, **kwargs): + id_ = kwargs["chat_id"] == message.chat_id + video_note = kwargs["video_note"] == "test_video_note" + return id_ and video_note + + assert check_shortcut_signature( + Message.reply_video_note, + Bot.send_video_note, + ["chat_id", "reply_to_message_id", "business_connection_id"], + ["quote", "do_quote", "reply_to_message_id"], + annotation_overrides={"message_thread_id": (ODVInput[int], DEFAULT_NONE)}, + ) + assert await check_shortcut_call( + message.reply_video_note, + message.get_bot(), + "send_video_note", + skip_params=["reply_to_message_id"], + shortcut_kwargs=["business_connection_id"], + ) + assert await check_defaults_handling( + message.reply_video_note, message.get_bot(), no_default_kwargs={"message_thread_id"} + ) + + monkeypatch.setattr(message.get_bot(), "send_video_note", make_assertion) + assert await message.reply_video_note(video_note="test_video_note") + await self.check_quote_parsing( + message, message.reply_video_note, "send_video_note", ["test_video_note"], monkeypatch + ) + + await self.check_thread_id_parsing( + message, message.reply_video_note, "send_video_note", ["test_video_note"], monkeypatch + ) + + async def test_reply_voice(self, monkeypatch, message): + async def make_assertion(*_, **kwargs): + id_ = kwargs["chat_id"] == message.chat_id + voice = kwargs["voice"] == "test_voice" + return id_ and voice + + assert check_shortcut_signature( + Message.reply_voice, + Bot.send_voice, + ["chat_id", "reply_to_message_id", "business_connection_id"], + ["quote", "do_quote", "reply_to_message_id"], + annotation_overrides={"message_thread_id": (ODVInput[int], DEFAULT_NONE)}, + ) + assert await check_shortcut_call( + message.reply_voice, + message.get_bot(), + "send_voice", + skip_params=["reply_to_message_id"], + shortcut_kwargs=["business_connection_id"], + ) + assert await check_defaults_handling( + message.reply_voice, message.get_bot(), no_default_kwargs={"message_thread_id"} + ) + + monkeypatch.setattr(message.get_bot(), "send_voice", make_assertion) + assert await message.reply_voice(voice="test_voice") + await self.check_quote_parsing( + message, message.reply_voice, "send_voice", ["test_voice"], monkeypatch + ) + + await self.check_thread_id_parsing( + message, message.reply_voice, "send_voice", ["test_voice"], monkeypatch + ) + + async def test_reply_location(self, monkeypatch, message): + async def make_assertion(*_, **kwargs): + id_ = kwargs["chat_id"] == message.chat_id + location = kwargs["location"] == "test_location" + return id_ and location + + assert check_shortcut_signature( + Message.reply_location, + Bot.send_location, + ["chat_id", "reply_to_message_id", "business_connection_id"], + ["quote", "do_quote", "reply_to_message_id"], + annotation_overrides={"message_thread_id": (ODVInput[int], DEFAULT_NONE)}, + ) + assert await check_shortcut_call( + message.reply_location, + message.get_bot(), + "send_location", + skip_params=["reply_to_message_id"], + shortcut_kwargs=["business_connection_id"], + ) + assert await check_defaults_handling( + message.reply_location, message.get_bot(), no_default_kwargs={"message_thread_id"} + ) + + monkeypatch.setattr(message.get_bot(), "send_location", make_assertion) + assert await message.reply_location(location="test_location") + await self.check_quote_parsing( + message, message.reply_location, "send_location", ["test_location"], monkeypatch + ) + + await self.check_thread_id_parsing( + message, message.reply_location, "send_location", ["test_location"], monkeypatch + ) + + async def test_reply_venue(self, monkeypatch, message): + async def make_assertion(*_, **kwargs): + id_ = kwargs["chat_id"] == message.chat_id + venue = kwargs["venue"] == "test_venue" + return id_ and venue + + assert check_shortcut_signature( + Message.reply_venue, + Bot.send_venue, + ["chat_id", "reply_to_message_id", "business_connection_id"], + ["quote", "do_quote", "reply_to_message_id"], + annotation_overrides={"message_thread_id": (ODVInput[int], DEFAULT_NONE)}, + ) + assert await check_shortcut_call( + message.reply_venue, + message.get_bot(), + "send_venue", + skip_params=["reply_to_message_id"], + shortcut_kwargs=["business_connection_id"], + ) + assert await check_defaults_handling( + message.reply_venue, message.get_bot(), no_default_kwargs={"message_thread_id"} + ) + + monkeypatch.setattr(message.get_bot(), "send_venue", make_assertion) + assert await message.reply_venue(venue="test_venue") + await self.check_quote_parsing( + message, message.reply_venue, "send_venue", ["test_venue"], monkeypatch + ) + + await self.check_thread_id_parsing( + message, message.reply_venue, "send_venue", ["test_venue"], monkeypatch + ) + + async def test_reply_contact(self, monkeypatch, message): + async def make_assertion(*_, **kwargs): + id_ = kwargs["chat_id"] == message.chat_id + contact = kwargs["contact"] == "test_contact" + return id_ and contact + + assert check_shortcut_signature( + Message.reply_contact, + Bot.send_contact, + ["chat_id", "reply_to_message_id", "business_connection_id"], + ["quote", "do_quote", "reply_to_message_id"], + annotation_overrides={"message_thread_id": (ODVInput[int], DEFAULT_NONE)}, + ) + assert await check_shortcut_call( + message.reply_contact, + message.get_bot(), + "send_contact", + skip_params=["reply_to_message_id"], + shortcut_kwargs=["business_connection_id"], + ) + assert await check_defaults_handling( + message.reply_contact, message.get_bot(), no_default_kwargs={"message_thread_id"} + ) + + monkeypatch.setattr(message.get_bot(), "send_contact", make_assertion) + assert await message.reply_contact(contact="test_contact") + await self.check_quote_parsing( + message, message.reply_contact, "send_contact", ["test_contact"], monkeypatch + ) + + await self.check_thread_id_parsing( + message, message.reply_contact, "send_contact", ["test_contact"], monkeypatch + ) + + async def test_reply_poll(self, monkeypatch, message): + async def make_assertion(*_, **kwargs): + id_ = kwargs["chat_id"] == message.chat_id + question = kwargs["question"] == "test_poll" + options = kwargs["options"] == ["1", "2", "3"] + return id_ and question and options + + assert check_shortcut_signature( + Message.reply_poll, + Bot.send_poll, + ["chat_id", "reply_to_message_id", "business_connection_id"], + ["quote", "do_quote", "reply_to_message_id"], + annotation_overrides={"message_thread_id": (ODVInput[int], DEFAULT_NONE)}, + ) + assert await check_shortcut_call( + message.reply_poll, + message.get_bot(), + "send_poll", + skip_params=["reply_to_message_id"], + shortcut_kwargs=["business_connection_id"], + ) + assert await check_defaults_handling( + message.reply_poll, message.get_bot(), no_default_kwargs={"message_thread_id"} + ) + + monkeypatch.setattr(message.get_bot(), "send_poll", make_assertion) + assert await message.reply_poll(question="test_poll", options=["1", "2", "3"]) + await self.check_quote_parsing( + message, message.reply_poll, "send_poll", ["test_poll", ["1", "2", "3"]], monkeypatch + ) + + await self.check_thread_id_parsing( + message, message.reply_poll, "send_poll", ["test_poll", ["1", "2", "3"]], monkeypatch + ) + + async def test_reply_dice(self, monkeypatch, message): + async def make_assertion(*_, **kwargs): + id_ = kwargs["chat_id"] == message.chat_id + contact = kwargs["disable_notification"] is True + return id_ and contact + + assert check_shortcut_signature( + Message.reply_dice, + Bot.send_dice, + ["chat_id", "reply_to_message_id", "business_connection_id"], + ["quote", "do_quote", "reply_to_message_id"], + annotation_overrides={"message_thread_id": (ODVInput[int], DEFAULT_NONE)}, + ) + assert await check_shortcut_call( + message.reply_dice, + message.get_bot(), + "send_dice", + skip_params=["reply_to_message_id"], + shortcut_kwargs=["business_connection_id"], + ) + assert await check_defaults_handling( + message.reply_dice, message.get_bot(), no_default_kwargs={"message_thread_id"} + ) + + monkeypatch.setattr(message.get_bot(), "send_dice", make_assertion) + assert await message.reply_dice(disable_notification=True) + await self.check_quote_parsing( + message, + message.reply_dice, + "send_dice", + [], + monkeypatch, + ) + + await self.check_thread_id_parsing( + message, message.reply_dice, "send_dice", [], monkeypatch + ) + + async def test_reply_action(self, monkeypatch, message: Message): + async def make_assertion(*_, **kwargs): + id_ = kwargs["chat_id"] == message.chat_id + action = kwargs["action"] == ChatAction.TYPING + return id_ and action + + assert check_shortcut_signature( + Message.reply_chat_action, + Bot.send_chat_action, + ["chat_id", "reply_to_message_id", "business_connection_id"], + [], + annotation_overrides={"message_thread_id": (ODVInput[int], DEFAULT_NONE)}, + ) + assert await check_shortcut_call( + message.reply_chat_action, + message.get_bot(), + "send_chat_action", + shortcut_kwargs=["business_connection_id"], + ) + assert await check_defaults_handling( + message.reply_chat_action, message.get_bot(), no_default_kwargs={"message_thread_id"} + ) + + monkeypatch.setattr(message.get_bot(), "send_chat_action", make_assertion) + assert await message.reply_chat_action(action=ChatAction.TYPING) + + await self.check_thread_id_parsing( + message, + message.reply_chat_action, + "send_chat_action", + [ChatAction.TYPING], + monkeypatch, + ) + + async def test_reply_game(self, monkeypatch, message): + async def make_assertion(*_, **kwargs): + return ( + kwargs["chat_id"] == message.chat_id and kwargs["game_short_name"] == "test_game" + ) + + assert check_shortcut_signature( + Message.reply_game, + Bot.send_game, + ["chat_id", "reply_to_message_id", "business_connection_id"], + ["quote", "do_quote", "reply_to_message_id"], + annotation_overrides={"message_thread_id": (ODVInput[int], DEFAULT_NONE)}, + ) + assert await check_shortcut_call( + message.reply_game, + message.get_bot(), + "send_game", + skip_params=["reply_to_message_id"], + shortcut_kwargs=["business_connection_id"], + ) + assert await check_defaults_handling( + message.reply_game, message.get_bot(), no_default_kwargs={"message_thread_id"} + ) + + monkeypatch.setattr(message.get_bot(), "send_game", make_assertion) + assert await message.reply_game(game_short_name="test_game") + await self.check_quote_parsing( + message, message.reply_game, "send_game", ["test_game"], monkeypatch + ) + + await self.check_thread_id_parsing( + message, + message.reply_game, + "send_game", + ["test_game"], + monkeypatch, + ) + + async def test_reply_invoice(self, monkeypatch, message): + async def make_assertion(*_, **kwargs): + title = kwargs["title"] == "title" + description = kwargs["description"] == "description" + payload = kwargs["payload"] == "payload" + provider_token = kwargs["provider_token"] == "provider_token" + currency = kwargs["currency"] == "currency" + prices = kwargs["prices"] == "prices" + args = title and description and payload and provider_token and currency and prices + return kwargs["chat_id"] == message.chat_id and args + + assert check_shortcut_signature( + Message.reply_invoice, + Bot.send_invoice, + ["chat_id", "reply_to_message_id", "business_connection_id"], + ["quote", "do_quote", "reply_to_message_id"], + annotation_overrides={"message_thread_id": (ODVInput[int], DEFAULT_NONE)}, + ) + assert await check_shortcut_call( + message.reply_invoice, + message.get_bot(), + "send_invoice", + skip_params=["reply_to_message_id"], + shortcut_kwargs=["business_connection_id"], + ) + assert await check_defaults_handling( + message.reply_invoice, message.get_bot(), no_default_kwargs={"message_thread_id"} + ) + + monkeypatch.setattr(message.get_bot(), "send_invoice", make_assertion) + assert await message.reply_invoice( + "title", + "description", + "payload", + "provider_token", + "currency", + "prices", + ) + await self.check_quote_parsing( + message, + message.reply_invoice, + "send_invoice", + ["title", "description", "payload", "provider_token", "currency", "prices"], + monkeypatch, + ) + + await self.check_thread_id_parsing( + message, + message.reply_invoice, + "send_invoice", + ["title", "description", "payload", "provider_token", "currency", "prices"], + monkeypatch, + ) + + @pytest.mark.parametrize(("disable_notification", "protected"), [(False, True), (True, False)]) + async def test_forward(self, monkeypatch, message, disable_notification, protected): + async def make_assertion(*_, **kwargs): + chat_id = kwargs["chat_id"] == 123456 + from_chat = kwargs["from_chat_id"] == message.chat_id + message_id = kwargs["message_id"] == message.message_id + notification = kwargs["disable_notification"] == disable_notification + protected_cont = kwargs["protect_content"] == protected + return chat_id and from_chat and message_id and notification and protected_cont + + assert check_shortcut_signature( + Message.forward, Bot.forward_message, ["from_chat_id", "message_id"], [] + ) + assert await check_shortcut_call(message.forward, message.get_bot(), "forward_message") + assert await check_defaults_handling(message.forward, message.get_bot()) + + monkeypatch.setattr(message.get_bot(), "forward_message", make_assertion) + assert await message.forward( + 123456, disable_notification=disable_notification, protect_content=protected + ) + assert not await message.forward(635241) + + @pytest.mark.parametrize(("disable_notification", "protected"), [(True, False), (False, True)]) + async def test_copy(self, monkeypatch, message, disable_notification, protected): + keyboard = [[1, 2]] + + async def make_assertion(*_, **kwargs): + chat_id = kwargs["chat_id"] == 123456 + from_chat = kwargs["from_chat_id"] == message.chat_id + message_id = kwargs["message_id"] == message.message_id + notification = kwargs["disable_notification"] == disable_notification + protected_cont = kwargs["protect_content"] == protected + if kwargs.get("reply_markup") is not None: + reply_markup = kwargs["reply_markup"] is keyboard + else: + reply_markup = True + return ( + chat_id + and from_chat + and message_id + and notification + and reply_markup + and protected_cont + ) + + assert check_shortcut_signature( + Message.copy, Bot.copy_message, ["from_chat_id", "message_id"], [] + ) + assert await check_shortcut_call(message.copy, message.get_bot(), "copy_message") + assert await check_defaults_handling(message.copy, message.get_bot()) + + monkeypatch.setattr(message.get_bot(), "copy_message", make_assertion) + assert await message.copy( + 123456, disable_notification=disable_notification, protect_content=protected + ) + assert await message.copy( + 123456, + reply_markup=keyboard, + disable_notification=disable_notification, + protect_content=protected, + ) + assert not await message.copy(635241) + + @pytest.mark.parametrize(("disable_notification", "protected"), [(True, False), (False, True)]) + async def test_reply_copy(self, monkeypatch, message, disable_notification, protected): + keyboard = [[1, 2]] + + async def make_assertion(*_, **kwargs): + chat_id = kwargs["from_chat_id"] == 123456 + from_chat = kwargs["chat_id"] == message.chat_id + message_id = kwargs["message_id"] == 456789 + notification = kwargs["disable_notification"] == disable_notification + is_protected = kwargs["protect_content"] == protected + if kwargs.get("reply_markup") is not None: + reply_markup = kwargs["reply_markup"] is keyboard + else: + reply_markup = True + return ( + chat_id + and from_chat + and message_id + and notification + and reply_markup + and is_protected + ) + + assert check_shortcut_signature( + Message.reply_copy, + Bot.copy_message, + ["chat_id", "reply_to_message_id", "business_connection_id"], + ["quote", "do_quote", "reply_to_message_id"], + annotation_overrides={"message_thread_id": (ODVInput[int], DEFAULT_NONE)}, + ) + assert await check_shortcut_call(message.copy, message.get_bot(), "copy_message") + assert await check_defaults_handling(message.copy, message.get_bot()) + + monkeypatch.setattr(message.get_bot(), "copy_message", make_assertion) + assert await message.reply_copy( + 123456, 456789, disable_notification=disable_notification, protect_content=protected + ) + assert await message.reply_copy( + 123456, + 456789, + reply_markup=keyboard, + disable_notification=disable_notification, + protect_content=protected, + ) + await self.check_quote_parsing( + message, + message.reply_copy, + "copy_message", + [123456, 456789], + monkeypatch, + ) + + await self.check_thread_id_parsing( + message, + message.reply_copy, + "copy_message", + [123456, 456789], + monkeypatch, + ) + + async def test_edit_text(self, monkeypatch, message): + async def make_assertion(*_, **kwargs): + chat_id = kwargs["chat_id"] == message.chat_id + message_id = kwargs["message_id"] == message.message_id + text = kwargs["text"] == "test" + return chat_id and message_id and text + + assert check_shortcut_signature( + Message.edit_text, + Bot.edit_message_text, + ["chat_id", "message_id", "inline_message_id", "business_connection_id"], + [], + ) + assert await check_shortcut_call( + message.edit_text, + message.get_bot(), + "edit_message_text", + skip_params=["inline_message_id"], + shortcut_kwargs=["message_id", "chat_id", "business_connection_id"], + ) + assert await check_defaults_handling(message.edit_text, message.get_bot()) + + monkeypatch.setattr(message.get_bot(), "edit_message_text", make_assertion) + assert await message.edit_text(text="test") + + async def test_edit_caption(self, monkeypatch, message): + async def make_assertion(*_, **kwargs): + chat_id = kwargs["chat_id"] == message.chat_id + message_id = kwargs["message_id"] == message.message_id + caption = kwargs["caption"] == "new caption" + return chat_id and message_id and caption + + assert check_shortcut_signature( + Message.edit_caption, + Bot.edit_message_caption, + ["chat_id", "message_id", "inline_message_id", "business_connection_id"], + [], + ) + assert await check_shortcut_call( + message.edit_caption, + message.get_bot(), + "edit_message_caption", + skip_params=["inline_message_id"], + shortcut_kwargs=["message_id", "chat_id", "business_connection_id"], + ) + assert await check_defaults_handling(message.edit_caption, message.get_bot()) + + monkeypatch.setattr(message.get_bot(), "edit_message_caption", make_assertion) + assert await message.edit_caption(caption="new caption") + + async def test_edit_media(self, monkeypatch, message): + async def make_assertion(*_, **kwargs): + chat_id = kwargs["chat_id"] == message.chat_id + message_id = kwargs["message_id"] == message.message_id + media = kwargs["media"] == "my_media" + return chat_id and message_id and media + + assert check_shortcut_signature( + Message.edit_media, + Bot.edit_message_media, + ["chat_id", "message_id", "inline_message_id", "business_connection_id"], + [], + ) + assert await check_shortcut_call( + message.edit_media, + message.get_bot(), + "edit_message_media", + skip_params=["inline_message_id"], + shortcut_kwargs=["message_id", "chat_id", "business_connection_id"], + ) + assert await check_defaults_handling(message.edit_media, message.get_bot()) + + monkeypatch.setattr(message.get_bot(), "edit_message_media", make_assertion) + assert await message.edit_media("my_media") + + async def test_edit_reply_markup(self, monkeypatch, message): + async def make_assertion(*_, **kwargs): + chat_id = kwargs["chat_id"] == message.chat_id + message_id = kwargs["message_id"] == message.message_id + reply_markup = kwargs["reply_markup"] == [["1", "2"]] + return chat_id and message_id and reply_markup + + assert check_shortcut_signature( + Message.edit_reply_markup, + Bot.edit_message_reply_markup, + ["chat_id", "message_id", "inline_message_id", "business_connection_id"], + [], + ) + assert await check_shortcut_call( + message.edit_reply_markup, + message.get_bot(), + "edit_message_reply_markup", + skip_params=["inline_message_id"], + shortcut_kwargs=["message_id", "chat_id", "business_connection_id"], + ) + assert await check_defaults_handling(message.edit_reply_markup, message.get_bot()) + + monkeypatch.setattr(message.get_bot(), "edit_message_reply_markup", make_assertion) + assert await message.edit_reply_markup(reply_markup=[["1", "2"]]) + + async def test_edit_live_location(self, monkeypatch, message): + async def make_assertion(*_, **kwargs): + chat_id = kwargs["chat_id"] == message.chat_id + message_id = kwargs["message_id"] == message.message_id + latitude = kwargs["latitude"] == 1 + longitude = kwargs["longitude"] == 2 + live = kwargs["live_period"] == 900 + return chat_id and message_id and longitude and latitude and live + + assert check_shortcut_signature( + Message.edit_live_location, + Bot.edit_message_live_location, + ["chat_id", "message_id", "inline_message_id", "business_connection_id"], + [], + ) + assert await check_shortcut_call( + message.edit_live_location, + message.get_bot(), + "edit_message_live_location", + skip_params=["inline_message_id"], + shortcut_kwargs=["message_id", "chat_id", "business_connection_id"], + ) + assert await check_defaults_handling(message.edit_live_location, message.get_bot()) + + monkeypatch.setattr(message.get_bot(), "edit_message_live_location", make_assertion) + assert await message.edit_live_location(latitude=1, longitude=2, live_period=900) + + async def test_stop_live_location(self, monkeypatch, message): + async def make_assertion(*_, **kwargs): + chat_id = kwargs["chat_id"] == message.chat_id + message_id = kwargs["message_id"] == message.message_id + return chat_id and message_id + + assert check_shortcut_signature( + Message.stop_live_location, + Bot.stop_message_live_location, + ["chat_id", "message_id", "inline_message_id", "business_connection_id"], + [], + ) + assert await check_shortcut_call( + message.stop_live_location, + message.get_bot(), + "stop_message_live_location", + skip_params=["inline_message_id"], + shortcut_kwargs=["message_id", "chat_id", "business_connection_id"], + ) + assert await check_defaults_handling(message.stop_live_location, message.get_bot()) + + monkeypatch.setattr(message.get_bot(), "stop_message_live_location", make_assertion) + assert await message.stop_live_location() + + async def test_set_game_score(self, monkeypatch, message): + async def make_assertion(*_, **kwargs): + chat_id = kwargs["chat_id"] == message.chat_id + message_id = kwargs["message_id"] == message.message_id + user_id = kwargs["user_id"] == 1 + score = kwargs["score"] == 2 + return chat_id and message_id and user_id and score + + assert check_shortcut_signature( + Message.set_game_score, + Bot.set_game_score, + ["chat_id", "message_id", "inline_message_id"], + [], + ) + assert await check_shortcut_call( + message.set_game_score, + message.get_bot(), + "set_game_score", + skip_params=["inline_message_id"], + shortcut_kwargs=["message_id", "chat_id"], + ) + assert await check_defaults_handling(message.set_game_score, message.get_bot()) + + monkeypatch.setattr(message.get_bot(), "set_game_score", make_assertion) + assert await message.set_game_score(user_id=1, score=2) + + async def test_get_game_high_scores(self, monkeypatch, message): + async def make_assertion(*_, **kwargs): + chat_id = kwargs["chat_id"] == message.chat_id + message_id = kwargs["message_id"] == message.message_id + user_id = kwargs["user_id"] == 1 + return chat_id and message_id and user_id + + assert check_shortcut_signature( + Message.get_game_high_scores, + Bot.get_game_high_scores, + ["chat_id", "message_id", "inline_message_id"], + [], + ) + assert await check_shortcut_call( + message.get_game_high_scores, + message.get_bot(), + "get_game_high_scores", + skip_params=["inline_message_id"], + shortcut_kwargs=["message_id", "chat_id"], + ) + assert await check_defaults_handling(message.get_game_high_scores, message.get_bot()) + + monkeypatch.setattr(message.get_bot(), "get_game_high_scores", make_assertion) + assert await message.get_game_high_scores(user_id=1) + + async def test_delete(self, monkeypatch, message): + async def make_assertion(*_, **kwargs): + chat_id = kwargs["chat_id"] == message.chat_id + message_id = kwargs["message_id"] == message.message_id + return chat_id and message_id + + assert check_shortcut_signature( + Message.delete, Bot.delete_message, ["chat_id", "message_id"], [] + ) + assert await check_shortcut_call(message.delete, message.get_bot(), "delete_message") + assert await check_defaults_handling(message.delete, message.get_bot()) + + monkeypatch.setattr(message.get_bot(), "delete_message", make_assertion) + assert await message.delete() + + async def test_stop_poll(self, monkeypatch, message): + async def make_assertion(*_, **kwargs): + chat_id = kwargs["chat_id"] == message.chat_id + message_id = kwargs["message_id"] == message.message_id + return chat_id and message_id + + assert check_shortcut_signature( + Message.stop_poll, + Bot.stop_poll, + ["chat_id", "message_id", "business_connection_id"], + [], + ) + assert await check_shortcut_call( + message.stop_poll, + message.get_bot(), + "stop_poll", + shortcut_kwargs=["business_connection_id"], + ) + assert await check_defaults_handling(message.stop_poll, message.get_bot()) + + monkeypatch.setattr(message.get_bot(), "stop_poll", make_assertion) + assert await message.stop_poll() + + async def test_pin(self, monkeypatch, message): + async def make_assertion(*args, **kwargs): + chat_id = kwargs["chat_id"] == message.chat_id + message_id = kwargs["message_id"] == message.message_id + return chat_id and message_id + + assert check_shortcut_signature( + Message.pin, + Bot.pin_chat_message, + ["chat_id", "message_id", "business_connection_id"], + [], + ) + assert await check_shortcut_call( + message.pin, + message.get_bot(), + "pin_chat_message", + shortcut_kwargs=["chat_id", "message_id", "business_connection_id"], + ) + assert await check_defaults_handling(message.pin, message.get_bot()) + + monkeypatch.setattr(message.get_bot(), "pin_chat_message", make_assertion) + assert await message.pin() + + async def test_unpin(self, monkeypatch, message): + async def make_assertion(*args, **kwargs): + chat_id = kwargs["chat_id"] == message.chat_id + message_id = kwargs["message_id"] == message.message_id + return chat_id and message_id + + assert check_shortcut_signature( + Message.unpin, + Bot.unpin_chat_message, + ["chat_id", "message_id", "business_connection_id"], + [], + ) + assert await check_shortcut_call( + message.unpin, + message.get_bot(), + "unpin_chat_message", + shortcut_kwargs=["chat_id", "message_id", "business_connection_id"], + ) + assert await check_defaults_handling(message.unpin, message.get_bot()) + + monkeypatch.setattr(message.get_bot(), "unpin_chat_message", make_assertion) + assert await message.unpin() + + @pytest.mark.parametrize( + ("default_quote", "chat_type", "expected"), + [ + (False, Chat.PRIVATE, False), + (None, Chat.PRIVATE, False), + (True, Chat.PRIVATE, True), + (False, Chat.GROUP, False), + (None, Chat.GROUP, True), + (True, Chat.GROUP, True), + (False, Chat.SUPERGROUP, False), + (None, Chat.SUPERGROUP, True), + (True, Chat.SUPERGROUP, True), + (False, Chat.CHANNEL, False), + (None, Chat.CHANNEL, True), + (True, Chat.CHANNEL, True), + ], + ) + async def test_default_do_quote( + self, bot, message, default_quote, chat_type, expected, monkeypatch + ): + original_bot = message.get_bot() + temp_bot = PytestExtBot(token=bot.token, defaults=Defaults(do_quote=default_quote)) + message.set_bot(temp_bot) + + async def make_assertion(*_, **kwargs): + reply_parameters = kwargs.get("reply_parameters") or ReplyParameters(message_id=False) + condition = reply_parameters.message_id == message.message_id + return condition == expected + + monkeypatch.setattr(message.get_bot(), "send_message", make_assertion) + + try: + message.chat.type = chat_type + assert await message.reply_text("test") + finally: + message.set_bot(original_bot) + + async def test_edit_forum_topic(self, monkeypatch, message): + async def make_assertion(*_, **kwargs): + return ( + kwargs["chat_id"] == message.chat_id + and kwargs["message_thread_id"] == message.message_thread_id + and kwargs["name"] == "New Name" + and kwargs["icon_custom_emoji_id"] == "12345" + ) + + assert check_shortcut_signature( + Message.edit_forum_topic, Bot.edit_forum_topic, ["chat_id", "message_thread_id"], [] + ) + assert await check_shortcut_call( + message.edit_forum_topic, + message.get_bot(), + "edit_forum_topic", + shortcut_kwargs=["chat_id", "message_thread_id"], + ) + assert await check_defaults_handling(message.edit_forum_topic, message.get_bot()) + + monkeypatch.setattr(message.get_bot(), "edit_forum_topic", make_assertion) + assert await message.edit_forum_topic(name="New Name", icon_custom_emoji_id="12345") + + async def test_close_forum_topic(self, monkeypatch, message): + async def make_assertion(*_, **kwargs): + return ( + kwargs["chat_id"] == message.chat_id + and kwargs["message_thread_id"] == message.message_thread_id + ) + + assert check_shortcut_signature( + Message.close_forum_topic, Bot.close_forum_topic, ["chat_id", "message_thread_id"], [] + ) + assert await check_shortcut_call( + message.close_forum_topic, + message.get_bot(), + "close_forum_topic", + shortcut_kwargs=["chat_id", "message_thread_id"], + ) + assert await check_defaults_handling(message.close_forum_topic, message.get_bot()) + + monkeypatch.setattr(message.get_bot(), "close_forum_topic", make_assertion) + assert await message.close_forum_topic() + + async def test_reopen_forum_topic(self, monkeypatch, message): + async def make_assertion(*_, **kwargs): + return ( + kwargs["chat_id"] == message.chat_id + and kwargs["message_thread_id"] == message.message_thread_id + ) + + assert check_shortcut_signature( + Message.reopen_forum_topic, + Bot.reopen_forum_topic, + ["chat_id", "message_thread_id"], + [], + ) + assert await check_shortcut_call( + message.reopen_forum_topic, + message.get_bot(), + "reopen_forum_topic", + shortcut_kwargs=["chat_id", "message_thread_id"], + ) + assert await check_defaults_handling(message.reopen_forum_topic, message.get_bot()) + + monkeypatch.setattr(message.get_bot(), "reopen_forum_topic", make_assertion) + assert await message.reopen_forum_topic() + + async def test_delete_forum_topic(self, monkeypatch, message): + async def make_assertion(*_, **kwargs): + return ( + kwargs["chat_id"] == message.chat_id + and kwargs["message_thread_id"] == message.message_thread_id + ) + + assert check_shortcut_signature( + Message.delete_forum_topic, + Bot.delete_forum_topic, + ["chat_id", "message_thread_id"], + [], + ) + assert await check_shortcut_call( + message.delete_forum_topic, + message.get_bot(), + "delete_forum_topic", + shortcut_kwargs=["chat_id", "message_thread_id"], + ) + assert await check_defaults_handling(message.delete_forum_topic, message.get_bot()) + + monkeypatch.setattr(message.get_bot(), "delete_forum_topic", make_assertion) + assert await message.delete_forum_topic() + + async def test_unpin_all_forum_topic_messages(self, monkeypatch, message): + async def make_assertion(*_, **kwargs): + return ( + kwargs["chat_id"] == message.chat_id + and kwargs["message_thread_id"] == message.message_thread_id + ) + + assert check_shortcut_signature( + Message.unpin_all_forum_topic_messages, + Bot.unpin_all_forum_topic_messages, + ["chat_id", "message_thread_id"], + [], + ) + assert await check_shortcut_call( + message.unpin_all_forum_topic_messages, + message.get_bot(), + "unpin_all_forum_topic_messages", + shortcut_kwargs=["chat_id", "message_thread_id"], + ) + assert await check_defaults_handling( + message.unpin_all_forum_topic_messages, message.get_bot() + ) + + monkeypatch.setattr(message.get_bot(), "unpin_all_forum_topic_messages", make_assertion) + assert await message.unpin_all_forum_topic_messages() + + def test_attachement_successful_payment_deprecated(self, message, recwarn): + message.successful_payment = "something" + # kinda unnecessary to assert but one needs to call the function ofc so. Here we are. + assert message.effective_attachment == "something" + assert len(recwarn) == 1 + assert ( + "successful_payment will no longer be considered an attachment in future major " + "versions" in str(recwarn[0].message) + ) + assert recwarn[0].category is PTBDeprecationWarning + assert recwarn[0].filename == __file__ diff --git a/test_messageautodeletetimerchanged.py b/test_messageautodeletetimerchanged.py new file mode 100644 index 0000000000000000000000000000000000000000..4628dae5b1bb52712bd5a7ef945bbcc0a6bfff98 --- /dev/null +++ b/test_messageautodeletetimerchanged.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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/]. + +from telegram import MessageAutoDeleteTimerChanged, VideoChatEnded +from tests.auxil.slots import mro_slots + + +class TestMessageAutoDeleteTimerChangedWithoutRequest: + message_auto_delete_time = 100 + + def test_slot_behaviour(self): + action = MessageAutoDeleteTimerChanged(self.message_auto_delete_time) + for attr in action.__slots__: + assert getattr(action, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(action)) == len(set(mro_slots(action))), "duplicate slot" + + def test_de_json(self): + json_dict = {"message_auto_delete_time": self.message_auto_delete_time} + madtc = MessageAutoDeleteTimerChanged.de_json(json_dict, None) + assert madtc.api_kwargs == {} + + assert madtc.message_auto_delete_time == self.message_auto_delete_time + + def test_to_dict(self): + madtc = MessageAutoDeleteTimerChanged(self.message_auto_delete_time) + madtc_dict = madtc.to_dict() + + assert isinstance(madtc_dict, dict) + assert madtc_dict["message_auto_delete_time"] == self.message_auto_delete_time + + def test_equality(self): + a = MessageAutoDeleteTimerChanged(100) + b = MessageAutoDeleteTimerChanged(100) + c = MessageAutoDeleteTimerChanged(50) + d = VideoChatEnded(25) + + assert a == b + assert hash(a) == hash(b) + + assert a != c + assert hash(a) != hash(c) + + assert a != d + assert hash(a) != hash(d) diff --git a/test_messageentity.py b/test_messageentity.py new file mode 100644 index 0000000000000000000000000000000000000000..3598454d8eb67772e5194db8cbf046306ad655e2 --- /dev/null +++ b/test_messageentity.py @@ -0,0 +1,168 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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/]. +import random +from typing import List, Tuple + +import pytest + +from telegram import MessageEntity, User +from telegram.constants import MessageEntityType +from tests.auxil.slots import mro_slots + + +@pytest.fixture(scope="module", params=MessageEntity.ALL_TYPES) +def message_entity(request): + type_ = request.param + url = None + if type_ == MessageEntity.TEXT_LINK: + url = "t.me" + user = None + if type_ == MessageEntity.TEXT_MENTION: + user = User(1, "test_user", False) + language = None + if type_ == MessageEntity.PRE: + language = "python" + return MessageEntity(type_, 1, 3, url=url, user=user, language=language) + + +class MessageEntityTestBase: + type_ = "url" + offset = 1 + length = 2 + url = "url" + + +class TestMessageEntityWithoutRequest(MessageEntityTestBase): + def test_slot_behaviour(self, message_entity): + inst = message_entity + for attr in inst.__slots__: + assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" + + def test_de_json(self, bot): + json_dict = {"type": self.type_, "offset": self.offset, "length": self.length} + entity = MessageEntity.de_json(json_dict, bot) + assert entity.api_kwargs == {} + + assert entity.type == self.type_ + assert entity.offset == self.offset + assert entity.length == self.length + + def test_to_dict(self, message_entity): + entity_dict = message_entity.to_dict() + + assert isinstance(entity_dict, dict) + assert entity_dict["type"] == message_entity.type + assert entity_dict["offset"] == message_entity.offset + assert entity_dict["length"] == message_entity.length + if message_entity.url: + assert entity_dict["url"] == message_entity.url + if message_entity.user: + assert entity_dict["user"] == message_entity.user.to_dict() + if message_entity.language: + assert entity_dict["language"] == message_entity.language + + def test_enum_init(self): + entity = MessageEntity(type="foo", offset=0, length=1) + assert entity.type == "foo" + entity = MessageEntity(type="url", offset=0, length=1) + assert entity.type is MessageEntityType.URL + + def test_fix_utf16(self): + text = "𠌕 bold 𝄢 italic underlined: 𝛙𝌢𑁍" + inputs_outputs: List[Tuple[Tuple[int, int, str], Tuple[int, int]]] = [ + ((2, 4, MessageEntity.BOLD), (3, 4)), + ((9, 6, MessageEntity.ITALIC), (11, 6)), + ((28, 3, MessageEntity.UNDERLINE), (30, 6)), + ] + random.shuffle(inputs_outputs) + unicode_entities = [ + MessageEntity(offset=_input[0], length=_input[1], type=_input[2]) + for _input, _ in inputs_outputs + ] + utf_16_entities = MessageEntity.adjust_message_entities_to_utf_16(text, unicode_entities) + for out_entity, input_output in zip(utf_16_entities, inputs_outputs): + _, output = input_output + offset, length = output + assert out_entity.offset == offset + assert out_entity.length == length + + @pytest.mark.parametrize("by", [6, "prefix", "𝛙𝌢𑁍"]) + def test_shift_entities(self, by): + kwargs = { + "url": "url", + "user": 42, + "language": "python", + "custom_emoji_id": "custom_emoji_id", + } + entities = [ + MessageEntity(MessageEntity.BOLD, 2, 3, **kwargs), + MessageEntity(MessageEntity.BOLD, 5, 6, **kwargs), + ] + shifted = MessageEntity.shift_entities(by, entities) + assert shifted[0].offset == 8 + assert shifted[1].offset == 11 + + assert shifted[0] is not entities[0] + assert shifted[1] is not entities[1] + + for entity in shifted: + for key, value in kwargs.items(): + assert getattr(entity, key) == value + + def test_concatenate(self): + kwargs = { + "url": "url", + "user": 42, + "language": "python", + "custom_emoji_id": "custom_emoji_id", + } + first_entity = MessageEntity(MessageEntity.BOLD, 0, 6, **kwargs) + second_entity = MessageEntity(MessageEntity.ITALIC, 0, 4, **kwargs) + third_entity = MessageEntity(MessageEntity.UNDERLINE, 3, 6, **kwargs) + + first = ("prefix 𝛙𝌢𑁍 | ", [first_entity], True) + second = ("text 𝛙𝌢𑁍", [second_entity], False) + third = (" | suffix 𝛙𝌢𑁍", [third_entity]) + + new_text, new_entities = MessageEntity.concatenate(first, second, third) + + assert new_text == "prefix 𝛙𝌢𑁍 | text 𝛙𝌢𑁍 | suffix 𝛙𝌢𑁍" + assert [entity.offset for entity in new_entities] == [0, 16, 30] + for old, new in zip([first_entity, second_entity, third_entity], new_entities): + assert new is not old + assert new.type == old.type + for key, value in kwargs.items(): + assert getattr(new, key) == value + + def test_equality(self): + a = MessageEntity(MessageEntity.BOLD, 2, 3) + b = MessageEntity(MessageEntity.BOLD, 2, 3) + c = MessageEntity(MessageEntity.CODE, 2, 3) + d = MessageEntity(MessageEntity.CODE, 5, 6) + + assert a == b + assert hash(a) == hash(b) + assert a is not b + + assert a != c + assert hash(a) != hash(c) + + assert a != d + assert hash(a) != hash(d) diff --git a/test_messageid.py b/test_messageid.py new file mode 100644 index 0000000000000000000000000000000000000000..fb7c23fe6e14455cd3a8ba5ff7dc7eae32f84613 --- /dev/null +++ b/test_messageid.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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/]. +import pytest + +from telegram import MessageId, User +from tests.auxil.slots import mro_slots + + +@pytest.fixture(scope="module") +def message_id(): + return MessageId(message_id=TestMessageIdWithoutRequest.m_id) + + +class TestMessageIdWithoutRequest: + m_id = 1234 + + def test_slot_behaviour(self, message_id): + for attr in message_id.__slots__: + assert getattr(message_id, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(message_id)) == len(set(mro_slots(message_id))), "duplicate slot" + + def test_de_json(self): + json_dict = {"message_id": self.m_id} + message_id = MessageId.de_json(json_dict, None) + assert message_id.api_kwargs == {} + + assert message_id.message_id == self.m_id + + def test_to_dict(self, message_id): + message_id_dict = message_id.to_dict() + + assert isinstance(message_id_dict, dict) + assert message_id_dict["message_id"] == message_id.message_id + + def test_equality(self): + a = MessageId(message_id=1) + b = MessageId(message_id=1) + c = MessageId(message_id=2) + d = User(id=1, first_name="name", is_bot=False) + + assert a == b + assert hash(a) == hash(b) + + assert a != c + assert hash(a) != hash(c) + + assert a != d + assert hash(a) != hash(d) diff --git a/test_messageorigin.py b/test_messageorigin.py new file mode 100644 index 0000000000000000000000000000000000000000..10d9fa77894c03ceabfd1f04ad6585a5d8f95c3e --- /dev/null +++ b/test_messageorigin.py @@ -0,0 +1,231 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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/]. +import datetime +import inspect +from copy import deepcopy + +import pytest + +from telegram import ( + Chat, + Dice, + MessageOrigin, + MessageOriginChannel, + MessageOriginChat, + MessageOriginHiddenUser, + MessageOriginUser, + User, +) +from telegram._utils.datetime import UTC, to_timestamp +from tests.auxil.slots import mro_slots + +ignored = ["self", "api_kwargs"] + + +class MODefaults: + date: datetime.datetime = to_timestamp(datetime.datetime.utcnow()) + chat = Chat(1, Chat.CHANNEL) + message_id = 123 + author_signautre = "PTB" + sender_chat = Chat(1, Chat.CHANNEL) + sender_user_name = "PTB" + sender_user = User(1, "user", False) + + +def message_origin_channel(): + return MessageOriginChannel( + MODefaults.date, MODefaults.chat, MODefaults.message_id, MODefaults.author_signautre + ) + + +def message_origin_chat(): + return MessageOriginChat( + MODefaults.date, + MODefaults.sender_chat, + MODefaults.author_signautre, + ) + + +def message_origin_hidden_user(): + return MessageOriginHiddenUser(MODefaults.date, MODefaults.sender_user_name) + + +def message_origin_user(): + return MessageOriginUser(MODefaults.date, MODefaults.sender_user) + + +def make_json_dict(instance: MessageOrigin, include_optional_args: bool = False) -> dict: + """Used to make the json dict which we use for testing de_json. Similar to iter_args()""" + json_dict = {"type": instance.type} + sig = inspect.signature(instance.__class__.__init__) + + for param in sig.parameters.values(): + if param.name in ignored: # ignore irrelevant params + continue + + val = getattr(instance, param.name) + # Compulsory args- + if param.default is inspect.Parameter.empty: + if hasattr(val, "to_dict"): # convert the user object or any future ones to dict. + val = val.to_dict() + json_dict[param.name] = val + + # If we want to test all args (for de_json)- + elif param.default is not inspect.Parameter.empty and include_optional_args: + json_dict[param.name] = val + return json_dict + + +def iter_args( + instance: MessageOrigin, de_json_inst: MessageOrigin, include_optional: bool = False +): + """ + We accept both the regular instance and de_json created instance and iterate over them for + easy one line testing later one. + """ + yield instance.type, de_json_inst.type # yield this here cause it's not available in sig. + + sig = inspect.signature(instance.__class__.__init__) + for param in sig.parameters.values(): + if param.name in ignored: + continue + inst_at, json_at = getattr(instance, param.name), getattr(de_json_inst, param.name) + if isinstance(json_at, datetime.datetime): # Convert datetime to int + json_at = to_timestamp(json_at) + if ( + param.default is not inspect.Parameter.empty and include_optional + ) or param.default is inspect.Parameter.empty: + yield inst_at, json_at + + +@pytest.fixture +def message_origin_type(request): + return request.param() + + +@pytest.mark.parametrize( + "message_origin_type", + [ + message_origin_channel, + message_origin_chat, + message_origin_hidden_user, + message_origin_user, + ], + indirect=True, +) +class TestMessageOriginTypesWithoutRequest: + def test_slot_behaviour(self, message_origin_type): + inst = message_origin_type + for attr in inst.__slots__: + assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" + + def test_de_json_required_args(self, bot, message_origin_type): + cls = message_origin_type.__class__ + assert cls.de_json({}, bot) is None + + json_dict = make_json_dict(message_origin_type) + const_message_origin = MessageOrigin.de_json(json_dict, bot) + assert const_message_origin.api_kwargs == {} + + assert isinstance(const_message_origin, MessageOrigin) + assert isinstance(const_message_origin, cls) + for msg_origin_type_at, const_msg_origin_at in iter_args( + message_origin_type, const_message_origin + ): + assert msg_origin_type_at == const_msg_origin_at + + def test_de_json_all_args(self, bot, message_origin_type): + json_dict = make_json_dict(message_origin_type, include_optional_args=True) + const_message_origin = MessageOrigin.de_json(json_dict, bot) + + assert const_message_origin.api_kwargs == {} + + assert isinstance(const_message_origin, MessageOrigin) + assert isinstance(const_message_origin, message_origin_type.__class__) + for msg_origin_type_at, const_msg_origin_at in iter_args( + message_origin_type, const_message_origin, True + ): + assert msg_origin_type_at == const_msg_origin_at + + def test_de_json_messageorigin_localization(self, message_origin_type, tz_bot, bot, raw_bot): + json_dict = make_json_dict(message_origin_type, include_optional_args=True) + msgorigin_raw = MessageOrigin.de_json(json_dict, raw_bot) + msgorigin_bot = MessageOrigin.de_json(json_dict, bot) + msgorigin_tz = MessageOrigin.de_json(json_dict, tz_bot) + + # comparing utcoffsets because comparing timezones is unpredicatable + msgorigin_offset = msgorigin_tz.date.utcoffset() + tz_bot_offset = tz_bot.defaults.tzinfo.utcoffset(msgorigin_tz.date.replace(tzinfo=None)) + + assert msgorigin_raw.date.tzinfo == UTC + assert msgorigin_bot.date.tzinfo == UTC + assert msgorigin_offset == tz_bot_offset + + def test_de_json_invalid_type(self, message_origin_type, bot): + json_dict = {"type": "invalid", "date": MODefaults.date} + message_origin_type = MessageOrigin.de_json(json_dict, bot) + + assert type(message_origin_type) is MessageOrigin + assert message_origin_type.type == "invalid" + + def test_de_json_subclass(self, message_origin_type, bot, chat_id): + """This makes sure that e.g. MessageOriginChat(data, bot) never returns a + MessageOriginUser instance.""" + cls = message_origin_type.__class__ + json_dict = make_json_dict(message_origin_type, True) + assert type(cls.de_json(json_dict, bot)) is cls + + def test_to_dict(self, message_origin_type): + message_origin_dict = message_origin_type.to_dict() + + assert isinstance(message_origin_dict, dict) + assert message_origin_dict["type"] == message_origin_type.type + assert message_origin_dict["date"] == message_origin_type.date + + for slot in message_origin_type.__slots__: # additional verification for the optional args + if slot in ("chat", "sender_chat", "sender_user"): + assert (getattr(message_origin_type, slot)).to_dict() == message_origin_dict[slot] + continue + assert getattr(message_origin_type, slot) == message_origin_dict[slot] + + def test_equality(self, message_origin_type): + a = MessageOrigin(type="type", date=MODefaults.date) + b = MessageOrigin(type="type", date=MODefaults.date) + c = message_origin_type + d = deepcopy(message_origin_type) + e = Dice(4, "emoji") + + assert a == b + assert hash(a) == hash(b) + + assert a != c + assert hash(a) != hash(c) + + assert a != d + assert hash(a) != hash(d) + + assert a != e + assert hash(a) != hash(e) + + assert c == d + assert hash(c) == hash(d) + + assert c != e + assert hash(c) != hash(e) diff --git a/test_meta.py b/test_meta.py new file mode 100644 index 0000000000000000000000000000000000000000..7b83e7bb93ab575c0b1898bb31a448e439ee8082 --- /dev/null +++ b/test_meta.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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/]. +import os + +import pytest + +from tests.auxil.envvars import env_var_2_bool + +skip_disabled = pytest.mark.skipif( + not env_var_2_bool(os.getenv("TEST_BUILD", "")), reason="TEST_BUILD not enabled" +) + + +# To make the tests agnostic of the cwd +@pytest.fixture(autouse=True) +def _change_test_dir(request, monkeypatch): + monkeypatch.chdir(request.config.rootdir) + + +@skip_disabled +def test_build(): + assert os.system("python -m build") == 0 # pragma: no cover diff --git a/test_modules.py b/test_modules.py new file mode 100644 index 0000000000000000000000000000000000000000..4d69b6a09af8bc4f5b6c6878fa5e93bc36822a76 --- /dev/null +++ b/test_modules.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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 tests whether our submodules have __all__ or not. +Additionally also tests if all public submodules are included in __all__ for __init__'s. +""" +import importlib +import os +from pathlib import Path + + +def test_public_submodules_dunder_all(): + modules_to_search = list(Path("telegram").rglob("*.py")) + + if not modules_to_search: + raise AssertionError("No modules found to search through, please modify this test.") + + for mod_path in modules_to_search: + path = str(mod_path) + folder = mod_path.parent + + if mod_path.name == "__init__.py" and "_" not in path[:-11]: # init of public submodules + mod = load_module(mod_path) + assert hasattr(mod, "__all__"), f"{folder}'s __init__ does not have an __all__!" + + pub_mods = get_public_submodules_in_folder(folder) + cond = all(pub_mod in mod.__all__ for pub_mod in pub_mods) + + assert cond, f"{path}'s __all__ should contain all public submodules ({pub_mods})!" + continue + + if "_" in path: # skip private modules + continue + + mod = load_module(mod_path) + assert hasattr(mod, "__all__"), f"{mod_path.name} does not have an __all__!" + + +def load_module(path: Path): + if path.name == "__init__.py": + mod_name = str(path.parent).replace(os.sep, ".") # telegram(.ext) format + else: + mod_name = f"{path.parent}.{path.stem}".replace(os.sep, ".") # telegram(.ext).(...) format + return importlib.import_module(mod_name) + + +def get_public_submodules_in_folder(path: Path): + return [i.stem for i in path.glob("[!_]*.py")] diff --git a/test_paidmedia.py b/test_paidmedia.py new file mode 100644 index 0000000000000000000000000000000000000000..be9ac14905bf0aedec4830cc49a6117a6a36f4da --- /dev/null +++ b/test_paidmedia.py @@ -0,0 +1,325 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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/]. + +from copy import deepcopy + +import pytest + +from telegram import ( + Dice, + PaidMedia, + PaidMediaInfo, + PaidMediaPhoto, + PaidMediaPreview, + PaidMediaVideo, + PhotoSize, + Video, +) +from telegram.constants import PaidMediaType +from tests.auxil.slots import mro_slots + + +@pytest.fixture( + scope="module", + params=[ + PaidMedia.PREVIEW, + PaidMedia.PHOTO, + PaidMedia.VIDEO, + ], +) +def pm_scope_type(request): + return request.param + + +@pytest.fixture( + scope="module", + params=[ + PaidMediaPreview, + PaidMediaPhoto, + PaidMediaVideo, + ], + ids=[ + PaidMedia.PREVIEW, + PaidMedia.PHOTO, + PaidMedia.VIDEO, + ], +) +def pm_scope_class(request): + return request.param + + +@pytest.fixture( + scope="module", + params=[ + ( + PaidMediaPreview, + PaidMedia.PREVIEW, + ), + ( + PaidMediaPhoto, + PaidMedia.PHOTO, + ), + ( + PaidMediaVideo, + PaidMedia.VIDEO, + ), + ], + ids=[ + PaidMedia.PREVIEW, + PaidMedia.PHOTO, + PaidMedia.VIDEO, + ], +) +def pm_scope_class_and_type(request): + return request.param + + +@pytest.fixture(scope="module") +def paid_media(pm_scope_class_and_type): + # We use de_json here so that we don't have to worry about which class gets which arguments + return pm_scope_class_and_type[0].de_json( + { + "type": pm_scope_class_and_type[1], + "width": PaidMediaTestBase.width, + "height": PaidMediaTestBase.height, + "duration": PaidMediaTestBase.duration, + "video": PaidMediaTestBase.video.to_dict(), + "photo": [p.to_dict() for p in PaidMediaTestBase.photo], + }, + bot=None, + ) + + +def paid_media_video(): + return PaidMediaVideo(video=PaidMediaTestBase.video) + + +def paid_media_photo(): + return PaidMediaPhoto(photo=PaidMediaTestBase.photo) + + +@pytest.fixture(scope="module") +def paid_media_info(): + return PaidMediaInfo( + star_count=PaidMediaInfoTestBase.star_count, + paid_media=[paid_media_video(), paid_media_photo()], + ) + + +class PaidMediaTestBase: + width = 640 + height = 480 + duration = 60 + video = Video( + file_id="video_file_id", + width=640, + height=480, + file_unique_id="file_unique_id", + duration=60, + ) + photo = ( + PhotoSize( + file_id="photo_file_id", + width=640, + height=480, + file_unique_id="file_unique_id", + ), + ) + + +class TestPaidMediaWithoutRequest(PaidMediaTestBase): + def test_slot_behaviour(self, paid_media): + inst = paid_media + for attr in inst.__slots__: + assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" + + def test_de_json(self, bot, pm_scope_class_and_type): + cls = pm_scope_class_and_type[0] + type_ = pm_scope_class_and_type[1] + + json_dict = { + "type": type_, + "width": self.width, + "height": self.height, + "duration": self.duration, + "video": self.video.to_dict(), + "photo": [p.to_dict() for p in self.photo], + } + pm = PaidMedia.de_json(json_dict, bot) + assert set(pm.api_kwargs.keys()) == { + "width", + "height", + "duration", + "video", + "photo", + } - set(cls.__slots__) + + assert isinstance(pm, PaidMedia) + assert type(pm) is cls + assert pm.type == type_ + if "width" in cls.__slots__: + assert pm.width == self.width + assert pm.height == self.height + assert pm.duration == self.duration + if "video" in cls.__slots__: + assert pm.video == self.video + if "photo" in cls.__slots__: + assert pm.photo == self.photo + + assert cls.de_json(None, bot) is None + assert PaidMedia.de_json({}, bot) is None + + def test_de_json_invalid_type(self, bot): + json_dict = { + "type": "invalid", + "width": self.width, + "height": self.height, + "duration": self.duration, + "video": self.video.to_dict(), + "photo": [p.to_dict() for p in self.photo], + } + pm = PaidMedia.de_json(json_dict, bot) + assert pm.api_kwargs == { + "width": self.width, + "height": self.height, + "duration": self.duration, + "video": self.video.to_dict(), + "photo": [p.to_dict() for p in self.photo], + } + + assert type(pm) is PaidMedia + assert pm.type == "invalid" + + def test_de_json_subclass(self, pm_scope_class, bot): + """This makes sure that e.g. PaidMediaPreivew(data) never returns a + TransactionPartnerPhoto instance.""" + json_dict = { + "type": "invalid", + "width": self.width, + "height": self.height, + "duration": self.duration, + "video": self.video.to_dict(), + "photo": [p.to_dict() for p in self.photo], + } + assert type(pm_scope_class.de_json(json_dict, bot)) is pm_scope_class + + def test_to_dict(self, paid_media): + pm_dict = paid_media.to_dict() + + assert isinstance(pm_dict, dict) + assert pm_dict["type"] == paid_media.type + if hasattr(paid_media_info, "width"): + assert pm_dict["width"] == paid_media.width + assert pm_dict["height"] == paid_media.height + assert pm_dict["duration"] == paid_media.duration + if hasattr(paid_media_info, "video"): + assert pm_dict["video"] == paid_media.video.to_dict() + if hasattr(paid_media_info, "photo"): + assert pm_dict["photo"] == [p.to_dict() for p in paid_media.photo] + + def test_type_enum_conversion(self): + assert type(PaidMedia("video").type) is PaidMediaType + assert PaidMedia("unknown").type == "unknown" + + def test_equality(self, paid_media, bot): + a = PaidMedia("base_type") + b = PaidMedia("base_type") + c = paid_media + d = deepcopy(paid_media) + e = Dice(4, "emoji") + + assert a == b + assert hash(a) == hash(b) + + assert a != c + assert hash(a) != hash(c) + + assert a != d + assert hash(a) != hash(d) + + assert a != e + assert hash(a) != hash(e) + + assert c == d + assert hash(c) == hash(d) + + assert c != e + assert hash(c) != hash(e) + + if hasattr(c, "video"): + json_dict = c.to_dict() + json_dict["video"] = Video("different", "d2", 1, 1, 1).to_dict() + f = c.__class__.de_json(json_dict, bot) + + assert c != f + assert hash(c) != hash(f) + + if hasattr(c, "photo"): + json_dict = c.to_dict() + json_dict["photo"] = [PhotoSize("different", "d2", 1, 1, 1).to_dict()] + f = c.__class__.de_json(json_dict, bot) + + assert c != f + assert hash(c) != hash(f) + + +class PaidMediaInfoTestBase: + star_count = 200 + paid_media = [paid_media_video(), paid_media_photo()] + + +class TestPaidMediaInfoWithoutRequest(PaidMediaInfoTestBase): + def test_slot_behaviour(self, paid_media_info): + inst = paid_media_info + for attr in inst.__slots__: + assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" + + def test_de_json(self, bot): + json_dict = { + "star_count": self.star_count, + "paid_media": [t.to_dict() for t in self.paid_media], + } + pmi = PaidMediaInfo.de_json(json_dict, bot) + pmi_none = PaidMediaInfo.de_json(None, bot) + assert pmi.paid_media == tuple(self.paid_media) + assert pmi.star_count == self.star_count + assert pmi_none is None + + def test_to_dict(self, paid_media_info): + assert paid_media_info.to_dict() == { + "star_count": self.star_count, + "paid_media": [t.to_dict() for t in self.paid_media], + } + + def test_equality(self): + pmi1 = PaidMediaInfo( + star_count=self.star_count, paid_media=[paid_media_video(), paid_media_photo()] + ) + pmi2 = PaidMediaInfo( + star_count=self.star_count, paid_media=[paid_media_video(), paid_media_photo()] + ) + pmi3 = PaidMediaInfo(star_count=100, paid_media=[paid_media_photo()]) + + assert pmi1 == pmi2 + assert hash(pmi1) == hash(pmi2) + + assert pmi1 != pmi3 + assert hash(pmi1) != hash(pmi3) diff --git a/test_poll.py b/test_poll.py new file mode 100644 index 0000000000000000000000000000000000000000..81619a1129923738a9a5eac0c85b5bf3ac66405d --- /dev/null +++ b/test_poll.py @@ -0,0 +1,484 @@ +#!/usr/bin/env python +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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/]. +from datetime import datetime, timedelta, timezone + +import pytest + +from telegram import Chat, InputPollOption, MessageEntity, Poll, PollAnswer, PollOption, User +from telegram._utils.datetime import UTC, to_timestamp +from telegram.constants import PollType +from tests.auxil.slots import mro_slots + + +@pytest.fixture(scope="module") +def input_poll_option(): + out = InputPollOption( + text=InputPollOptionTestBase.text, + text_parse_mode=InputPollOptionTestBase.text_parse_mode, + text_entities=InputPollOptionTestBase.text_entities, + ) + out._unfreeze() + return out + + +class InputPollOptionTestBase: + text = "test option" + text_parse_mode = "MarkdownV2" + text_entities = [ + MessageEntity(0, 4, MessageEntity.BOLD), + MessageEntity(5, 7, MessageEntity.ITALIC), + ] + + +class TestInputPollOptionWithoutRequest(InputPollOptionTestBase): + def test_slot_behaviour(self, input_poll_option): + for attr in input_poll_option.__slots__: + assert getattr(input_poll_option, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(input_poll_option)) == len( + set(mro_slots(input_poll_option)) + ), "duplicate slot" + + def test_de_json(self): + assert InputPollOption.de_json({}, None) is None + + json_dict = { + "text": self.text, + "text_parse_mode": self.text_parse_mode, + "text_entities": [e.to_dict() for e in self.text_entities], + } + input_poll_option = InputPollOption.de_json(json_dict, None) + assert input_poll_option.api_kwargs == {} + + assert input_poll_option.text == self.text + assert input_poll_option.text_parse_mode == self.text_parse_mode + assert input_poll_option.text_entities == tuple(self.text_entities) + + def test_to_dict(self, input_poll_option): + input_poll_option_dict = input_poll_option.to_dict() + + assert isinstance(input_poll_option_dict, dict) + assert input_poll_option_dict["text"] == input_poll_option.text + assert input_poll_option_dict["text_parse_mode"] == input_poll_option.text_parse_mode + assert input_poll_option_dict["text_entities"] == [ + e.to_dict() for e in input_poll_option.text_entities + ] + + # Test that the default-value parameter is handled correctly + input_poll_option = InputPollOption("text") + input_poll_option_dict = input_poll_option.to_dict() + assert "text_parse_mode" not in input_poll_option_dict + + def test_equality(self): + a = InputPollOption("text") + b = InputPollOption("text", self.text_parse_mode) + c = InputPollOption("text", text_entities=self.text_entities) + d = InputPollOption("different_text") + e = Poll(123, "question", ["O1", "O2"], 1, False, True, Poll.REGULAR, True) + + assert a == b + assert hash(a) == hash(b) + + assert a == c + assert hash(a) == hash(c) + + assert a != d + assert hash(a) != hash(d) + + assert a != e + assert hash(a) != hash(e) + + +@pytest.fixture(scope="module") +def poll_option(): + out = PollOption( + text=PollOptionTestBase.text, + voter_count=PollOptionTestBase.voter_count, + text_entities=PollOptionTestBase.text_entities, + ) + out._unfreeze() + return out + + +class PollOptionTestBase: + text = "test option" + voter_count = 3 + text_entities = [ + MessageEntity(MessageEntity.BOLD, 0, 4), + MessageEntity(MessageEntity.ITALIC, 5, 6), + ] + + +class TestPollOptionWithoutRequest(PollOptionTestBase): + def test_slot_behaviour(self, poll_option): + for attr in poll_option.__slots__: + assert getattr(poll_option, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(poll_option)) == len(set(mro_slots(poll_option))), "duplicate slot" + + def test_de_json(self): + json_dict = {"text": self.text, "voter_count": self.voter_count} + poll_option = PollOption.de_json(json_dict, None) + assert poll_option.api_kwargs == {} + + assert poll_option.text == self.text + assert poll_option.voter_count == self.voter_count + + def test_de_json_all(self): + json_dict = { + "text": self.text, + "voter_count": self.voter_count, + "text_entities": [e.to_dict() for e in self.text_entities], + } + poll_option = PollOption.de_json(json_dict, None) + assert PollOption.de_json(None, None) is None + assert poll_option.api_kwargs == {} + + assert poll_option.text == self.text + assert poll_option.voter_count == self.voter_count + assert poll_option.text_entities == tuple(self.text_entities) + + def test_to_dict(self, poll_option): + poll_option_dict = poll_option.to_dict() + + assert isinstance(poll_option_dict, dict) + assert poll_option_dict["text"] == poll_option.text + assert poll_option_dict["voter_count"] == poll_option.voter_count + assert poll_option_dict["text_entities"] == [ + e.to_dict() for e in poll_option.text_entities + ] + + def test_parse_entity(self, poll_option): + entity = MessageEntity(MessageEntity.BOLD, 0, 4) + poll_option.text_entities = [entity] + + assert poll_option.parse_entity(entity) == "test" + + def test_parse_entities(self, poll_option): + entity = MessageEntity(MessageEntity.BOLD, 0, 4) + entity_2 = MessageEntity(MessageEntity.ITALIC, 5, 6) + poll_option.text_entities = [entity, entity_2] + + assert poll_option.parse_entities(MessageEntity.BOLD) == {entity: "test"} + assert poll_option.parse_entities() == {entity: "test", entity_2: "option"} + + def test_equality(self): + a = PollOption("text", 1) + b = PollOption("text", 1) + c = PollOption("text_1", 1) + d = PollOption("text", 2) + e = Poll(123, "question", ["O1", "O2"], 1, False, True, Poll.REGULAR, True) + + assert a == b + assert hash(a) == hash(b) + + assert a != c + assert hash(a) != hash(c) + + assert a != d + assert hash(a) != hash(d) + + assert a != e + assert hash(a) != hash(e) + + +@pytest.fixture(scope="module") +def poll_answer(): + return PollAnswer( + PollAnswerTestBase.poll_id, + PollAnswerTestBase.option_ids, + PollAnswerTestBase.user, + PollAnswerTestBase.voter_chat, + ) + + +class PollAnswerTestBase: + poll_id = "id" + option_ids = [2] + user = User(1, "", False) + voter_chat = Chat(1, "") + + +class TestPollAnswerWithoutRequest(PollAnswerTestBase): + def test_de_json(self): + json_dict = { + "poll_id": self.poll_id, + "option_ids": self.option_ids, + "user": self.user.to_dict(), + "voter_chat": self.voter_chat.to_dict(), + } + poll_answer = PollAnswer.de_json(json_dict, None) + assert poll_answer.api_kwargs == {} + + assert poll_answer.poll_id == self.poll_id + assert poll_answer.option_ids == tuple(self.option_ids) + assert poll_answer.user == self.user + assert poll_answer.voter_chat == self.voter_chat + + def test_to_dict(self, poll_answer): + poll_answer_dict = poll_answer.to_dict() + + assert isinstance(poll_answer_dict, dict) + assert poll_answer_dict["poll_id"] == poll_answer.poll_id + assert poll_answer_dict["option_ids"] == list(poll_answer.option_ids) + assert poll_answer_dict["user"] == poll_answer.user.to_dict() + assert poll_answer_dict["voter_chat"] == poll_answer.voter_chat.to_dict() + + def test_equality(self): + a = PollAnswer(123, [2], self.user, self.voter_chat) + b = PollAnswer(123, [2], self.user, Chat(1, "")) + c = PollAnswer(123, [2], User(1, "first", False), self.voter_chat) + d = PollAnswer(123, [1, 2], self.user, self.voter_chat) + e = PollAnswer(456, [2], self.user, self.voter_chat) + f = PollOption("Text", 1) + + assert a == b + assert hash(a) == hash(b) + + assert a == c + assert hash(a) == hash(c) + + assert a != d + assert hash(a) != hash(d) + + assert a != e + assert hash(a) != hash(e) + + assert a != f + assert hash(a) != hash(f) + + +@pytest.fixture(scope="module") +def poll(): + poll = Poll( + PollTestBase.id_, + PollTestBase.question, + PollTestBase.options, + PollTestBase.total_voter_count, + PollTestBase.is_closed, + PollTestBase.is_anonymous, + PollTestBase.type, + PollTestBase.allows_multiple_answers, + explanation=PollTestBase.explanation, + explanation_entities=PollTestBase.explanation_entities, + open_period=PollTestBase.open_period, + close_date=PollTestBase.close_date, + question_entities=PollTestBase.question_entities, + ) + poll._unfreeze() + return poll + + +class PollTestBase: + id_ = "id" + question = "Test Question?" + options = [PollOption("test", 10), PollOption("test2", 11)] + total_voter_count = 0 + is_closed = True + is_anonymous = False + type = Poll.REGULAR + allows_multiple_answers = True + explanation = ( + b"\\U0001f469\\u200d\\U0001f469\\u200d\\U0001f467" + b"\\u200d\\U0001f467\\U0001f431http://google.com" + ).decode("unicode-escape") + explanation_entities = [MessageEntity(13, 17, MessageEntity.URL)] + open_period = 42 + close_date = datetime.now(timezone.utc) + question_entities = [ + MessageEntity(MessageEntity.BOLD, 0, 4), + MessageEntity(MessageEntity.ITALIC, 5, 8), + ] + + +class TestPollWithoutRequest(PollTestBase): + def test_de_json(self, bot): + json_dict = { + "id": self.id_, + "question": self.question, + "options": [o.to_dict() for o in self.options], + "total_voter_count": self.total_voter_count, + "is_closed": self.is_closed, + "is_anonymous": self.is_anonymous, + "type": self.type, + "allows_multiple_answers": self.allows_multiple_answers, + "explanation": self.explanation, + "explanation_entities": [self.explanation_entities[0].to_dict()], + "open_period": self.open_period, + "close_date": to_timestamp(self.close_date), + "question_entities": [e.to_dict() for e in self.question_entities], + } + poll = Poll.de_json(json_dict, bot) + assert poll.api_kwargs == {} + + assert poll.id == self.id_ + assert poll.question == self.question + assert poll.options == tuple(self.options) + assert poll.options[0].text == self.options[0].text + assert poll.options[0].voter_count == self.options[0].voter_count + assert poll.options[1].text == self.options[1].text + assert poll.options[1].voter_count == self.options[1].voter_count + assert poll.total_voter_count == self.total_voter_count + assert poll.is_closed == self.is_closed + assert poll.is_anonymous == self.is_anonymous + assert poll.type == self.type + assert poll.allows_multiple_answers == self.allows_multiple_answers + assert poll.explanation == self.explanation + assert poll.explanation_entities == tuple(self.explanation_entities) + assert poll.open_period == self.open_period + assert abs(poll.close_date - self.close_date) < timedelta(seconds=1) + assert to_timestamp(poll.close_date) == to_timestamp(self.close_date) + assert poll.question_entities == tuple(self.question_entities) + + def test_de_json_localization(self, tz_bot, bot, raw_bot): + json_dict = { + "id": self.id_, + "question": self.question, + "options": [o.to_dict() for o in self.options], + "total_voter_count": self.total_voter_count, + "is_closed": self.is_closed, + "is_anonymous": self.is_anonymous, + "type": self.type, + "allows_multiple_answers": self.allows_multiple_answers, + "explanation": self.explanation, + "explanation_entities": [self.explanation_entities[0].to_dict()], + "open_period": self.open_period, + "close_date": to_timestamp(self.close_date), + "question_entities": [e.to_dict() for e in self.question_entities], + } + + poll_raw = Poll.de_json(json_dict, raw_bot) + poll_bot = Poll.de_json(json_dict, bot) + poll_bot_tz = Poll.de_json(json_dict, tz_bot) + + # comparing utcoffsets because comparing timezones is unpredicatable + poll_bot_tz_offset = poll_bot_tz.close_date.utcoffset() + tz_bot_offset = tz_bot.defaults.tzinfo.utcoffset( + poll_bot_tz.close_date.replace(tzinfo=None) + ) + + assert poll_raw.close_date.tzinfo == UTC + assert poll_bot.close_date.tzinfo == UTC + assert poll_bot_tz_offset == tz_bot_offset + + def test_to_dict(self, poll): + poll_dict = poll.to_dict() + + assert isinstance(poll_dict, dict) + assert poll_dict["id"] == poll.id + assert poll_dict["question"] == poll.question + assert poll_dict["options"] == [o.to_dict() for o in poll.options] + assert poll_dict["total_voter_count"] == poll.total_voter_count + assert poll_dict["is_closed"] == poll.is_closed + assert poll_dict["is_anonymous"] == poll.is_anonymous + assert poll_dict["type"] == poll.type + assert poll_dict["allows_multiple_answers"] == poll.allows_multiple_answers + assert poll_dict["explanation"] == poll.explanation + assert poll_dict["explanation_entities"] == [poll.explanation_entities[0].to_dict()] + assert poll_dict["open_period"] == poll.open_period + assert poll_dict["close_date"] == to_timestamp(poll.close_date) + assert poll_dict["question_entities"] == [e.to_dict() for e in poll.question_entities] + + def test_equality(self): + a = Poll(123, "question", ["O1", "O2"], 1, False, True, Poll.REGULAR, True) + b = Poll(123, "question", ["o1", "o2"], 1, True, False, Poll.REGULAR, True) + c = Poll(456, "question", ["o1", "o2"], 1, True, False, Poll.REGULAR, True) + d = PollOption("Text", 1) + + assert a == b + assert hash(a) == hash(b) + + assert a != c + assert hash(a) != hash(c) + + assert a != d + assert hash(a) != hash(d) + + def test_enum_init(self): + poll = Poll( + type="foo", + id="id", + question="question", + options=[], + total_voter_count=0, + is_closed=False, + is_anonymous=False, + allows_multiple_answers=False, + ) + assert poll.type == "foo" + poll = Poll( + type=PollType.QUIZ, + id="id", + question="question", + options=[], + total_voter_count=0, + is_closed=False, + is_anonymous=False, + allows_multiple_answers=False, + ) + assert poll.type is PollType.QUIZ + + def test_parse_explanation_entity(self, poll): + entity = MessageEntity(type=MessageEntity.URL, offset=13, length=17) + poll.explanation_entities = [entity] + + assert poll.parse_explanation_entity(entity) == "http://google.com" + + with pytest.raises(RuntimeError, match="Poll has no"): + Poll( + "id", + "question", + [PollOption("text", voter_count=0)], + total_voter_count=0, + is_closed=False, + is_anonymous=False, + type=Poll.QUIZ, + allows_multiple_answers=False, + ).parse_explanation_entity(entity) + + def test_parse_explanation_entities(self, poll): + entity = MessageEntity(type=MessageEntity.URL, offset=13, length=17) + entity_2 = MessageEntity(type=MessageEntity.BOLD, offset=13, length=1) + poll.explanation_entities = [entity_2, entity] + + assert poll.parse_explanation_entities(MessageEntity.URL) == {entity: "http://google.com"} + assert poll.parse_explanation_entities() == {entity: "http://google.com", entity_2: "h"} + + with pytest.raises(RuntimeError, match="Poll has no"): + Poll( + "id", + "question", + [PollOption("text", voter_count=0)], + total_voter_count=0, + is_closed=False, + is_anonymous=False, + type=Poll.QUIZ, + allows_multiple_answers=False, + ).parse_explanation_entities() + + def test_parse_question_entity(self, poll): + entity = MessageEntity(MessageEntity.ITALIC, 5, 8) + poll.question_entities = [entity] + + assert poll.parse_question_entity(entity) == "Question" + + def test_parse_question_entities(self, poll): + entity = MessageEntity(MessageEntity.ITALIC, 5, 8) + entity_2 = MessageEntity(MessageEntity.BOLD, 0, 4) + poll.question_entities = [entity_2, entity] + + assert poll.parse_question_entities(MessageEntity.ITALIC) == {entity: "Question"} + assert poll.parse_question_entities() == {entity: "Question", entity_2: "Test"} diff --git a/test_proximityalerttriggered.py b/test_proximityalerttriggered.py new file mode 100644 index 0000000000000000000000000000000000000000..de49b699fea7b9a535ff6bac09222bc6a8dbf94f --- /dev/null +++ b/test_proximityalerttriggered.py @@ -0,0 +1,97 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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/]. +import pytest + +from telegram import BotCommand, ProximityAlertTriggered, User +from tests.auxil.slots import mro_slots + + +@pytest.fixture(scope="module") +def proximity_alert_triggered(): + return ProximityAlertTriggered( + ProximityAlertTriggeredTestBase.traveler, + ProximityAlertTriggeredTestBase.watcher, + ProximityAlertTriggeredTestBase.distance, + ) + + +class ProximityAlertTriggeredTestBase: + traveler = User(1, "foo", False) + watcher = User(2, "bar", False) + distance = 42 + + +class TestProximityAlertTriggeredWithoutRequest(ProximityAlertTriggeredTestBase): + def test_slot_behaviour(self, proximity_alert_triggered): + inst = proximity_alert_triggered + for attr in inst.__slots__: + assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" + + def test_de_json(self, bot): + json_dict = { + "traveler": self.traveler.to_dict(), + "watcher": self.watcher.to_dict(), + "distance": self.distance, + } + proximity_alert_triggered = ProximityAlertTriggered.de_json(json_dict, bot) + assert proximity_alert_triggered.api_kwargs == {} + + assert proximity_alert_triggered.traveler == self.traveler + assert proximity_alert_triggered.traveler.first_name == self.traveler.first_name + assert proximity_alert_triggered.watcher == self.watcher + assert proximity_alert_triggered.watcher.first_name == self.watcher.first_name + assert proximity_alert_triggered.distance == self.distance + + def test_to_dict(self, proximity_alert_triggered): + proximity_alert_triggered_dict = proximity_alert_triggered.to_dict() + + assert isinstance(proximity_alert_triggered_dict, dict) + assert ( + proximity_alert_triggered_dict["traveler"] + == proximity_alert_triggered.traveler.to_dict() + ) + assert ( + proximity_alert_triggered_dict["watcher"] + == proximity_alert_triggered.watcher.to_dict() + ) + assert proximity_alert_triggered_dict["distance"] == proximity_alert_triggered.distance + + def test_equality(self, proximity_alert_triggered): + a = proximity_alert_triggered + b = ProximityAlertTriggered(User(1, "John", False), User(2, "Doe", False), 42) + c = ProximityAlertTriggered(User(3, "John", False), User(2, "Doe", False), 42) + d = ProximityAlertTriggered(User(1, "John", False), User(3, "Doe", False), 42) + e = ProximityAlertTriggered(User(1, "John", False), User(2, "Doe", False), 43) + f = BotCommand("start", "description") + + assert a == b + assert hash(a) == hash(b) + + assert a != c + assert hash(a) != hash(c) + + assert a != d + assert hash(a) != hash(d) + + assert a != e + assert hash(a) != hash(e) + + assert a != f + assert hash(a) != hash(f) diff --git a/test_reaction.py b/test_reaction.py new file mode 100644 index 0000000000000000000000000000000000000000..67ece80e3b07e0d16724c79f51b32bde0340b871 --- /dev/null +++ b/test_reaction.py @@ -0,0 +1,283 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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/]. +import inspect +from copy import deepcopy + +import pytest + +from telegram import ( + BotCommand, + Dice, + ReactionCount, + ReactionType, + ReactionTypeCustomEmoji, + ReactionTypeEmoji, + ReactionTypePaid, +) +from telegram.constants import ReactionEmoji +from tests.auxil.slots import mro_slots + +ignored = ["self", "api_kwargs"] + + +class RTDefaults: + custom_emoji = "123custom" + normal_emoji = ReactionEmoji.THUMBS_UP + + +def reaction_type_custom_emoji(): + return ReactionTypeCustomEmoji(RTDefaults.custom_emoji) + + +def reaction_type_emoji(): + return ReactionTypeEmoji(RTDefaults.normal_emoji) + + +def reaction_type_paid(): + return ReactionTypePaid() + + +def make_json_dict(instance: ReactionType, include_optional_args: bool = False) -> dict: + """Used to make the json dict which we use for testing de_json. Similar to iter_args()""" + json_dict = {"type": instance.type} + sig = inspect.signature(instance.__class__.__init__) + + for param in sig.parameters.values(): + if param.name in ignored: # ignore irrelevant params + continue + + val = getattr(instance, param.name) + # Compulsory args- + if param.default is inspect.Parameter.empty: + if hasattr(val, "to_dict"): # convert the user object or any future ones to dict. + val = val.to_dict() + json_dict[param.name] = val + + # If we want to test all args (for de_json)- + # currently not needed, keeping for completeness + elif param.default is not inspect.Parameter.empty and include_optional_args: + json_dict[param.name] = val + return json_dict + + +def iter_args(instance: ReactionType, de_json_inst: ReactionType, include_optional: bool = False): + """ + We accept both the regular instance and de_json created instance and iterate over them for + easy one line testing later one. + """ + yield instance.type, de_json_inst.type # yield this here cause it's not available in sig. + + sig = inspect.signature(instance.__class__.__init__) + for param in sig.parameters.values(): + if param.name in ignored: + continue + inst_at, json_at = getattr(instance, param.name), getattr(de_json_inst, param.name) + if ( + param.default is not inspect.Parameter.empty and include_optional + ) or param.default is inspect.Parameter.empty: + yield inst_at, json_at + + +@pytest.fixture +def reaction_type(request): + return request.param() + + +@pytest.mark.parametrize( + "reaction_type", + [ + reaction_type_custom_emoji, + reaction_type_emoji, + reaction_type_paid, + ], + indirect=True, +) +class TestReactionTypesWithoutRequest: + def test_slot_behaviour(self, reaction_type): + inst = reaction_type + for attr in inst.__slots__: + assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" + + def test_de_json_required_args(self, bot, reaction_type): + cls = reaction_type.__class__ + assert cls.de_json(None, bot) is None + assert ReactionType.de_json({}, bot) is None + + json_dict = make_json_dict(reaction_type) + const_reaction_type = ReactionType.de_json(json_dict, bot) + assert const_reaction_type.api_kwargs == {} + + assert isinstance(const_reaction_type, ReactionType) + assert isinstance(const_reaction_type, cls) + for reaction_type_at, const_reaction_type_at in iter_args( + reaction_type, const_reaction_type + ): + assert reaction_type_at == const_reaction_type_at + + def test_de_json_all_args(self, bot, reaction_type): + json_dict = make_json_dict(reaction_type, include_optional_args=True) + const_reaction_type = ReactionType.de_json(json_dict, bot) + assert const_reaction_type.api_kwargs == {} + + assert isinstance(const_reaction_type, ReactionType) + assert isinstance(const_reaction_type, reaction_type.__class__) + for c_mem_type_at, const_c_mem_at in iter_args(reaction_type, const_reaction_type, True): + assert c_mem_type_at == const_c_mem_at + + def test_de_json_invalid_type(self, bot, reaction_type): + json_dict = {"type": "invalid"} + reaction_type = ReactionType.de_json(json_dict, bot) + + assert type(reaction_type) is ReactionType + assert reaction_type.type == "invalid" + + def test_de_json_subclass(self, reaction_type, bot, chat_id): + """This makes sure that e.g. ReactionTypeEmoji(data, bot) never returns a + ReactionTypeCustomEmoji instance.""" + cls = reaction_type.__class__ + json_dict = make_json_dict(reaction_type, True) + assert type(cls.de_json(json_dict, bot)) is cls + + def test_to_dict(self, reaction_type): + reaction_type_dict = reaction_type.to_dict() + + assert isinstance(reaction_type_dict, dict) + assert reaction_type_dict["type"] == reaction_type.type + if reaction_type.type == ReactionType.EMOJI: + assert reaction_type_dict["emoji"] == reaction_type.emoji + elif reaction_type.type == ReactionType.CUSTOM_EMOJI: + assert reaction_type_dict["custom_emoji_id"] == reaction_type.custom_emoji_id + + for slot in reaction_type.__slots__: # additional verification for the optional args + assert getattr(reaction_type, slot) == reaction_type_dict[slot] + + def test_reaction_type_api_kwargs(self, reaction_type): + json_dict = make_json_dict(reaction_type_custom_emoji()) + json_dict["custom_arg"] = "wuhu" + reaction_type_custom_emoji_instance = ReactionType.de_json(json_dict, None) + assert reaction_type_custom_emoji_instance.api_kwargs == { + "custom_arg": "wuhu", + } + + def test_equality(self, reaction_type): + a = ReactionTypeEmoji(emoji=RTDefaults.normal_emoji) + b = ReactionTypeEmoji(emoji=RTDefaults.normal_emoji) + c = ReactionTypeCustomEmoji(custom_emoji_id=RTDefaults.custom_emoji) + d = ReactionTypeCustomEmoji(custom_emoji_id=RTDefaults.custom_emoji) + e = ReactionTypeEmoji(emoji=ReactionEmoji.RED_HEART) + f = ReactionTypeCustomEmoji(custom_emoji_id="1234custom") + g = deepcopy(a) + h = deepcopy(c) + i = Dice(4, "emoji") + + assert a == b + assert hash(a) == hash(b) + + assert a != c + assert hash(a) != hash(c) + + assert a != e + assert hash(a) != hash(e) + + assert a == g + assert hash(a) == hash(g) + + assert a != i + assert hash(a) != hash(i) + + assert c == d + assert hash(c) == hash(d) + + assert c != e + assert hash(c) != hash(e) + + assert c != f + assert hash(c) != hash(f) + + assert c == h + assert hash(c) == hash(h) + + assert c != i + assert hash(c) != hash(i) + + +@pytest.fixture(scope="module") +def reaction_count(): + return ReactionCount( + type=TestReactionCountWithoutRequest.type, + total_count=TestReactionCountWithoutRequest.total_count, + ) + + +class TestReactionCountWithoutRequest: + type = ReactionTypeEmoji(ReactionEmoji.THUMBS_UP) + total_count = 42 + + def test_slot_behaviour(self, reaction_count): + for attr in reaction_count.__slots__: + assert getattr(reaction_count, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(reaction_count)) == len( + set(mro_slots(reaction_count)) + ), "duplicate slot" + + def test_de_json(self, bot): + json_dict = { + "type": self.type.to_dict(), + "total_count": self.total_count, + } + + reaction_count = ReactionCount.de_json(json_dict, bot) + assert reaction_count.api_kwargs == {} + + assert isinstance(reaction_count, ReactionCount) + assert reaction_count.type == self.type + assert reaction_count.type.type == self.type.type + assert reaction_count.type.emoji == self.type.emoji + assert reaction_count.total_count == self.total_count + + assert ReactionCount.de_json(None, bot) is None + + def test_to_dict(self, reaction_count): + reaction_count_dict = reaction_count.to_dict() + + assert isinstance(reaction_count_dict, dict) + assert reaction_count_dict["type"] == reaction_count.type.to_dict() + assert reaction_count_dict["total_count"] == reaction_count.total_count + + def test_equality(self, reaction_count): + a = reaction_count + b = ReactionCount( + type=self.type, + total_count=self.total_count, + ) + c = ReactionCount( + type=self.type, + total_count=self.total_count + 1, + ) + d = BotCommand("start", "description") + + assert a == b + assert hash(a) == hash(b) + + assert a != c + assert hash(a) != hash(c) + + assert a != d + assert hash(a) != hash(d) diff --git a/test_reply.py b/test_reply.py new file mode 100644 index 0000000000000000000000000000000000000000..5cacb7b6b5cfb33d42f52ac5821b31605041edde --- /dev/null +++ b/test_reply.py @@ -0,0 +1,291 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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/]. + +import datetime as dtm + +import pytest + +from telegram import ( + BotCommand, + Chat, + ExternalReplyInfo, + Giveaway, + LinkPreviewOptions, + MessageEntity, + MessageOriginUser, + PaidMediaInfo, + PaidMediaPreview, + ReplyParameters, + TextQuote, + User, +) +from tests.auxil.slots import mro_slots + + +@pytest.fixture(scope="module") +def external_reply_info(): + return ExternalReplyInfo( + origin=ExternalReplyInfoTestBase.origin, + chat=ExternalReplyInfoTestBase.chat, + message_id=ExternalReplyInfoTestBase.message_id, + link_preview_options=ExternalReplyInfoTestBase.link_preview_options, + giveaway=ExternalReplyInfoTestBase.giveaway, + paid_media=ExternalReplyInfoTestBase.paid_media, + ) + + +class ExternalReplyInfoTestBase: + origin = MessageOriginUser( + dtm.datetime.now(dtm.timezone.utc).replace(microsecond=0), User(1, "user", False) + ) + chat = Chat(1, Chat.SUPERGROUP) + message_id = 123 + link_preview_options = LinkPreviewOptions(True) + giveaway = Giveaway( + (Chat(1, Chat.CHANNEL), Chat(2, Chat.SUPERGROUP)), + dtm.datetime.now(dtm.timezone.utc).replace(microsecond=0), + 1, + ) + paid_media = PaidMediaInfo(5, [PaidMediaPreview(10, 10, 10)]) + + +class TestExternalReplyInfoWithoutRequest(ExternalReplyInfoTestBase): + def test_slot_behaviour(self, external_reply_info): + for attr in external_reply_info.__slots__: + assert getattr(external_reply_info, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(external_reply_info)) == len( + set(mro_slots(external_reply_info)) + ), "duplicate slot" + + def test_de_json(self, bot): + json_dict = { + "origin": self.origin.to_dict(), + "chat": self.chat.to_dict(), + "message_id": self.message_id, + "link_preview_options": self.link_preview_options.to_dict(), + "giveaway": self.giveaway.to_dict(), + "paid_media": self.paid_media.to_dict(), + } + + external_reply_info = ExternalReplyInfo.de_json(json_dict, bot) + assert external_reply_info.api_kwargs == {} + + assert external_reply_info.origin == self.origin + assert external_reply_info.chat == self.chat + assert external_reply_info.message_id == self.message_id + assert external_reply_info.link_preview_options == self.link_preview_options + assert external_reply_info.giveaway == self.giveaway + assert external_reply_info.paid_media == self.paid_media + + assert ExternalReplyInfo.de_json(None, bot) is None + + def test_to_dict(self, external_reply_info): + ext_reply_info_dict = external_reply_info.to_dict() + + assert isinstance(ext_reply_info_dict, dict) + assert ext_reply_info_dict["origin"] == self.origin.to_dict() + assert ext_reply_info_dict["chat"] == self.chat.to_dict() + assert ext_reply_info_dict["message_id"] == self.message_id + assert ext_reply_info_dict["link_preview_options"] == self.link_preview_options.to_dict() + assert ext_reply_info_dict["giveaway"] == self.giveaway.to_dict() + assert ext_reply_info_dict["paid_media"] == self.paid_media.to_dict() + + def test_equality(self, external_reply_info): + a = external_reply_info + b = ExternalReplyInfo(origin=self.origin) + c = ExternalReplyInfo( + origin=MessageOriginUser(dtm.datetime.utcnow(), User(2, "user", False)) + ) + + d = BotCommand("start", "description") + + assert a == b + assert hash(a) == hash(b) + + assert a != c + assert hash(a) != hash(c) + + assert a != d + assert hash(a) != hash(d) + + +@pytest.fixture(scope="module") +def text_quote(): + return TextQuote( + text=TextQuoteTestBase.text, + position=TextQuoteTestBase.position, + entities=TextQuoteTestBase.entities, + is_manual=TextQuoteTestBase.is_manual, + ) + + +class TextQuoteTestBase: + text = "text" + position = 1 + entities = [ + MessageEntity(MessageEntity.MENTION, 1, 2), + MessageEntity(MessageEntity.EMAIL, 3, 4), + ] + is_manual = True + + +class TestTextQuoteWithoutRequest(TextQuoteTestBase): + def test_slot_behaviour(self, text_quote): + for attr in text_quote.__slots__: + assert getattr(text_quote, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(text_quote)) == len(set(mro_slots(text_quote))), "duplicate slot" + + def test_de_json(self, bot): + json_dict = { + "text": self.text, + "position": self.position, + "entities": [entity.to_dict() for entity in self.entities], + "is_manual": self.is_manual, + } + + text_quote = TextQuote.de_json(json_dict, bot) + assert text_quote.api_kwargs == {} + + assert text_quote.text == self.text + assert text_quote.position == self.position + assert text_quote.entities == tuple(self.entities) + assert text_quote.is_manual == self.is_manual + + assert TextQuote.de_json(None, bot) is None + + def test_to_dict(self, text_quote): + text_quote_dict = text_quote.to_dict() + + assert isinstance(text_quote_dict, dict) + assert text_quote_dict["text"] == self.text + assert text_quote_dict["position"] == self.position + assert text_quote_dict["entities"] == [entity.to_dict() for entity in self.entities] + assert text_quote_dict["is_manual"] == self.is_manual + + def test_equality(self, text_quote): + a = text_quote + b = TextQuote(text=self.text, position=self.position) + c = TextQuote(text="foo", position=self.position) + d = TextQuote(text=self.text, position=7) + + e = BotCommand("start", "description") + + assert a == b + assert hash(a) == hash(b) + + assert a != c + assert hash(a) != hash(c) + + assert a != d + assert hash(a) != hash(d) + + assert a != e + assert hash(a) != hash(e) + + +@pytest.fixture(scope="module") +def reply_parameters(): + return ReplyParameters( + message_id=ReplyParametersTestBase.message_id, + chat_id=ReplyParametersTestBase.chat_id, + allow_sending_without_reply=ReplyParametersTestBase.allow_sending_without_reply, + quote=ReplyParametersTestBase.quote, + quote_parse_mode=ReplyParametersTestBase.quote_parse_mode, + quote_entities=ReplyParametersTestBase.quote_entities, + quote_position=ReplyParametersTestBase.quote_position, + ) + + +class ReplyParametersTestBase: + message_id = 123 + chat_id = 456 + allow_sending_without_reply = True + quote = "foo" + quote_parse_mode = "html" + quote_entities = [ + MessageEntity(MessageEntity.MENTION, 1, 2), + MessageEntity(MessageEntity.EMAIL, 3, 4), + ] + quote_position = 5 + + +class TestReplyParametersWithoutRequest(ReplyParametersTestBase): + def test_slot_behaviour(self, reply_parameters): + for attr in reply_parameters.__slots__: + assert getattr(reply_parameters, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(reply_parameters)) == len( + set(mro_slots(reply_parameters)) + ), "duplicate slot" + + def test_de_json(self, bot): + json_dict = { + "message_id": self.message_id, + "chat_id": self.chat_id, + "allow_sending_without_reply": self.allow_sending_without_reply, + "quote": self.quote, + "quote_parse_mode": self.quote_parse_mode, + "quote_entities": [entity.to_dict() for entity in self.quote_entities], + "quote_position": self.quote_position, + } + + reply_parameters = ReplyParameters.de_json(json_dict, bot) + assert reply_parameters.api_kwargs == {} + + assert reply_parameters.message_id == self.message_id + assert reply_parameters.chat_id == self.chat_id + assert reply_parameters.allow_sending_without_reply == self.allow_sending_without_reply + assert reply_parameters.quote == self.quote + assert reply_parameters.quote_parse_mode == self.quote_parse_mode + assert reply_parameters.quote_entities == tuple(self.quote_entities) + assert reply_parameters.quote_position == self.quote_position + + assert ReplyParameters.de_json(None, bot) is None + + def test_to_dict(self, reply_parameters): + reply_parameters_dict = reply_parameters.to_dict() + + assert isinstance(reply_parameters_dict, dict) + assert reply_parameters_dict["message_id"] == self.message_id + assert reply_parameters_dict["chat_id"] == self.chat_id + assert ( + reply_parameters_dict["allow_sending_without_reply"] + == self.allow_sending_without_reply + ) + assert reply_parameters_dict["quote"] == self.quote + assert reply_parameters_dict["quote_parse_mode"] == self.quote_parse_mode + assert reply_parameters_dict["quote_entities"] == [ + entity.to_dict() for entity in self.quote_entities + ] + assert reply_parameters_dict["quote_position"] == self.quote_position + + def test_equality(self, reply_parameters): + a = reply_parameters + b = ReplyParameters(message_id=self.message_id) + c = ReplyParameters(message_id=7) + + d = BotCommand("start", "description") + + assert a == b + assert hash(a) == hash(b) + + assert a != c + assert hash(a) != hash(c) + + assert a != d + assert hash(a) != hash(d) diff --git a/test_replykeyboardmarkup.py b/test_replykeyboardmarkup.py new file mode 100644 index 0000000000000000000000000000000000000000..68996a246f5f5d8e5edba7fa3cb4a1de2052851c --- /dev/null +++ b/test_replykeyboardmarkup.py @@ -0,0 +1,170 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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/]. + +import pytest + +from telegram import InlineKeyboardMarkup, KeyboardButton, ReplyKeyboardMarkup +from tests.auxil.slots import mro_slots + + +@pytest.fixture(scope="module") +def reply_keyboard_markup(): + return ReplyKeyboardMarkup( + ReplyKeyboardMarkupTestBase.keyboard, + resize_keyboard=ReplyKeyboardMarkupTestBase.resize_keyboard, + one_time_keyboard=ReplyKeyboardMarkupTestBase.one_time_keyboard, + selective=ReplyKeyboardMarkupTestBase.selective, + is_persistent=ReplyKeyboardMarkupTestBase.is_persistent, + ) + + +class ReplyKeyboardMarkupTestBase: + keyboard = [[KeyboardButton("button1"), KeyboardButton("button2")]] + resize_keyboard = True + one_time_keyboard = True + selective = True + is_persistent = True + + +class TestReplyKeyboardMarkupWithoutRequest(ReplyKeyboardMarkupTestBase): + def test_slot_behaviour(self, reply_keyboard_markup): + inst = reply_keyboard_markup + for attr in inst.__slots__: + assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" + + def test_expected_values(self, reply_keyboard_markup): + assert isinstance(reply_keyboard_markup.keyboard, tuple) + assert all(isinstance(row, tuple) for row in reply_keyboard_markup.keyboard) + assert isinstance(reply_keyboard_markup.keyboard[0][0], KeyboardButton) + assert isinstance(reply_keyboard_markup.keyboard[0][1], KeyboardButton) + assert reply_keyboard_markup.resize_keyboard == self.resize_keyboard + assert reply_keyboard_markup.one_time_keyboard == self.one_time_keyboard + assert reply_keyboard_markup.selective == self.selective + assert reply_keyboard_markup.is_persistent == self.is_persistent + + def test_to_dict(self, reply_keyboard_markup): + reply_keyboard_markup_dict = reply_keyboard_markup.to_dict() + + assert isinstance(reply_keyboard_markup_dict, dict) + assert ( + reply_keyboard_markup_dict["keyboard"][0][0] + == reply_keyboard_markup.keyboard[0][0].to_dict() + ) + assert ( + reply_keyboard_markup_dict["keyboard"][0][1] + == reply_keyboard_markup.keyboard[0][1].to_dict() + ) + assert ( + reply_keyboard_markup_dict["resize_keyboard"] == reply_keyboard_markup.resize_keyboard + ) + assert ( + reply_keyboard_markup_dict["one_time_keyboard"] + == reply_keyboard_markup.one_time_keyboard + ) + assert reply_keyboard_markup_dict["selective"] == reply_keyboard_markup.selective + assert reply_keyboard_markup_dict["is_persistent"] == reply_keyboard_markup.is_persistent + + def test_equality(self): + a = ReplyKeyboardMarkup.from_column(["button1", "button2", "button3"]) + b = ReplyKeyboardMarkup.from_column( + [KeyboardButton(text) for text in ["button1", "button2", "button3"]] + ) + c = ReplyKeyboardMarkup.from_column(["button1", "button2"]) + d = ReplyKeyboardMarkup.from_column(["button1", "button2", "button3.1"]) + e = ReplyKeyboardMarkup([["button1", "button1"], ["button2"], ["button3.1"]]) + f = InlineKeyboardMarkup.from_column(["button1", "button2", "button3"]) + + assert a == b + assert hash(a) == hash(b) + + assert a != c + assert hash(a) != hash(c) + + assert a != d + assert hash(a) != hash(d) + + assert a != e + assert hash(a) != hash(e) + + assert a != f + assert hash(a) != hash(f) + + def test_wrong_keyboard_inputs(self): + with pytest.raises(ValueError, match="should be a sequence of sequences"): + ReplyKeyboardMarkup([["button1"], 1]) + with pytest.raises(ValueError, match="should be a sequence of sequences"): + ReplyKeyboardMarkup("strings_are_not_allowed") + with pytest.raises(ValueError, match="should be a sequence of sequences"): + ReplyKeyboardMarkup(["strings_are_not_allowed_in_the_rows_either"]) + with pytest.raises(ValueError, match="should be a sequence of sequences"): + ReplyKeyboardMarkup(KeyboardButton("button1")) + with pytest.raises(ValueError, match="should be a sequence of sequences"): + ReplyKeyboardMarkup([[["button1"]]]) + + def test_from_button(self): + reply_keyboard_markup = ReplyKeyboardMarkup.from_button( + KeyboardButton(text="button1") + ).keyboard + assert len(reply_keyboard_markup) == 1 + assert len(reply_keyboard_markup[0]) == 1 + + reply_keyboard_markup = ReplyKeyboardMarkup.from_button("button1").keyboard + assert len(reply_keyboard_markup) == 1 + assert len(reply_keyboard_markup[0]) == 1 + + def test_from_row(self): + reply_keyboard_markup = ReplyKeyboardMarkup.from_row( + [KeyboardButton(text="button1"), KeyboardButton(text="button2")] + ).keyboard + assert len(reply_keyboard_markup) == 1 + assert len(reply_keyboard_markup[0]) == 2 + + reply_keyboard_markup = ReplyKeyboardMarkup.from_row(["button1", "button2"]).keyboard + assert len(reply_keyboard_markup) == 1 + assert len(reply_keyboard_markup[0]) == 2 + + def test_from_column(self): + reply_keyboard_markup = ReplyKeyboardMarkup.from_column( + [KeyboardButton(text="button1"), KeyboardButton(text="button2")] + ).keyboard + assert len(reply_keyboard_markup) == 2 + assert len(reply_keyboard_markup[0]) == 1 + assert len(reply_keyboard_markup[1]) == 1 + + reply_keyboard_markup = ReplyKeyboardMarkup.from_column(["button1", "button2"]).keyboard + assert len(reply_keyboard_markup) == 2 + assert len(reply_keyboard_markup[0]) == 1 + assert len(reply_keyboard_markup[1]) == 1 + + +class TestReplyKeyboardMarkupWithRequest(ReplyKeyboardMarkupTestBase): + async def test_send_message_with_reply_keyboard_markup( + self, bot, chat_id, reply_keyboard_markup + ): + message = await bot.send_message(chat_id, "Text", reply_markup=reply_keyboard_markup) + + assert message.text == "Text" + + async def test_send_message_with_data_markup(self, bot, chat_id): + message = await bot.send_message( + chat_id, "text 2", reply_markup={"keyboard": [["1", "2"]]} + ) + + assert message.text == "text 2" diff --git a/test_replykeyboardremove.py b/test_replykeyboardremove.py new file mode 100644 index 0000000000000000000000000000000000000000..f0054588168ca9e2d831322f1262eb1f57b3f03d --- /dev/null +++ b/test_replykeyboardremove.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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/]. +import pytest + +from telegram import ReplyKeyboardRemove +from tests.auxil.slots import mro_slots + + +@pytest.fixture(scope="module") +def reply_keyboard_remove(): + return ReplyKeyboardRemove(selective=ReplyKeyboardRemoveTestBase.selective) + + +class ReplyKeyboardRemoveTestBase: + remove_keyboard = True + selective = True + + +class TestReplyKeyboardRemoveWithoutRequest(ReplyKeyboardRemoveTestBase): + def test_slot_behaviour(self, reply_keyboard_remove): + inst = reply_keyboard_remove + for attr in inst.__slots__: + assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" + + def test_expected_values(self, reply_keyboard_remove): + assert reply_keyboard_remove.remove_keyboard == self.remove_keyboard + assert reply_keyboard_remove.selective == self.selective + + def test_to_dict(self, reply_keyboard_remove): + reply_keyboard_remove_dict = reply_keyboard_remove.to_dict() + + assert ( + reply_keyboard_remove_dict["remove_keyboard"] == reply_keyboard_remove.remove_keyboard + ) + assert reply_keyboard_remove_dict["selective"] == reply_keyboard_remove.selective + + +class TestReplyKeyboardRemoveWithRequest(ReplyKeyboardRemoveTestBase): + async def test_send_message_with_reply_keyboard_remove( + self, bot, chat_id, reply_keyboard_remove + ): + message = await bot.send_message(chat_id, "Text", reply_markup=reply_keyboard_remove) + assert message.text == "Text" diff --git a/test_sentwebappmessage.py b/test_sentwebappmessage.py new file mode 100644 index 0000000000000000000000000000000000000000..e4bc116d035ff57c9d305f628ae8f4cbb49ea3f0 --- /dev/null +++ b/test_sentwebappmessage.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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/]. + +import pytest + +from telegram import SentWebAppMessage +from tests.auxil.slots import mro_slots + + +@pytest.fixture(scope="module") +def sent_web_app_message(): + return SentWebAppMessage(inline_message_id=SentWebAppMessageTestBase.inline_message_id) + + +class SentWebAppMessageTestBase: + inline_message_id = "123" + + +class TestSentWebAppMessageWithoutRequest(SentWebAppMessageTestBase): + def test_slot_behaviour(self, sent_web_app_message): + inst = sent_web_app_message + for attr in inst.__slots__: + assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" + + def test_to_dict(self, sent_web_app_message): + sent_web_app_message_dict = sent_web_app_message.to_dict() + + assert isinstance(sent_web_app_message_dict, dict) + assert sent_web_app_message_dict["inline_message_id"] == self.inline_message_id + + def test_de_json(self, bot): + data = {"inline_message_id": self.inline_message_id} + m = SentWebAppMessage.de_json(data, None) + assert m.api_kwargs == {} + assert m.inline_message_id == self.inline_message_id + + def test_equality(self): + a = SentWebAppMessage(self.inline_message_id) + b = SentWebAppMessage(self.inline_message_id) + c = SentWebAppMessage("") + d = SentWebAppMessage("not_inline_message_id") + + assert a == b + assert hash(a) == hash(b) + assert a is not b + + assert a != c + assert hash(a) != hash(c) + + assert a != d + assert hash(a) != hash(d) diff --git a/test_shared.py b/test_shared.py new file mode 100644 index 0000000000000000000000000000000000000000..c51bcf5ea91edd33b04e56d1c6e7586d6182fdcc --- /dev/null +++ b/test_shared.py @@ -0,0 +1,234 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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/]. + +import pytest + +from telegram import ChatShared, PhotoSize, SharedUser, UsersShared +from tests.auxil.slots import mro_slots + + +@pytest.fixture(scope="class") +def users_shared(): + return UsersShared(UsersSharedTestBase.request_id, users=UsersSharedTestBase.users) + + +class UsersSharedTestBase: + request_id = 789 + user_ids = (101112, 101113) + users = (SharedUser(101112, "user1"), SharedUser(101113, "user2")) + + +class TestUsersSharedWithoutRequest(UsersSharedTestBase): + def test_slot_behaviour(self, users_shared): + for attr in users_shared.__slots__: + assert getattr(users_shared, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(users_shared)) == len(set(mro_slots(users_shared))), "duplicate slot" + + def test_to_dict(self, users_shared): + users_shared_dict = users_shared.to_dict() + + assert isinstance(users_shared_dict, dict) + assert users_shared_dict["request_id"] == self.request_id + assert users_shared_dict["users"] == [user.to_dict() for user in self.users] + + def test_de_json(self, bot): + json_dict = { + "request_id": self.request_id, + "users": [user.to_dict() for user in self.users], + "user_ids": self.user_ids, + } + users_shared = UsersShared.de_json(json_dict, bot) + assert users_shared.api_kwargs == {"user_ids": self.user_ids} + + assert users_shared.request_id == self.request_id + assert users_shared.users == self.users + + assert UsersShared.de_json({}, bot) is None + + def test_equality(self): + a = UsersShared(self.request_id, users=self.users) + b = UsersShared(self.request_id, users=self.users) + c = UsersShared(1, users=self.users) + d = UsersShared(self.request_id, users=(SharedUser(1, "user1"), SharedUser(1, "user2"))) + e = PhotoSize("file_id", "1", 1, 1) + + assert a == b + assert hash(a) == hash(b) + assert a is not b + + assert a != c + assert hash(a) != hash(c) + + assert a != d + assert hash(a) != hash(d) + + assert a != e + assert hash(a) != hash(e) + + +@pytest.fixture(scope="class") +def chat_shared(): + return ChatShared( + ChatSharedTestBase.request_id, + ChatSharedTestBase.chat_id, + ) + + +class ChatSharedTestBase: + request_id = 131415 + chat_id = 161718 + + +class TestChatSharedWithoutRequest(ChatSharedTestBase): + def test_slot_behaviour(self, chat_shared): + for attr in chat_shared.__slots__: + assert getattr(chat_shared, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(chat_shared)) == len(set(mro_slots(chat_shared))), "duplicate slot" + + def test_to_dict(self, chat_shared): + chat_shared_dict = chat_shared.to_dict() + + assert isinstance(chat_shared_dict, dict) + assert chat_shared_dict["request_id"] == self.request_id + assert chat_shared_dict["chat_id"] == self.chat_id + + def test_de_json(self, bot): + json_dict = { + "request_id": self.request_id, + "chat_id": self.chat_id, + } + chat_shared = ChatShared.de_json(json_dict, bot) + assert chat_shared.api_kwargs == {} + + assert chat_shared.request_id == self.request_id + assert chat_shared.chat_id == self.chat_id + + def test_equality(self, users_shared): + a = ChatShared(self.request_id, self.chat_id) + b = ChatShared(self.request_id, self.chat_id) + c = ChatShared(1, self.chat_id) + d = ChatShared(self.request_id, 1) + e = users_shared + + assert a == b + assert hash(a) == hash(b) + assert a is not b + + assert a != c + assert hash(a) != hash(c) + + assert a != d + assert hash(a) != hash(d) + + assert a != e + assert hash(a) != hash(e) + + +@pytest.fixture(scope="class") +def shared_user(): + return SharedUser( + SharedUserTestBase.user_id, + SharedUserTestBase.first_name, + last_name=SharedUserTestBase.last_name, + username=SharedUserTestBase.username, + photo=SharedUserTestBase.photo, + ) + + +class SharedUserTestBase: + user_id = 101112 + first_name = "first" + last_name = "last" + username = "user" + photo = ( + PhotoSize(file_id="file_id", width=1, height=1, file_unique_id="1"), + PhotoSize(file_id="file_id", width=2, height=2, file_unique_id="2"), + ) + + +class TestSharedUserWithoutRequest(SharedUserTestBase): + def test_slot_behaviour(self, shared_user): + for attr in shared_user.__slots__: + assert getattr(shared_user, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(shared_user)) == len(set(mro_slots(shared_user))), "duplicate slot" + + def test_to_dict(self, shared_user): + shared_user_dict = shared_user.to_dict() + + assert isinstance(shared_user_dict, dict) + assert shared_user_dict["user_id"] == self.user_id + assert shared_user_dict["first_name"] == self.first_name + assert shared_user_dict["last_name"] == self.last_name + assert shared_user_dict["username"] == self.username + assert shared_user_dict["photo"] == [photo.to_dict() for photo in self.photo] + + def test_de_json_required(self, bot): + json_dict = { + "user_id": self.user_id, + "first_name": self.first_name, + } + shared_user = SharedUser.de_json(json_dict, bot) + assert shared_user.api_kwargs == {} + + assert shared_user.user_id == self.user_id + assert shared_user.first_name == self.first_name + assert shared_user.last_name is None + assert shared_user.username is None + assert shared_user.photo == () + + def test_de_json_all(self, bot): + json_dict = { + "user_id": self.user_id, + "first_name": self.first_name, + "last_name": self.last_name, + "username": self.username, + "photo": [photo.to_dict() for photo in self.photo], + } + shared_user = SharedUser.de_json(json_dict, bot) + assert shared_user.api_kwargs == {} + + assert shared_user.user_id == self.user_id + assert shared_user.first_name == self.first_name + assert shared_user.last_name == self.last_name + assert shared_user.username == self.username + assert shared_user.photo == self.photo + + assert SharedUser.de_json({}, bot) is None + + def test_equality(self, chat_shared): + a = SharedUser( + self.user_id, + self.first_name, + last_name=self.last_name, + username=self.username, + photo=self.photo, + ) + b = SharedUser(self.user_id, "other_firs_name") + c = SharedUser(self.user_id + 1, self.first_name) + d = chat_shared + + assert a == b + assert hash(a) == hash(b) + assert a is not b + + assert a != c + assert hash(a) != hash(c) + + assert a != d + assert hash(a) != hash(d) diff --git a/test_slots.py b/test_slots.py new file mode 100644 index 0000000000000000000000000000000000000000..31a5a7e33121a3a00fc9c13d6667c65bfcd8c13e --- /dev/null +++ b/test_slots.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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/]. +import importlib +import inspect +import os +from pathlib import Path + +included = { # These modules/classes intentionally have __dict__. + "CallbackContext", +} + + +def test_class_has_slots_and_no_dict(): + tg_paths = Path("telegram").rglob("*.py") + + for path in tg_paths: + if "__" in str(path): # Exclude __init__, __main__, etc + continue + mod_name = str(path)[:-3].replace(os.sep, ".") + module = importlib.import_module(mod_name) # import module to get classes in it. + + for name, cls in inspect.getmembers(module, inspect.isclass): + if cls.__module__ != module.__name__ or any( # exclude 'imported' modules + x in name for x in ("__class__", "__init__", "Queue", "Webhook") + ): + continue + + assert "__slots__" in cls.__dict__, f"class '{name}' in {path} doesn't have __slots__" + # if the class slots is a string, then mro_slots() iterates through that string (bad). + assert not isinstance(cls.__slots__, str), f"{name!r}s slots shouldn't be strings" + + # specify if a certain module/class/base class should have dict- + if any(i in included for i in (cls.__module__, name, cls.__base__.__name__)): + assert "__dict__" in get_slots(cls), f"class {name!r} ({path}) has no __dict__" + continue + + assert "__dict__" not in get_slots(cls), f"class '{name}' in {path} has __dict__" + + +def get_slots(_class): + return [attr for cls in _class.__mro__ if hasattr(cls, "__slots__") for attr in cls.__slots__] diff --git a/test_stars.py b/test_stars.py new file mode 100644 index 0000000000000000000000000000000000000000..ef700ae392aa29ac9815cc72797544e2690cd17c --- /dev/null +++ b/test_stars.py @@ -0,0 +1,610 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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/]. + +import datetime +from copy import deepcopy + +import pytest + +from telegram import ( + Dice, + PaidMediaPhoto, + PhotoSize, + RevenueWithdrawalState, + RevenueWithdrawalStateFailed, + RevenueWithdrawalStatePending, + RevenueWithdrawalStateSucceeded, + StarTransaction, + StarTransactions, + TransactionPartner, + TransactionPartnerFragment, + TransactionPartnerOther, + TransactionPartnerTelegramAds, + TransactionPartnerUser, + User, +) +from telegram._utils.datetime import UTC, from_timestamp, to_timestamp +from telegram.constants import RevenueWithdrawalStateType, TransactionPartnerType +from tests.auxil.slots import mro_slots + + +def withdrawal_state_succeeded(): + return RevenueWithdrawalStateSucceeded( + date=datetime.datetime(2024, 1, 1, 0, 0, 0, 0, tzinfo=UTC), + url="url", + ) + + +@pytest.fixture +def withdrawal_state_failed(): + return RevenueWithdrawalStateFailed() + + +@pytest.fixture +def withdrawal_state_pending(): + return RevenueWithdrawalStatePending() + + +def transaction_partner_user(): + return TransactionPartnerUser( + user=User(id=1, is_bot=False, first_name="first_name", username="username"), + invoice_payload="payload", + paid_media=[ + PaidMediaPhoto( + photo=[ + PhotoSize( + file_id="file_id", width=1, height=1, file_unique_id="file_unique_id" + ) + ] + ) + ], + ) + + +@pytest.fixture +def transaction_partner_other(): + return TransactionPartnerOther() + + +def transaction_partner_fragment(): + return TransactionPartnerFragment( + withdrawal_state=withdrawal_state_succeeded(), + ) + + +def star_transaction(): + return StarTransaction( + id="1", + amount=1, + date=to_timestamp(datetime.datetime(2024, 1, 1, 0, 0, 0, 0, tzinfo=UTC)), + source=transaction_partner_user(), + receiver=transaction_partner_fragment(), + ) + + +@pytest.fixture +def star_transactions(): + return StarTransactions( + transactions=[ + star_transaction(), + star_transaction(), + ] + ) + + +@pytest.fixture( + scope="module", + params=[ + TransactionPartner.FRAGMENT, + TransactionPartner.OTHER, + TransactionPartner.USER, + TransactionPartner.TELEGRAM_ADS, + ], +) +def tp_scope_type(request): + return request.param + + +@pytest.fixture( + scope="module", + params=[ + TransactionPartnerFragment, + TransactionPartnerOther, + TransactionPartnerUser, + TransactionPartnerTelegramAds, + ], + ids=[ + TransactionPartner.FRAGMENT, + TransactionPartner.OTHER, + TransactionPartner.USER, + TransactionPartner.TELEGRAM_ADS, + ], +) +def tp_scope_class(request): + return request.param + + +@pytest.fixture( + scope="module", + params=[ + (TransactionPartnerFragment, TransactionPartner.FRAGMENT), + (TransactionPartnerOther, TransactionPartner.OTHER), + (TransactionPartnerUser, TransactionPartner.USER), + (TransactionPartnerTelegramAds, TransactionPartner.TELEGRAM_ADS), + ], + ids=[ + TransactionPartner.FRAGMENT, + TransactionPartner.OTHER, + TransactionPartner.USER, + TransactionPartner.TELEGRAM_ADS, + ], +) +def tp_scope_class_and_type(request): + return request.param + + +@pytest.fixture(scope="module") +def transaction_partner(tp_scope_class_and_type): + # We use de_json here so that we don't have to worry about which class gets which arguments + return tp_scope_class_and_type[0].de_json( + { + "type": tp_scope_class_and_type[1], + "invoice_payload": TransactionPartnerTestBase.invoice_payload, + "withdrawal_state": TransactionPartnerTestBase.withdrawal_state.to_dict(), + "user": TransactionPartnerTestBase.user.to_dict(), + }, + bot=None, + ) + + +@pytest.fixture( + scope="module", + params=[ + RevenueWithdrawalState.FAILED, + RevenueWithdrawalState.SUCCEEDED, + RevenueWithdrawalState.PENDING, + ], +) +def rws_scope_type(request): + return request.param + + +@pytest.fixture( + scope="module", + params=[ + RevenueWithdrawalStateFailed, + RevenueWithdrawalStateSucceeded, + RevenueWithdrawalStatePending, + ], + ids=[ + RevenueWithdrawalState.FAILED, + RevenueWithdrawalState.SUCCEEDED, + RevenueWithdrawalState.PENDING, + ], +) +def rws_scope_class(request): + return request.param + + +@pytest.fixture( + scope="module", + params=[ + (RevenueWithdrawalStateFailed, RevenueWithdrawalState.FAILED), + (RevenueWithdrawalStateSucceeded, RevenueWithdrawalState.SUCCEEDED), + (RevenueWithdrawalStatePending, RevenueWithdrawalState.PENDING), + ], + ids=[ + RevenueWithdrawalState.FAILED, + RevenueWithdrawalState.SUCCEEDED, + RevenueWithdrawalState.PENDING, + ], +) +def rws_scope_class_and_type(request): + return request.param + + +@pytest.fixture(scope="module") +def revenue_withdrawal_state(rws_scope_class_and_type): + # We use de_json here so that we don't have to worry about which class gets which arguments + return rws_scope_class_and_type[0].de_json( + { + "type": rws_scope_class_and_type[1], + "date": to_timestamp(RevenueWithdrawalStateTestBase.date), + "url": RevenueWithdrawalStateTestBase.url, + }, + bot=None, + ) + + +class StarTransactionTestBase: + id = "2" + amount = 2 + date = to_timestamp(datetime.datetime(2024, 1, 1, 0, 0, 0, 0, tzinfo=UTC)) + source = TransactionPartnerUser( + user=User( + id=2, + is_bot=False, + first_name="first_name", + ), + ) + receiver = TransactionPartnerOther() + + +class TestStarTransactionWithoutRequest(StarTransactionTestBase): + def test_slot_behaviour(self): + inst = star_transaction() + for attr in inst.__slots__: + assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" + + def test_de_json(self, bot): + json_dict = { + "id": self.id, + "amount": self.amount, + "date": self.date, + "source": self.source.to_dict(), + "receiver": self.receiver.to_dict(), + } + st = StarTransaction.de_json(json_dict, bot) + st_none = StarTransaction.de_json(None, bot) + assert st.api_kwargs == {} + assert st.id == self.id + assert st.amount == self.amount + assert st.date == from_timestamp(self.date) + assert st.source == self.source + assert st.receiver == self.receiver + assert st_none is None + + def test_de_json_star_transaction_localization(self, tz_bot, bot, raw_bot): + json_dict = star_transaction().to_dict() + st_raw = StarTransaction.de_json(json_dict, raw_bot) + st_bot = StarTransaction.de_json(json_dict, bot) + st_tz = StarTransaction.de_json(json_dict, tz_bot) + + # comparing utcoffsets because comparing timezones is unpredicatable + st_offset = st_tz.date.utcoffset() + tz_bot_offset = tz_bot.defaults.tzinfo.utcoffset(st_tz.date.replace(tzinfo=None)) + + assert st_raw.date.tzinfo == UTC + assert st_bot.date.tzinfo == UTC + assert st_offset == tz_bot_offset + + def test_to_dict(self): + st = star_transaction() + expected_dict = { + "id": "1", + "amount": 1, + "date": st.date, + "source": st.source.to_dict(), + "receiver": st.receiver.to_dict(), + } + assert st.to_dict() == expected_dict + + def test_equality(self): + a = StarTransaction( + id=self.id, + amount=self.amount, + date=self.date, + source=self.source, + receiver=self.receiver, + ) + b = StarTransaction( + id=self.id, + amount=self.amount, + date=None, + source=self.source, + receiver=self.receiver, + ) + c = StarTransaction( + id="3", + amount=3, + date=to_timestamp(datetime.datetime.utcnow()), + source=TransactionPartnerUser( + user=User( + id=3, + is_bot=False, + first_name="first_name", + ), + ), + receiver=TransactionPartnerOther(), + ) + + assert a == b + assert hash(a) == hash(b) + + assert a != c + assert hash(a) != hash(c) + + +class StarTransactionsTestBase: + transactions = [star_transaction(), star_transaction()] + + +class TestStarTransactionsWithoutRequest(StarTransactionsTestBase): + def test_slot_behaviour(self, star_transactions): + inst = star_transactions + for attr in inst.__slots__: + assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" + + def test_de_json(self, bot): + json_dict = { + "transactions": [t.to_dict() for t in self.transactions], + } + st = StarTransactions.de_json(json_dict, bot) + st_none = StarTransactions.de_json(None, bot) + assert st.api_kwargs == {} + assert st.transactions == tuple(self.transactions) + assert st_none is None + + def test_to_dict(self, star_transactions): + expected_dict = { + "transactions": [t.to_dict() for t in self.transactions], + } + assert star_transactions.to_dict() == expected_dict + + def test_equality(self): + a = StarTransactions( + transactions=self.transactions, + ) + b = StarTransactions( + transactions=self.transactions, + ) + c = StarTransactions( + transactions=[star_transaction()], + ) + + assert a == b + assert hash(a) == hash(b) + + assert a != c + assert hash(a) != hash(c) + + +class TransactionPartnerTestBase: + withdrawal_state = withdrawal_state_succeeded() + user = transaction_partner_user().user + invoice_payload = "payload" + + +class TestTransactionPartnerWithoutRequest(TransactionPartnerTestBase): + def test_slot_behaviour(self, transaction_partner): + inst = transaction_partner + for attr in inst.__slots__: + assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" + + def test_de_json(self, bot, tp_scope_class_and_type): + cls = tp_scope_class_and_type[0] + type_ = tp_scope_class_and_type[1] + + json_dict = { + "type": type_, + "invoice_payload": self.invoice_payload, + "withdrawal_state": self.withdrawal_state.to_dict(), + "user": self.user.to_dict(), + } + tp = TransactionPartner.de_json(json_dict, bot) + assert set(tp.api_kwargs.keys()) == {"user", "withdrawal_state", "invoice_payload"} - set( + cls.__slots__ + ) + + assert isinstance(tp, TransactionPartner) + assert type(tp) is cls + assert tp.type == type_ + if "withdrawal_state" in cls.__slots__: + assert tp.withdrawal_state == self.withdrawal_state + if "user" in cls.__slots__: + assert tp.user == self.user + assert tp.invoice_payload == self.invoice_payload + + assert cls.de_json(None, bot) is None + assert TransactionPartner.de_json({}, bot) is None + + def test_de_json_invalid_type(self, bot): + json_dict = { + "type": "invalid", + "invoice_payload": self.invoice_payload, + "withdrawal_state": self.withdrawal_state.to_dict(), + "user": self.user.to_dict(), + } + tp = TransactionPartner.de_json(json_dict, bot) + assert tp.api_kwargs == { + "withdrawal_state": self.withdrawal_state.to_dict(), + "user": self.user.to_dict(), + "invoice_payload": self.invoice_payload, + } + + assert type(tp) is TransactionPartner + assert tp.type == "invalid" + + def test_de_json_subclass(self, tp_scope_class, bot): + """This makes sure that e.g. TransactionPartnerUser(data) never returns a + TransactionPartnerFragment instance.""" + json_dict = { + "type": "invalid", + "invoice_payload": self.invoice_payload, + "withdrawal_state": self.withdrawal_state.to_dict(), + "user": self.user.to_dict(), + } + assert type(tp_scope_class.de_json(json_dict, bot)) is tp_scope_class + + def test_to_dict(self, transaction_partner): + tp_dict = transaction_partner.to_dict() + + assert isinstance(tp_dict, dict) + assert tp_dict["type"] == transaction_partner.type + if hasattr(transaction_partner, "user"): + assert tp_dict["user"] == transaction_partner.user.to_dict() + assert tp_dict["invoice_payload"] == transaction_partner.invoice_payload + if hasattr(transaction_partner, "withdrawal_state"): + assert tp_dict["withdrawal_state"] == transaction_partner.withdrawal_state.to_dict() + + def test_type_enum_conversion(self): + assert type(TransactionPartner("other").type) is TransactionPartnerType + assert TransactionPartner("unknown").type == "unknown" + + def test_equality(self, transaction_partner, bot): + a = TransactionPartner("base_type") + b = TransactionPartner("base_type") + c = transaction_partner + d = deepcopy(transaction_partner) + e = Dice(4, "emoji") + + assert a == b + assert hash(a) == hash(b) + + assert a != c + assert hash(a) != hash(c) + + assert a != d + assert hash(a) != hash(d) + + assert a != e + assert hash(a) != hash(e) + + assert c == d + assert hash(c) == hash(d) + + assert c != e + assert hash(c) != hash(e) + + if hasattr(c, "user"): + json_dict = c.to_dict() + json_dict["user"] = User(2, "something", True).to_dict() + f = c.__class__.de_json(json_dict, bot) + + assert c != f + assert hash(c) != hash(f) + + +class RevenueWithdrawalStateTestBase: + date = datetime.datetime(2024, 1, 1, 0, 0, 0, 0, tzinfo=UTC) + url = "url" + + +class TestRevenueWithdrawalStateWithoutRequest(RevenueWithdrawalStateTestBase): + def test_slot_behaviour(self, revenue_withdrawal_state): + inst = revenue_withdrawal_state + for attr in inst.__slots__: + assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" + + def test_de_json(self, bot, rws_scope_class_and_type): + cls = rws_scope_class_and_type[0] + type_ = rws_scope_class_and_type[1] + + json_dict = { + "type": type_, + "date": to_timestamp(self.date), + "url": self.url, + } + rws = RevenueWithdrawalState.de_json(json_dict, bot) + assert set(rws.api_kwargs.keys()) == {"date", "url"} - set(cls.__slots__) + + assert isinstance(rws, RevenueWithdrawalState) + assert type(rws) is cls + assert rws.type == type_ + if "date" in cls.__slots__: + assert rws.date == self.date + if "url" in cls.__slots__: + assert rws.url == self.url + + assert cls.de_json(None, bot) is None + assert RevenueWithdrawalState.de_json({}, bot) is None + + def test_de_json_invalid_type(self, bot): + json_dict = { + "type": "invalid", + "date": to_timestamp(self.date), + "url": self.url, + } + rws = RevenueWithdrawalState.de_json(json_dict, bot) + assert rws.api_kwargs == { + "date": to_timestamp(self.date), + "url": self.url, + } + + assert type(rws) is RevenueWithdrawalState + assert rws.type == "invalid" + + def test_de_json_subclass(self, rws_scope_class, bot): + """This makes sure that e.g. RevenueWithdrawalState(data) never returns a + RevenueWithdrawalStateFailed instance.""" + json_dict = { + "type": "invalid", + "date": to_timestamp(self.date), + "url": self.url, + } + assert type(rws_scope_class.de_json(json_dict, bot)) is rws_scope_class + + def test_to_dict(self, revenue_withdrawal_state): + rws_dict = revenue_withdrawal_state.to_dict() + + assert isinstance(rws_dict, dict) + assert rws_dict["type"] == revenue_withdrawal_state.type + if hasattr(revenue_withdrawal_state, "date"): + assert rws_dict["date"] == to_timestamp(revenue_withdrawal_state.date) + if hasattr(revenue_withdrawal_state, "url"): + assert rws_dict["url"] == revenue_withdrawal_state.url + + def test_type_enum_conversion(self): + assert type(RevenueWithdrawalState("failed").type) is RevenueWithdrawalStateType + assert RevenueWithdrawalState("unknown").type == "unknown" + + def test_equality(self, revenue_withdrawal_state, bot): + a = RevenueWithdrawalState("base_type") + b = RevenueWithdrawalState("base_type") + c = revenue_withdrawal_state + d = deepcopy(revenue_withdrawal_state) + e = Dice(4, "emoji") + + assert a == b + assert hash(a) == hash(b) + + assert a != c + assert hash(a) != hash(c) + + assert a != d + assert hash(a) != hash(d) + + assert a != e + assert hash(a) != hash(e) + + assert c == d + assert hash(c) == hash(d) + + assert c != e + assert hash(c) != hash(e) + + if hasattr(c, "url"): + json_dict = c.to_dict() + json_dict["url"] = "something" + f = c.__class__.de_json(json_dict, bot) + + assert c == f + assert hash(c) == hash(f) + + if hasattr(c, "date"): + json_dict = c.to_dict() + json_dict["date"] = to_timestamp(datetime.datetime.utcnow()) + f = c.__class__.de_json(json_dict, bot) + + assert c != f + assert hash(c) != hash(f) diff --git a/test_story.py b/test_story.py new file mode 100644 index 0000000000000000000000000000000000000000..1aad292cb45731a4a617a9a783f80b0e084879c3 --- /dev/null +++ b/test_story.py @@ -0,0 +1,72 @@ +#!/usr/bin/env python +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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/]. + +import pytest + +from telegram import Chat, Story +from tests.auxil.slots import mro_slots + + +@pytest.fixture(scope="module") +def story(): + return Story(StoryTestBase.chat, StoryTestBase.id) + + +class StoryTestBase: + chat = Chat(1, "") + id = 0 + + +class TestStoryWithoutRequest(StoryTestBase): + def test_slot_behaviour(self, story): + for attr in story.__slots__: + assert getattr(story, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(story)) == len(set(mro_slots(story))), "duplicate slot" + + def test_de_json(self, bot): + json_dict = {"chat": self.chat.to_dict(), "id": self.id} + story = Story.de_json(json_dict, bot) + assert story.api_kwargs == {} + assert story.chat == self.chat + assert story.id == self.id + assert isinstance(story, Story) + assert Story.de_json(None, bot) is None + + def test_to_dict(self, story): + story_dict = story.to_dict() + assert story_dict["chat"] == self.chat.to_dict() + assert story_dict["id"] == self.id + + def test_equality(self): + a = Story(Chat(1, ""), 0) + b = Story(Chat(1, ""), 0) + c = Story(Chat(1, ""), 1) + d = Story(Chat(2, ""), 0) + e = Chat(1, "") + + assert a == b + assert hash(a) == hash(b) + + assert a != c + assert hash(a) != hash(c) + + assert a != d + assert hash(a) != hash(d) + + assert a != e + assert hash(a) != hash(e) diff --git a/test_switchinlinequerychosenchat.py b/test_switchinlinequerychosenchat.py new file mode 100644 index 0000000000000000000000000000000000000000..e522c1644181fe7bcbce67c5d3b7c90dd886d719 --- /dev/null +++ b/test_switchinlinequerychosenchat.py @@ -0,0 +1,87 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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/]. + +import pytest + +from telegram import SwitchInlineQueryChosenChat +from tests.auxil.slots import mro_slots + + +@pytest.fixture(scope="module") +def switch_inline_query_chosen_chat(): + return SwitchInlineQueryChosenChat( + query=SwitchInlineQueryChosenChatTestBase.query, + allow_user_chats=SwitchInlineQueryChosenChatTestBase.allow_user_chats, + allow_bot_chats=SwitchInlineQueryChosenChatTestBase.allow_bot_chats, + allow_channel_chats=SwitchInlineQueryChosenChatTestBase.allow_channel_chats, + allow_group_chats=SwitchInlineQueryChosenChatTestBase.allow_group_chats, + ) + + +class SwitchInlineQueryChosenChatTestBase: + query = "query" + allow_user_chats = True + allow_bot_chats = True + allow_channel_chats = False + allow_group_chats = True + + +class TestSwitchInlineQueryChosenChat(SwitchInlineQueryChosenChatTestBase): + def test_slot_behaviour(self, switch_inline_query_chosen_chat): + inst = switch_inline_query_chosen_chat + for attr in inst.__slots__: + assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" + + def test_expected_values(self, switch_inline_query_chosen_chat): + assert switch_inline_query_chosen_chat.query == self.query + assert switch_inline_query_chosen_chat.allow_user_chats == self.allow_user_chats + assert switch_inline_query_chosen_chat.allow_bot_chats == self.allow_bot_chats + assert switch_inline_query_chosen_chat.allow_channel_chats == self.allow_channel_chats + assert switch_inline_query_chosen_chat.allow_group_chats == self.allow_group_chats + + def test_to_dict(self, switch_inline_query_chosen_chat): + siqcc = switch_inline_query_chosen_chat.to_dict() + + assert isinstance(siqcc, dict) + assert siqcc["query"] == switch_inline_query_chosen_chat.query + assert siqcc["allow_user_chats"] == switch_inline_query_chosen_chat.allow_user_chats + assert siqcc["allow_bot_chats"] == switch_inline_query_chosen_chat.allow_bot_chats + assert siqcc["allow_channel_chats"] == switch_inline_query_chosen_chat.allow_channel_chats + assert siqcc["allow_group_chats"] == switch_inline_query_chosen_chat.allow_group_chats + + def test_equality(self): + siqcc = SwitchInlineQueryChosenChat + a = siqcc(self.query, self.allow_user_chats, self.allow_bot_chats) + b = siqcc(self.query, self.allow_user_chats, self.allow_bot_chats) + c = siqcc(self.query, self.allow_user_chats) + d = siqcc("", self.allow_user_chats, self.allow_bot_chats) + e = siqcc(self.query, self.allow_user_chats, self.allow_bot_chats, self.allow_group_chats) + + assert a == b + assert hash(a) == hash(b) + + assert a != c + assert hash(a) != hash(c) + + assert a != d + assert hash(a) != hash(d) + + assert a != e + assert hash(a) != hash(e) diff --git a/test_telegramobject.py b/test_telegramobject.py new file mode 100644 index 0000000000000000000000000000000000000000..ca893dec4d89ab0828e28803c4ddab7e87725c0b --- /dev/null +++ b/test_telegramobject.py @@ -0,0 +1,591 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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/]. +import datetime +import inspect +import pickle +import re +from copy import deepcopy +from pathlib import Path +from types import MappingProxyType + +import pytest + +from telegram import Bot, BotCommand, Chat, Message, PhotoSize, TelegramObject, User +from telegram._utils.defaultvalue import DEFAULT_FALSE, DEFAULT_NONE, DefaultValue +from telegram.ext import PicklePersistence +from telegram.warnings import PTBUserWarning +from tests.auxil.files import data_file +from tests.auxil.slots import mro_slots + + +def all_subclasses(cls): + # Gets all subclasses of the specified object, recursively. from + # https://stackoverflow.com/a/3862957/9706202 + # also includes the class itself + return ( + set(cls.__subclasses__()) + .union([s for c in cls.__subclasses__() for s in all_subclasses(c)]) + .union({cls}) + ) + + +TO_SUBCLASSES = sorted(all_subclasses(TelegramObject), key=lambda cls: cls.__name__) + + +class TestTelegramObject: + class Sub(TelegramObject): + def __init__(self, private, normal, b): + super().__init__() + self._private = private + self.normal = normal + self._bot = b + + class ChangingTO(TelegramObject): + # Don't use in any tests, this is just for testing the pickle behaviour and the + # class is altered during the test procedure + pass + + def test_to_json(self, monkeypatch): + class Subclass(TelegramObject): + def __init__(self): + super().__init__() + self.arg = "arg" + self.arg2 = ["arg2", "arg2"] + self.arg3 = {"arg3": "arg3"} + self.empty_tuple = () + + json = Subclass().to_json() + # Order isn't guarantied + assert '"arg": "arg"' in json + assert '"arg2": ["arg2", "arg2"]' in json + assert '"arg3": {"arg3": "arg3"}' in json + assert "empty_tuple" not in json + + # Now make sure that it doesn't work with not json stuff and that it fails loudly + # Tuples aren't allowed as keys in json + d = {("str", "str"): "str"} + + monkeypatch.setattr("telegram.TelegramObject.to_dict", lambda _: d) + with pytest.raises(TypeError): + TelegramObject().to_json() + + def test_de_json_api_kwargs(self, bot): + to = TelegramObject.de_json(data={"foo": "bar"}, bot=bot) + assert to.api_kwargs == {"foo": "bar"} + assert to.get_bot() is bot + + def test_de_json_optional_bot(self): + to = TelegramObject.de_json(data={}) + with pytest.raises(RuntimeError, match="no bot associated with it"): + to.get_bot() + + def test_de_list(self, bot): + class SubClass(TelegramObject): + def __init__(self, arg: int, **kwargs): + super().__init__(**kwargs) + self.arg = arg + + self._id_attrs = (self.arg,) + + assert SubClass.de_list([{"arg": 1}, None, {"arg": 2}, None], bot) == ( + SubClass(1), + SubClass(2), + ) + + def test_api_kwargs_read_only(self): + tg_object = TelegramObject(api_kwargs={"foo": "bar"}) + tg_object._freeze() + assert isinstance(tg_object.api_kwargs, MappingProxyType) + with pytest.raises(TypeError): + tg_object.api_kwargs["foo"] = "baz" + with pytest.raises(AttributeError, match="can't be set"): + tg_object.api_kwargs = {"foo": "baz"} + + @pytest.mark.parametrize("cls", TO_SUBCLASSES, ids=[cls.__name__ for cls in TO_SUBCLASSES]) + def test_subclasses_have_api_kwargs(self, cls): + """Checks that all subclasses of TelegramObject have an api_kwargs argument that is + kw-only. Also, tries to check that this argument is passed to super - by checking that + the `__init__` contains `api_kwargs=api_kwargs` + """ + if issubclass(cls, Bot): + # Bot doesn't have api_kwargs, because it's not defined by TG + return + + # only relevant for subclasses that have their own init + if inspect.getsourcefile(cls.__init__) != inspect.getsourcefile(cls): + return + + # Ignore classes in the test directory + source_file = Path(inspect.getsourcefile(cls)) + parents = source_file.parents + is_test_file = Path(__file__).parent.resolve() in parents + if is_test_file: + return + + # check the signature first + signature = inspect.signature(cls) + assert signature.parameters.get("api_kwargs").kind == inspect.Parameter.KEYWORD_ONLY + + # Now check for `api_kwargs=api_kwargs` in the source code of `__init__` + if cls is TelegramObject: + # TelegramObject doesn't have a super class + return + assert "api_kwargs=api_kwargs" in inspect.getsource( + cls.__init__ + ), f"{cls.__name__} doesn't seem to pass `api_kwargs` to `super().__init__`" + + def test_de_json_arbitrary_exceptions(self, bot): + class SubClass(TelegramObject): + def __init__(self, **kwargs): + super().__init__(**kwargs) + raise TypeError("This is a test") + + with pytest.raises(TypeError, match="This is a test"): + SubClass.de_json({}, bot) + + def test_to_dict_private_attribute(self): + class TelegramObjectSubclass(TelegramObject): + __slots__ = ("_b", "a") # Added slots so that the attrs are converted to dict + + def __init__(self): + super().__init__() + self.a = 1 + self._b = 2 + + subclass_instance = TelegramObjectSubclass() + assert subclass_instance.to_dict() == {"a": 1} + + def test_to_dict_api_kwargs(self): + to = TelegramObject(api_kwargs={"foo": "bar"}) + assert to.to_dict() == {"foo": "bar"} + + def test_to_dict_missing_attribute(self): + message = Message( + 1, datetime.datetime.now(), Chat(1, "private"), from_user=User(1, "", False) + ) + message._unfreeze() + del message.chat + + message_dict = message.to_dict() + assert "chat" not in message_dict + + message_dict = message.to_dict(recursive=False) + assert message_dict["chat"] is None + + def test_to_dict_recursion(self): + class Recursive(TelegramObject): + __slots__ = ("recursive",) + + def __init__(self): + super().__init__() + self.recursive = "recursive" + + class SubClass(TelegramObject): + """This class doesn't have `__slots__`, so has `__dict__` instead.""" + + def __init__(self): + super().__init__() + self.subclass = Recursive() + + to = SubClass() + to_dict_no_recurse = to.to_dict(recursive=False) + assert to_dict_no_recurse + assert isinstance(to_dict_no_recurse["subclass"], Recursive) + to_dict_recurse = to.to_dict(recursive=True) + assert to_dict_recurse + assert isinstance(to_dict_recurse["subclass"], dict) + assert to_dict_recurse["subclass"]["recursive"] == "recursive" + + def test_to_dict_default_value(self): + class SubClass(TelegramObject): + def __init__(self): + super().__init__() + self.default_none = DEFAULT_NONE + self.default_false = DEFAULT_FALSE + + to = SubClass() + to_dict = to.to_dict() + assert "default_none" not in to_dict + assert to_dict["default_false"] is False + + def test_slot_behaviour(self): + inst = TelegramObject() + for attr in inst.__slots__: + assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" + + def test_meaningless_comparison(self, recwarn): + expected_warning = "Objects of type TGO can not be meaningfully tested for equivalence." + + class TGO(TelegramObject): + pass + + a = TGO() + b = TGO() + assert a == b + assert len(recwarn) == 1 + assert str(recwarn[0].message) == expected_warning + assert recwarn[0].category is PTBUserWarning + assert recwarn[0].filename == __file__, "wrong stacklevel" + + def test_meaningful_comparison(self, recwarn): + class TGO(TelegramObject): + def __init__(self): + self._id_attrs = (1,) + + a = TGO() + b = TGO() + assert a == b + assert len(recwarn) == 0 + assert b == a + assert len(recwarn) == 0 + + def test_bot_instance_none(self): + tg_object = TelegramObject() + with pytest.raises(RuntimeError): + tg_object.get_bot() + + @pytest.mark.parametrize("bot_inst", ["bot", None]) + def test_bot_instance_states(self, bot_inst): + tg_object = TelegramObject() + tg_object.set_bot("bot" if bot_inst == "bot" else bot_inst) + if bot_inst == "bot": + assert tg_object.get_bot() == "bot" + elif bot_inst is None: + with pytest.raises(RuntimeError): + tg_object.get_bot() + + def test_subscription(self): + # We test with Message because that gives us everything we want to test - easier than + # implementing a custom subclass just for this test + chat = Chat(2, Chat.PRIVATE) + user = User(3, "first_name", False) + message = Message(1, None, chat=chat, from_user=user, text="foobar") + assert message["text"] == "foobar" + assert message["chat"] is chat + assert message["chat_id"] == 2 + assert message["from"] is user + assert message["from_user"] is user + with pytest.raises(KeyError, match="Message don't have an attribute called `no_key`"): + message["no_key"] + + def test_pickle(self, bot): + chat = Chat(2, Chat.PRIVATE) + user = User(3, "first_name", False) + date = datetime.datetime.now() + photo = PhotoSize("file_id", "unique", 21, 21) + photo.set_bot(bot) + msg = Message( + 1, + date, + chat, + from_user=user, + text="foobar", + photo=[photo], + animation=DEFAULT_NONE, + api_kwargs={"api": "kwargs"}, + ) + msg.set_bot(bot) + + # Test pickling of TGObjects, we choose Message since it's contains the most subclasses. + assert msg.get_bot() + unpickled = pickle.loads(pickle.dumps(msg)) + + with pytest.raises(RuntimeError): + unpickled.get_bot() # There should be no bot when we pickle TGObjects + + assert unpickled.chat == chat, f"{unpickled.chat._id_attrs} != {chat._id_attrs}" + assert unpickled.from_user == user + assert unpickled.date == date, f"{unpickled.date} != {date}" + assert unpickled.photo[0] == photo + assert isinstance(unpickled.animation, DefaultValue) + assert unpickled.animation.value is None + assert isinstance(unpickled.api_kwargs, MappingProxyType) + assert unpickled.api_kwargs == {"api": "kwargs"} + + def test_pickle_apply_api_kwargs(self): + """Makes sure that when a class gets new attributes, the api_kwargs are moved to the + new attributes on unpickling.""" + obj = self.ChangingTO(api_kwargs={"foo": "bar"}) + pickled = pickle.dumps(obj) + + self.ChangingTO.foo = None + obj = pickle.loads(pickled) + + assert obj.foo == "bar" + assert obj.api_kwargs == {} + + async def test_pickle_backwards_compatibility(self): + """Test when newer versions of the library remove or add attributes from classes (which + the old pickled versions still/don't have). + """ + # We use a modified version of the 20.0a5 Chat class, which + # * has an `all_members_are_admins` attribute, + # * a non-empty `api_kwargs` dict + # * does not have the `is_forum` attribute + # This specific version was pickled + # using PicklePersistence.update_chat_data and that's what we use here to test if + # * the (now) removed attribute `all_members_are_admins` was added to api_kwargs + # * the (now) added attribute `is_forum` does not affect the unpickling + pp = PicklePersistence(data_file("20a5_modified_chat.pickle")) + chat = (await pp.get_chat_data())[1] + assert chat.id == 1 + assert chat.type == Chat.PRIVATE + api_kwargs_expected = { + "all_members_are_administrators": True, + "something": "Manually inserted", + } + # There are older attrs in Chat's api_kwargs which are present but we don't care about them + for k, v in api_kwargs_expected.items(): + assert chat.api_kwargs[k] == v + + with pytest.raises(AttributeError): + # removed attribute should not be available as attribute, only though api_kwargs + chat.all_members_are_administrators + with pytest.raises(AttributeError): + # New attribute should not be available either as is always the case for pickle + chat.is_forum + + # Ensure that loading objects that were pickled before attributes were made immutable + # are still mutable + chat.id = 7 + assert chat.id == 7 + + def test_pickle_handle_properties(self): + # Very hard to properly test, can't use a pickle file since newer versions of the library + # will stop having the property. + # The code below uses exec statements to simulate library changes. There is no other way + # to test this. + # Original class: + v1 = """ +class PicklePropertyTest(TelegramObject): + __slots__ = ("forward_from", "to_be_removed", "forward_date") + def __init__(self, forward_from=None, forward_date=None, api_kwargs=None): + super().__init__(api_kwargs=api_kwargs) + self.forward_from = forward_from + self.forward_date = forward_date + self.to_be_removed = "to_be_removed" +""" + exec(v1, globals(), None) + old = PicklePropertyTest("old_val", "date", api_kwargs={"new_attr": 1}) # noqa: F821 + pickled_v1 = pickle.dumps(old) + + # After some API changes: + v2 = """ +class PicklePropertyTest(TelegramObject): + __slots__ = ("_forward_from", "_date", "_new_attr") + def __init__(self, forward_from=None, f_date=None, new_attr=None, api_kwargs=None): + super().__init__(api_kwargs=api_kwargs) + self._forward_from = forward_from + self.f_date = f_date + self._new_attr = new_attr + @property + def forward_from(self): + return self._forward_from + @property + def forward_date(self): + return self.f_date + @property + def new_attr(self): + return self._new_attr + """ + exec(v2, globals(), None) + v2_unpickle = pickle.loads(pickled_v1) + assert v2_unpickle.forward_from == "old_val" == v2_unpickle._forward_from + with pytest.raises(AttributeError): + # New attribute should not be available either as is always the case for pickle + v2_unpickle.forward_date + assert v2_unpickle.new_attr == 1 == v2_unpickle._new_attr + assert not hasattr(v2_unpickle, "to_be_removed") + assert v2_unpickle.api_kwargs == {"to_be_removed": "to_be_removed"} + pickled_v2 = pickle.dumps(v2_unpickle) + + # After PTB removes the property and the attribute: + v3 = """ +class PicklePropertyTest(TelegramObject): + __slots__ = () + def __init__(self, api_kwargs=None): + super().__init__(api_kwargs=api_kwargs) +""" + exec(v3, globals(), None) + v3_unpickle = pickle.loads(pickled_v2) + assert v3_unpickle.api_kwargs == {"to_be_removed": "to_be_removed"} + assert not hasattr(v3_unpickle, "_forward_from") + assert not hasattr(v3_unpickle, "_new_attr") + + def test_deepcopy_telegram_obj(self, bot): + chat = Chat(2, Chat.PRIVATE) + user = User(3, "first_name", False) + date = datetime.datetime.now() + photo = PhotoSize("file_id", "unique", 21, 21) + photo.set_bot(bot) + msg = Message( + 1, date, chat, from_user=user, text="foobar", photo=[photo], api_kwargs={"foo": "bar"} + ) + msg.set_bot(bot) + + new_msg = deepcopy(msg) + + assert new_msg == msg + assert new_msg is not msg + + # The same bot should be present when deepcopying. + assert new_msg.get_bot() == bot + assert new_msg.get_bot() is bot + + assert new_msg.date == date + assert new_msg.date is not date + assert new_msg.chat == chat + assert new_msg.chat is not chat + assert new_msg.from_user == user + assert new_msg.from_user is not user + assert new_msg.photo[0] == photo + assert new_msg.photo[0] is not photo + assert new_msg.api_kwargs == {"foo": "bar"} + assert new_msg.api_kwargs is not msg.api_kwargs + + # check that deepcopy preserves the freezing status + with pytest.raises( + AttributeError, match="Attribute `text` of class `Message` can't be set!" + ): + new_msg.text = "new text" + + msg._unfreeze() + new_message = deepcopy(msg) + new_message.text = "new text" + assert new_message.text == "new text" + + def test_deepcopy_subclass_telegram_obj(self, bot): + s = self.Sub("private", "normal", bot) + d = deepcopy(s) + assert d is not s + assert d._private == s._private # Can't test for identity since two equal strings is True + assert d._bot == s._bot + assert d._bot is s._bot + assert d.normal == s.normal + + def test_string_representation(self): + class TGO(TelegramObject): + def __init__(self, api_kwargs=None): + super().__init__(api_kwargs=api_kwargs) + self.string_attr = "string" + self.int_attr = 42 + self.to_attr = BotCommand("command", "description") + self.list_attr = [ + BotCommand("command_1", "description_1"), + BotCommand("command_2", "description_2"), + ] + self.dict_attr = { + BotCommand("command_1", "description_1"): BotCommand( + "command_2", "description_2" + ) + } + self.empty_tuple_attrs = () + self.empty_str_attribute = "" + # Should not be included in string representation + self.none_attr = None + + expected_without_api_kwargs = ( + "TGO(dict_attr={BotCommand(command='command_1', description='description_1'): " + "BotCommand(command='command_2', description='description_2')}, int_attr=42, " + "list_attr=[BotCommand(command='command_1', description='description_1'), " + "BotCommand(command='command_2', description='description_2')], " + "string_attr='string', to_attr=BotCommand(command='command', " + "description='description'))" + ) + assert str(TGO()) == expected_without_api_kwargs + assert repr(TGO()) == expected_without_api_kwargs + + expected_with_api_kwargs = ( + "TGO(api_kwargs={'foo': 'bar'}, dict_attr={BotCommand(command='command_1', " + "description='description_1'): BotCommand(command='command_2', " + "description='description_2')}, int_attr=42, " + "list_attr=[BotCommand(command='command_1', description='description_1'), " + "BotCommand(command='command_2', description='description_2')], " + "string_attr='string', to_attr=BotCommand(command='command', " + "description='description'))" + ) + assert str(TGO(api_kwargs={"foo": "bar"})) == expected_with_api_kwargs + assert repr(TGO(api_kwargs={"foo": "bar"})) == expected_with_api_kwargs + + @pytest.mark.parametrize("cls", TO_SUBCLASSES, ids=[cls.__name__ for cls in TO_SUBCLASSES]) + def test_subclasses_are_frozen(self, cls): + if cls is TelegramObject or cls.__name__.startswith("_"): + # Protected classes don't need to be frozen and neither does the base class + return + + # instantiating each subclass would be tedious as some attributes require special init + # args. So we inspect the code instead. + + source_file = inspect.getsourcefile(cls.__init__) + parents = Path(source_file).parents + is_test_file = Path(__file__).parent.resolve() in parents + + if is_test_file: + # If the class is defined in a test file, we don't want to test it. + return + + if source_file.endswith("telegramobject.py"): + pytest.fail( + f"{cls.__name__} does not have its own `__init__` " + "and can therefore not be frozen correctly" + ) + + source_lines, _ = inspect.getsourcelines(cls.__init__) + + # We use regex matching since a simple "if self._freeze() in source_lines[-1]" would also + # allo commented lines. + last_line_freezes = re.match(r"\s*self\.\_freeze\(\)", source_lines[-1]) + uses_with_unfrozen = re.search( + r"\n\s*with self\.\_unfrozen\(\)\:", inspect.getsource(cls.__init__) + ) + + assert last_line_freezes or uses_with_unfrozen, f"{cls.__name__} is not frozen correctly" + + def test_freeze_unfreeze(self): + class TestSub(TelegramObject): + def __init__(self): + super().__init__() + self._protected = True + self.public = True + self._freeze() + + foo = TestSub() + foo._protected = False + assert foo._protected is False + + with pytest.raises( + AttributeError, match="Attribute `public` of class `TestSub` can't be set!" + ): + foo.public = False + + with pytest.raises( + AttributeError, match="Attribute `public` of class `TestSub` can't be deleted!" + ): + del foo.public + + foo._unfreeze() + foo._protected = True + assert foo._protected is True + foo.public = False + assert foo.public is False + del foo.public + del foo._protected + assert not hasattr(foo, "public") + assert not hasattr(foo, "_protected") diff --git a/test_update.py b/test_update.py new file mode 100644 index 0000000000000000000000000000000000000000..46619fbfd9d09c6355a4bb74edc736a932cab233 --- /dev/null +++ b/test_update.py @@ -0,0 +1,427 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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/]. +import time +from copy import deepcopy +from datetime import datetime + +import pytest + +from telegram import ( + BusinessConnection, + BusinessMessagesDeleted, + CallbackQuery, + Chat, + ChatBoost, + ChatBoostRemoved, + ChatBoostSourcePremium, + ChatBoostUpdated, + ChatJoinRequest, + ChatMemberOwner, + ChatMemberUpdated, + ChosenInlineResult, + InaccessibleMessage, + InlineQuery, + Message, + MessageReactionCountUpdated, + MessageReactionUpdated, + Poll, + PollAnswer, + PollOption, + PreCheckoutQuery, + ReactionCount, + ReactionTypeEmoji, + ShippingQuery, + Update, + User, +) +from telegram._utils.datetime import from_timestamp +from telegram.warnings import PTBUserWarning +from tests.auxil.slots import mro_slots + +message = Message( + 1, + datetime.utcnow(), + Chat(1, ""), + from_user=User(1, "", False), + text="Text", + sender_chat=Chat(1, ""), +) +channel_post = Message( + 1, + datetime.utcnow(), + Chat(1, ""), + text="Text", + sender_chat=Chat(1, ""), +) +chat_member_updated = ChatMemberUpdated( + Chat(1, "chat"), + User(1, "", False), + from_timestamp(int(time.time())), + ChatMemberOwner(User(1, "", False), True), + ChatMemberOwner(User(1, "", False), True), +) + + +chat_join_request = ChatJoinRequest( + chat=Chat(1, Chat.SUPERGROUP), + from_user=User(1, "first_name", False), + date=from_timestamp(int(time.time())), + user_chat_id=1, + bio="bio", +) + +chat_boost = ChatBoostUpdated( + chat=Chat(1, "priv"), + boost=ChatBoost( + "1", + from_timestamp(int(time.time())), + from_timestamp(int(time.time())), + ChatBoostSourcePremium(User(1, "", False)), + ), +) + +removed_chat_boost = ChatBoostRemoved( + Chat(1, "private"), + "2", + from_timestamp(int(time.time())), + ChatBoostSourcePremium(User(1, "name", False)), +) + +message_reaction = MessageReactionUpdated( + chat=Chat(1, "chat"), + message_id=1, + date=from_timestamp(int(time.time())), + old_reaction=(ReactionTypeEmoji("👍"),), + new_reaction=(ReactionTypeEmoji("👍"),), + user=User(1, "name", False), + actor_chat=Chat(1, ""), +) + + +message_reaction_count = MessageReactionCountUpdated( + chat=Chat(1, "chat"), + message_id=1, + date=from_timestamp(int(time.time())), + reactions=(ReactionCount(ReactionTypeEmoji("👍"), 1),), +) + +business_connection = BusinessConnection( + "1", + User(1, "name", False), + 1, + from_timestamp(int(time.time())), + True, + True, +) + +deleted_business_messages = BusinessMessagesDeleted( + "1", + Chat(1, ""), + (1, 2), +) + +business_message = Message( + 1, + datetime.utcnow(), + Chat(1, ""), + User(1, "", False), +) + + +params = [ + {"message": message}, + {"edited_message": message}, + {"callback_query": CallbackQuery(1, User(1, "", False), "chat", message=message)}, + {"channel_post": channel_post}, + {"edited_channel_post": channel_post}, + {"inline_query": InlineQuery(1, User(1, "", False), "", "")}, + {"chosen_inline_result": ChosenInlineResult("id", User(1, "", False), "")}, + {"shipping_query": ShippingQuery("id", User(1, "", False), "", None)}, + {"pre_checkout_query": PreCheckoutQuery("id", User(1, "", False), "", 0, "")}, + {"poll": Poll("id", "?", [PollOption(".", 1)], False, False, False, Poll.REGULAR, True)}, + { + "poll_answer": PollAnswer( + "id", + [1], + User( + 1, + "", + False, + ), + Chat(1, ""), + ) + }, + {"my_chat_member": chat_member_updated}, + {"chat_member": chat_member_updated}, + {"chat_join_request": chat_join_request}, + {"chat_boost": chat_boost}, + {"removed_chat_boost": removed_chat_boost}, + {"message_reaction": message_reaction}, + {"message_reaction_count": message_reaction_count}, + {"business_connection": business_connection}, + {"deleted_business_messages": deleted_business_messages}, + {"business_message": business_message}, + {"edited_business_message": business_message}, + # Must be last to conform with `ids` below! + {"callback_query": CallbackQuery(1, User(1, "", False), "chat")}, +] + +all_types = ( + "message", + "edited_message", + "callback_query", + "channel_post", + "edited_channel_post", + "inline_query", + "chosen_inline_result", + "shipping_query", + "pre_checkout_query", + "poll", + "poll_answer", + "my_chat_member", + "chat_member", + "chat_join_request", + "chat_boost", + "removed_chat_boost", + "message_reaction", + "message_reaction_count", + "business_connection", + "deleted_business_messages", + "business_message", + "edited_business_message", +) + +ids = (*all_types, "callback_query_without_message") + + +@pytest.fixture(scope="module", params=params, ids=ids) +def update(request): + return Update(update_id=UpdateTestBase.update_id, **request.param) + + +class UpdateTestBase: + update_id = 868573637 + + +class TestUpdateWithoutRequest(UpdateTestBase): + def test_slot_behaviour(self): + update = Update(self.update_id) + for attr in update.__slots__: + assert getattr(update, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(update)) == len(set(mro_slots(update))), "duplicate slot" + + @pytest.mark.parametrize("paramdict", argvalues=params, ids=ids) + def test_de_json(self, bot, paramdict): + json_dict = {"update_id": self.update_id} + # Convert the single update 'item' to a dict of that item and apply it to the json_dict + json_dict.update({k: v.to_dict() for k, v in paramdict.items()}) + update = Update.de_json(json_dict, bot) + assert update.api_kwargs == {} + + assert update.update_id == self.update_id + + # Make sure only one thing in the update (other than update_id) is not None + i = 0 + for _type in all_types: + if getattr(update, _type) is not None: + i += 1 + assert getattr(update, _type) == paramdict[_type] + assert i == 1 + + def test_update_de_json_empty(self, bot): + update = Update.de_json(None, bot) + + assert update is None + + def test_to_dict(self, update): + update_dict = update.to_dict() + + assert isinstance(update_dict, dict) + assert update_dict["update_id"] == update.update_id + for _type in all_types: + if getattr(update, _type) is not None: + assert update_dict[_type] == getattr(update, _type).to_dict() + + def test_equality(self): + a = Update(self.update_id, message=message) + b = Update(self.update_id, message=message) + c = Update(self.update_id) + d = Update(0, message=message) + e = User(self.update_id, "", False) + + assert a == b + assert hash(a) == hash(b) + assert a is not b + + assert a == c + assert hash(a) == hash(c) + + assert a != d + assert hash(a) != hash(d) + + assert a != e + assert hash(a) != hash(e) + + def test_effective_chat(self, update): + # Test that it's sometimes None per docstring + chat = update.effective_chat + if not ( + update.inline_query is not None + or update.chosen_inline_result is not None + or (update.callback_query is not None and update.callback_query.message is None) + or update.shipping_query is not None + or update.pre_checkout_query is not None + or update.poll is not None + or update.poll_answer is not None + or update.business_connection is not None + ): + assert chat.id == 1 + else: + assert chat is None + + def test_effective_user(self, update): + # Test that it's sometimes None per docstring + user = update.effective_user + if not ( + update.channel_post is not None + or update.edited_channel_post is not None + or update.poll is not None + or update.chat_boost is not None + or update.removed_chat_boost is not None + or update.message_reaction_count is not None + or update.deleted_business_messages is not None + ): + assert user.id == 1 + else: + assert user is None + + def test_effective_sender_non_anonymous(self, update): + update = deepcopy(update) + # Simulate 'Remain anonymous' being turned off + if message := (update.message or update.edited_message): + message._unfreeze() + message.sender_chat = None + elif reaction := (update.message_reaction): + reaction._unfreeze() + reaction.actor_chat = None + elif answer := (update.poll_answer): + answer._unfreeze() + answer.voter_chat = None + + # Test that it's sometimes None per docstring + sender = update.effective_sender + if not ( + update.poll is not None + or update.chat_boost is not None + or update.removed_chat_boost is not None + or update.message_reaction_count is not None + or update.deleted_business_messages is not None + ): + if update.channel_post or update.edited_channel_post: + assert isinstance(sender, Chat) + else: + assert isinstance(sender, User) + + else: + assert sender is None + + cached = update.effective_sender + assert cached is sender + + def test_effective_sender_anonymous(self, update): + update = deepcopy(update) + # Simulate 'Remain anonymous' being turned on + if message := (update.message or update.edited_message): + message._unfreeze() + message.from_user = None + elif reaction := (update.message_reaction): + reaction._unfreeze() + reaction.user = None + elif answer := (update.poll_answer): + answer._unfreeze() + answer.user = None + + # Test that it's sometimes None per docstring + sender = update.effective_sender + if not ( + update.poll is not None + or update.chat_boost is not None + or update.removed_chat_boost is not None + or update.message_reaction_count is not None + or update.deleted_business_messages is not None + ): + if ( + update.message + or update.edited_message + or update.channel_post + or update.edited_channel_post + or update.message_reaction + or update.poll_answer + ): + assert isinstance(sender, Chat) + else: + assert isinstance(sender, User) + else: + assert sender is None + + cached = update.effective_sender + assert cached is sender + + def test_effective_message(self, update): + # Test that it's sometimes None per docstring + eff_message = update.effective_message + if not ( + update.inline_query is not None + or update.chosen_inline_result is not None + or (update.callback_query is not None and update.callback_query.message is None) + or update.shipping_query is not None + or update.pre_checkout_query is not None + or update.poll is not None + or update.poll_answer is not None + or update.my_chat_member is not None + or update.chat_member is not None + or update.chat_join_request is not None + or update.chat_boost is not None + or update.removed_chat_boost is not None + or update.message_reaction is not None + or update.message_reaction_count is not None + or update.deleted_business_messages is not None + or update.business_connection is not None + ): + assert eff_message.message_id == message.message_id + else: + assert eff_message is None + + def test_effective_message_inaccessible(self): + update = Update( + update_id=1, + callback_query=CallbackQuery( + "id", + User(1, "", False), + "chat", + message=InaccessibleMessage(message_id=1, chat=Chat(1, "")), + ), + ) + with pytest.warns( + PTBUserWarning, + match="update.callback_query` is not `None`, but of type `InaccessibleMessage`", + ) as record: + assert update.effective_message is None + + assert record[0].filename == __file__ diff --git a/test_user.py b/test_user.py new file mode 100644 index 0000000000000000000000000000000000000000..073d0af58e075565f385908f8be1ee26b914ed4e --- /dev/null +++ b/test_user.py @@ -0,0 +1,722 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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/]. +import pytest + +from telegram import Bot, InlineKeyboardButton, Update, User +from telegram.helpers import escape_markdown +from tests.auxil.bot_method_checks import ( + check_defaults_handling, + check_shortcut_call, + check_shortcut_signature, +) +from tests.auxil.slots import mro_slots + + +@pytest.fixture(scope="module") +def json_dict(): + return { + "id": UserTestBase.id_, + "is_bot": UserTestBase.is_bot, + "first_name": UserTestBase.first_name, + "last_name": UserTestBase.last_name, + "username": UserTestBase.username, + "language_code": UserTestBase.language_code, + "can_join_groups": UserTestBase.can_join_groups, + "can_read_all_group_messages": UserTestBase.can_read_all_group_messages, + "supports_inline_queries": UserTestBase.supports_inline_queries, + "is_premium": UserTestBase.is_premium, + "added_to_attachment_menu": UserTestBase.added_to_attachment_menu, + "can_connect_to_business": UserTestBase.can_connect_to_business, + "has_main_web_app": UserTestBase.has_main_web_app, + } + + +@pytest.fixture +def user(bot): + user = User( + id=UserTestBase.id_, + first_name=UserTestBase.first_name, + is_bot=UserTestBase.is_bot, + last_name=UserTestBase.last_name, + username=UserTestBase.username, + language_code=UserTestBase.language_code, + can_join_groups=UserTestBase.can_join_groups, + can_read_all_group_messages=UserTestBase.can_read_all_group_messages, + supports_inline_queries=UserTestBase.supports_inline_queries, + is_premium=UserTestBase.is_premium, + added_to_attachment_menu=UserTestBase.added_to_attachment_menu, + can_connect_to_business=UserTestBase.can_connect_to_business, + has_main_web_app=UserTestBase.has_main_web_app, + ) + user.set_bot(bot) + user._unfreeze() + return user + + +class UserTestBase: + id_ = 1 + is_bot = True + first_name = "first\u2022name" + last_name = "last\u2022name" + username = "username" + language_code = "en_us" + can_join_groups = True + can_read_all_group_messages = True + supports_inline_queries = False + is_premium = True + added_to_attachment_menu = False + can_connect_to_business = True + has_main_web_app = False + + +class TestUserWithoutRequest(UserTestBase): + def test_slot_behaviour(self, user): + for attr in user.__slots__: + assert getattr(user, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(user)) == len(set(mro_slots(user))), "duplicate slot" + + def test_de_json(self, json_dict, bot): + user = User.de_json(json_dict, bot) + assert user.api_kwargs == {} + + assert user.id == self.id_ + assert user.is_bot == self.is_bot + assert user.first_name == self.first_name + assert user.last_name == self.last_name + assert user.username == self.username + assert user.language_code == self.language_code + assert user.can_join_groups == self.can_join_groups + assert user.can_read_all_group_messages == self.can_read_all_group_messages + assert user.supports_inline_queries == self.supports_inline_queries + assert user.is_premium == self.is_premium + assert user.added_to_attachment_menu == self.added_to_attachment_menu + assert user.can_connect_to_business == self.can_connect_to_business + assert user.has_main_web_app == self.has_main_web_app + + def test_to_dict(self, user): + user_dict = user.to_dict() + + assert isinstance(user_dict, dict) + assert user_dict["id"] == user.id + assert user_dict["is_bot"] == user.is_bot + assert user_dict["first_name"] == user.first_name + assert user_dict["last_name"] == user.last_name + assert user_dict["username"] == user.username + assert user_dict["language_code"] == user.language_code + assert user_dict["can_join_groups"] == user.can_join_groups + assert user_dict["can_read_all_group_messages"] == user.can_read_all_group_messages + assert user_dict["supports_inline_queries"] == user.supports_inline_queries + assert user_dict["is_premium"] == user.is_premium + assert user_dict["added_to_attachment_menu"] == user.added_to_attachment_menu + assert user_dict["can_connect_to_business"] == user.can_connect_to_business + assert user_dict["has_main_web_app"] == user.has_main_web_app + + def test_equality(self): + a = User(self.id_, self.first_name, self.is_bot, self.last_name) + b = User(self.id_, self.first_name, self.is_bot, self.last_name) + c = User(self.id_, self.first_name, self.is_bot) + d = User(0, self.first_name, self.is_bot, self.last_name) + e = Update(self.id_) + + assert a == b + assert hash(a) == hash(b) + assert a is not b + + assert a == c + assert hash(a) == hash(c) + + assert a != d + assert hash(a) != hash(d) + + assert a != e + assert hash(a) != hash(e) + + def test_name(self, user): + assert user.name == "@username" + user.username = None + assert user.name == "first\u2022name last\u2022name" + user.last_name = None + assert user.name == "first\u2022name" + user.username = self.username + assert user.name == "@username" + + def test_full_name(self, user): + assert user.full_name == "first\u2022name last\u2022name" + user.last_name = None + assert user.full_name == "first\u2022name" + + def test_link(self, user): + assert user.link == f"https://t.me/{user.username}" + user.username = None + assert user.link is None + + async def test_instance_method_get_profile_photos(self, monkeypatch, user): + async def make_assertion(*_, **kwargs): + return kwargs["user_id"] == user.id + + assert check_shortcut_signature( + User.get_profile_photos, Bot.get_user_profile_photos, ["user_id"], [] + ) + assert await check_shortcut_call( + user.get_profile_photos, user.get_bot(), "get_user_profile_photos" + ) + assert await check_defaults_handling(user.get_profile_photos, user.get_bot()) + + monkeypatch.setattr(user.get_bot(), "get_user_profile_photos", make_assertion) + assert await user.get_profile_photos() + + async def test_instance_method_pin_message(self, monkeypatch, user): + async def make_assertion(*_, **kwargs): + return kwargs["chat_id"] == user.id + + assert check_shortcut_signature(User.pin_message, Bot.pin_chat_message, ["chat_id"], []) + assert await check_shortcut_call(user.pin_message, user.get_bot(), "pin_chat_message") + assert await check_defaults_handling(user.pin_message, user.get_bot()) + + monkeypatch.setattr(user.get_bot(), "pin_chat_message", make_assertion) + assert await user.pin_message(1) + + async def test_instance_method_unpin_message(self, monkeypatch, user): + async def make_assertion(*_, **kwargs): + return kwargs["chat_id"] == user.id + + assert check_shortcut_signature( + User.unpin_message, Bot.unpin_chat_message, ["chat_id"], [] + ) + assert await check_shortcut_call(user.unpin_message, user.get_bot(), "unpin_chat_message") + assert await check_defaults_handling(user.unpin_message, user.get_bot()) + + monkeypatch.setattr(user.get_bot(), "unpin_chat_message", make_assertion) + assert await user.unpin_message() + + async def test_instance_method_unpin_all_messages(self, monkeypatch, user): + async def make_assertion(*_, **kwargs): + return kwargs["chat_id"] == user.id + + assert check_shortcut_signature( + User.unpin_all_messages, Bot.unpin_all_chat_messages, ["chat_id"], [] + ) + assert await check_shortcut_call( + user.unpin_all_messages, user.get_bot(), "unpin_all_chat_messages" + ) + assert await check_defaults_handling(user.unpin_all_messages, user.get_bot()) + + monkeypatch.setattr(user.get_bot(), "unpin_all_chat_messages", make_assertion) + assert await user.unpin_all_messages() + + async def test_instance_method_send_message(self, monkeypatch, user): + async def make_assertion(*_, **kwargs): + return kwargs["chat_id"] == user.id and kwargs["text"] == "test" + + assert check_shortcut_signature(User.send_message, Bot.send_message, ["chat_id"], []) + assert await check_shortcut_call(user.send_message, user.get_bot(), "send_message") + assert await check_defaults_handling(user.send_message, user.get_bot()) + + monkeypatch.setattr(user.get_bot(), "send_message", make_assertion) + assert await user.send_message("test") + + async def test_instance_method_send_photo(self, monkeypatch, user): + async def make_assertion(*_, **kwargs): + return kwargs["chat_id"] == user.id and kwargs["photo"] == "test_photo" + + assert check_shortcut_signature(User.send_photo, Bot.send_photo, ["chat_id"], []) + assert await check_shortcut_call(user.send_photo, user.get_bot(), "send_photo") + assert await check_defaults_handling(user.send_photo, user.get_bot()) + + monkeypatch.setattr(user.get_bot(), "send_photo", make_assertion) + assert await user.send_photo("test_photo") + + async def test_instance_method_send_media_group(self, monkeypatch, user): + async def make_assertion(*_, **kwargs): + return kwargs["chat_id"] == user.id and kwargs["media"] == "test_media_group" + + assert check_shortcut_signature( + User.send_media_group, Bot.send_media_group, ["chat_id"], [] + ) + assert await check_shortcut_call(user.send_media_group, user.get_bot(), "send_media_group") + assert await check_defaults_handling(user.send_media_group, user.get_bot()) + + monkeypatch.setattr(user.get_bot(), "send_media_group", make_assertion) + assert await user.send_media_group("test_media_group") + + async def test_instance_method_send_audio(self, monkeypatch, user): + async def make_assertion(*_, **kwargs): + return kwargs["chat_id"] == user.id and kwargs["audio"] == "test_audio" + + assert check_shortcut_signature(User.send_audio, Bot.send_audio, ["chat_id"], []) + assert await check_shortcut_call(user.send_audio, user.get_bot(), "send_audio") + assert await check_defaults_handling(user.send_audio, user.get_bot()) + + monkeypatch.setattr(user.get_bot(), "send_audio", make_assertion) + assert await user.send_audio("test_audio") + + async def test_instance_method_send_chat_action(self, monkeypatch, user): + async def make_assertion(*_, **kwargs): + return kwargs["chat_id"] == user.id and kwargs["action"] == "test_chat_action" + + assert check_shortcut_signature( + User.send_chat_action, Bot.send_chat_action, ["chat_id"], [] + ) + assert await check_shortcut_call(user.send_chat_action, user.get_bot(), "send_chat_action") + assert await check_defaults_handling(user.send_chat_action, user.get_bot()) + + monkeypatch.setattr(user.get_bot(), "send_chat_action", make_assertion) + assert await user.send_chat_action("test_chat_action") + + async def test_instance_method_send_contact(self, monkeypatch, user): + async def make_assertion(*_, **kwargs): + return kwargs["chat_id"] == user.id and kwargs["phone_number"] == "test_contact" + + assert check_shortcut_signature(User.send_contact, Bot.send_contact, ["chat_id"], []) + assert await check_shortcut_call(user.send_contact, user.get_bot(), "send_contact") + assert await check_defaults_handling(user.send_contact, user.get_bot()) + + monkeypatch.setattr(user.get_bot(), "send_contact", make_assertion) + assert await user.send_contact(phone_number="test_contact") + + async def test_instance_method_send_dice(self, monkeypatch, user): + async def make_assertion(*_, **kwargs): + return kwargs["chat_id"] == user.id and kwargs["emoji"] == "test_dice" + + assert check_shortcut_signature(User.send_dice, Bot.send_dice, ["chat_id"], []) + assert await check_shortcut_call(user.send_dice, user.get_bot(), "send_dice") + assert await check_defaults_handling(user.send_dice, user.get_bot()) + + monkeypatch.setattr(user.get_bot(), "send_dice", make_assertion) + assert await user.send_dice(emoji="test_dice") + + async def test_instance_method_send_document(self, monkeypatch, user): + async def make_assertion(*_, **kwargs): + return kwargs["chat_id"] == user.id and kwargs["document"] == "test_document" + + assert check_shortcut_signature(User.send_document, Bot.send_document, ["chat_id"], []) + assert await check_shortcut_call(user.send_document, user.get_bot(), "send_document") + assert await check_defaults_handling(user.send_document, user.get_bot()) + + monkeypatch.setattr(user.get_bot(), "send_document", make_assertion) + assert await user.send_document("test_document") + + async def test_instance_method_send_game(self, monkeypatch, user): + async def make_assertion(*_, **kwargs): + return kwargs["chat_id"] == user.id and kwargs["game_short_name"] == "test_game" + + assert check_shortcut_signature(User.send_game, Bot.send_game, ["chat_id"], []) + assert await check_shortcut_call(user.send_game, user.get_bot(), "send_game") + assert await check_defaults_handling(user.send_game, user.get_bot()) + + monkeypatch.setattr(user.get_bot(), "send_game", make_assertion) + assert await user.send_game(game_short_name="test_game") + + async def test_instance_method_send_invoice(self, monkeypatch, user): + async def make_assertion(*_, **kwargs): + title = kwargs["title"] == "title" + description = kwargs["description"] == "description" + payload = kwargs["payload"] == "payload" + provider_token = kwargs["provider_token"] == "provider_token" + currency = kwargs["currency"] == "currency" + prices = kwargs["prices"] == "prices" + args = title and description and payload and provider_token and currency and prices + return kwargs["chat_id"] == user.id and args + + assert check_shortcut_signature(User.send_invoice, Bot.send_invoice, ["chat_id"], []) + assert await check_shortcut_call(user.send_invoice, user.get_bot(), "send_invoice") + assert await check_defaults_handling(user.send_invoice, user.get_bot()) + + monkeypatch.setattr(user.get_bot(), "send_invoice", make_assertion) + assert await user.send_invoice( + "title", + "description", + "payload", + "provider_token", + "currency", + "prices", + ) + + async def test_instance_method_send_location(self, monkeypatch, user): + async def make_assertion(*_, **kwargs): + return kwargs["chat_id"] == user.id and kwargs["latitude"] == "test_location" + + assert check_shortcut_signature(User.send_location, Bot.send_location, ["chat_id"], []) + assert await check_shortcut_call(user.send_location, user.get_bot(), "send_location") + assert await check_defaults_handling(user.send_location, user.get_bot()) + + monkeypatch.setattr(user.get_bot(), "send_location", make_assertion) + assert await user.send_location("test_location") + + async def test_instance_method_send_sticker(self, monkeypatch, user): + async def make_assertion(*_, **kwargs): + return kwargs["chat_id"] == user.id and kwargs["sticker"] == "test_sticker" + + assert check_shortcut_signature(User.send_sticker, Bot.send_sticker, ["chat_id"], []) + assert await check_shortcut_call(user.send_sticker, user.get_bot(), "send_sticker") + assert await check_defaults_handling(user.send_sticker, user.get_bot()) + + monkeypatch.setattr(user.get_bot(), "send_sticker", make_assertion) + assert await user.send_sticker("test_sticker") + + async def test_instance_method_send_video(self, monkeypatch, user): + async def make_assertion(*_, **kwargs): + return kwargs["chat_id"] == user.id and kwargs["video"] == "test_video" + + assert check_shortcut_signature(User.send_video, Bot.send_video, ["chat_id"], []) + assert await check_shortcut_call(user.send_video, user.get_bot(), "send_video") + assert await check_defaults_handling(user.send_video, user.get_bot()) + + monkeypatch.setattr(user.get_bot(), "send_video", make_assertion) + assert await user.send_video("test_video") + + async def test_instance_method_send_venue(self, monkeypatch, user): + async def make_assertion(*_, **kwargs): + return kwargs["chat_id"] == user.id and kwargs["title"] == "test_venue" + + assert check_shortcut_signature(User.send_venue, Bot.send_venue, ["chat_id"], []) + assert await check_shortcut_call(user.send_venue, user.get_bot(), "send_venue") + assert await check_defaults_handling(user.send_venue, user.get_bot()) + + monkeypatch.setattr(user.get_bot(), "send_venue", make_assertion) + assert await user.send_venue(title="test_venue") + + async def test_instance_method_send_video_note(self, monkeypatch, user): + async def make_assertion(*_, **kwargs): + return kwargs["chat_id"] == user.id and kwargs["video_note"] == "test_video_note" + + assert check_shortcut_signature(User.send_video_note, Bot.send_video_note, ["chat_id"], []) + assert await check_shortcut_call(user.send_video_note, user.get_bot(), "send_video_note") + assert await check_defaults_handling(user.send_video_note, user.get_bot()) + + monkeypatch.setattr(user.get_bot(), "send_video_note", make_assertion) + assert await user.send_video_note("test_video_note") + + async def test_instance_method_send_voice(self, monkeypatch, user): + async def make_assertion(*_, **kwargs): + return kwargs["chat_id"] == user.id and kwargs["voice"] == "test_voice" + + assert check_shortcut_signature(User.send_voice, Bot.send_voice, ["chat_id"], []) + assert await check_shortcut_call(user.send_voice, user.get_bot(), "send_voice") + assert await check_defaults_handling(user.send_voice, user.get_bot()) + + monkeypatch.setattr(user.get_bot(), "send_voice", make_assertion) + assert await user.send_voice("test_voice") + + async def test_instance_method_send_animation(self, monkeypatch, user): + async def make_assertion(*_, **kwargs): + return kwargs["chat_id"] == user.id and kwargs["animation"] == "test_animation" + + assert check_shortcut_signature(User.send_animation, Bot.send_animation, ["chat_id"], []) + assert await check_shortcut_call(user.send_animation, user.get_bot(), "send_animation") + assert await check_defaults_handling(user.send_animation, user.get_bot()) + + monkeypatch.setattr(user.get_bot(), "send_animation", make_assertion) + assert await user.send_animation("test_animation") + + async def test_instance_method_send_poll(self, monkeypatch, user): + async def make_assertion(*_, **kwargs): + return kwargs["chat_id"] == user.id and kwargs["question"] == "test_poll" + + assert check_shortcut_signature(User.send_poll, Bot.send_poll, ["chat_id"], []) + assert await check_shortcut_call(user.send_poll, user.get_bot(), "send_poll") + assert await check_defaults_handling(user.send_poll, user.get_bot()) + + monkeypatch.setattr(user.get_bot(), "send_poll", make_assertion) + assert await user.send_poll(question="test_poll", options=[1, 2]) + + async def test_instance_method_send_copy(self, monkeypatch, user): + async def make_assertion(*_, **kwargs): + user_id = kwargs["chat_id"] == user.id + message_id = kwargs["message_id"] == "message_id" + from_chat_id = kwargs["from_chat_id"] == "from_chat_id" + return from_chat_id and message_id and user_id + + assert check_shortcut_signature(User.send_copy, Bot.copy_message, ["chat_id"], []) + assert await check_shortcut_call(user.send_copy, user.get_bot(), "copy_message") + assert await check_defaults_handling(user.send_copy, user.get_bot()) + + monkeypatch.setattr(user.get_bot(), "copy_message", make_assertion) + assert await user.send_copy(from_chat_id="from_chat_id", message_id="message_id") + + async def test_instance_method_copy_message(self, monkeypatch, user): + async def make_assertion(*_, **kwargs): + chat_id = kwargs["chat_id"] == "chat_id" + message_id = kwargs["message_id"] == "message_id" + user_id = kwargs["from_chat_id"] == user.id + return chat_id and message_id and user_id + + assert check_shortcut_signature(User.copy_message, Bot.copy_message, ["from_chat_id"], []) + assert await check_shortcut_call(user.copy_message, user.get_bot(), "copy_message") + assert await check_defaults_handling(user.copy_message, user.get_bot()) + + monkeypatch.setattr(user.get_bot(), "copy_message", make_assertion) + assert await user.copy_message(chat_id="chat_id", message_id="message_id") + + async def test_instance_method_get_user_chat_boosts(self, monkeypatch, user): + async def make_assertion(*_, **kwargs): + chat_id = kwargs["chat_id"] == "chat_id" + user_id = kwargs["user_id"] == user.id + return chat_id and user_id + + assert check_shortcut_signature( + User.get_chat_boosts, Bot.get_user_chat_boosts, ["user_id"], [] + ) + assert await check_shortcut_call( + user.get_chat_boosts, user.get_bot(), "get_user_chat_boosts" + ) + assert await check_defaults_handling(user.get_chat_boosts, user.get_bot()) + + monkeypatch.setattr(user.get_bot(), "get_user_chat_boosts", make_assertion) + assert await user.get_chat_boosts(chat_id="chat_id") + + async def test_instance_method_get_menu_button(self, monkeypatch, user): + async def make_assertion(*_, **kwargs): + return kwargs["chat_id"] == user.id + + assert check_shortcut_signature( + User.get_menu_button, Bot.get_chat_menu_button, ["chat_id"], [] + ) + assert await check_shortcut_call( + user.get_menu_button, + user.get_bot(), + "get_chat_menu_button", + shortcut_kwargs=["chat_id"], + ) + assert await check_defaults_handling(user.get_menu_button, user.get_bot()) + + monkeypatch.setattr(user.get_bot(), "get_chat_menu_button", make_assertion) + assert await user.get_menu_button() + + async def test_instance_method_set_menu_button(self, monkeypatch, user): + async def make_assertion(*_, **kwargs): + return kwargs["chat_id"] == user.id and kwargs["menu_button"] == "menu_button" + + assert check_shortcut_signature( + User.set_menu_button, Bot.set_chat_menu_button, ["chat_id"], [] + ) + assert await check_shortcut_call( + user.set_menu_button, + user.get_bot(), + "set_chat_menu_button", + shortcut_kwargs=["chat_id"], + ) + assert await check_defaults_handling(user.set_menu_button, user.get_bot()) + + monkeypatch.setattr(user.get_bot(), "set_chat_menu_button", make_assertion) + assert await user.set_menu_button(menu_button="menu_button") + + async def test_instance_method_approve_join_request(self, monkeypatch, user): + async def make_assertion(*_, **kwargs): + chat_id = kwargs["chat_id"] == "chat_id" + user_id = kwargs["user_id"] == user.id + return chat_id and user_id + + assert check_shortcut_signature( + User.approve_join_request, Bot.approve_chat_join_request, ["user_id"], [] + ) + assert await check_shortcut_call( + user.approve_join_request, user.get_bot(), "approve_chat_join_request" + ) + assert await check_defaults_handling(user.approve_join_request, user.get_bot()) + + monkeypatch.setattr(user.get_bot(), "approve_chat_join_request", make_assertion) + assert await user.approve_join_request(chat_id="chat_id") + + async def test_instance_method_decline_join_request(self, monkeypatch, user): + async def make_assertion(*_, **kwargs): + chat_id = kwargs["chat_id"] == "chat_id" + user_id = kwargs["user_id"] == user.id + return chat_id and user_id + + assert check_shortcut_signature( + User.decline_join_request, Bot.decline_chat_join_request, ["user_id"], [] + ) + assert await check_shortcut_call( + user.decline_join_request, user.get_bot(), "decline_chat_join_request" + ) + assert await check_defaults_handling(user.decline_join_request, user.get_bot()) + + monkeypatch.setattr(user.get_bot(), "decline_chat_join_request", make_assertion) + assert await user.decline_join_request(chat_id="chat_id") + + async def test_mention_html(self, user): + expected = '{}' + + assert user.mention_html() == expected.format(user.id, user.full_name) + assert user.mention_html("thename\u2022") == expected.format( + user.id, "the<b>name\u2022" + ) + assert user.mention_html(user.username) == expected.format(user.id, user.username) + + def test_mention_button(self, user): + expected_name = InlineKeyboardButton(text="Bob", url=f"tg://user?id={user.id}") + expected_full = InlineKeyboardButton(text=user.full_name, url=f"tg://user?id={user.id}") + + assert user.mention_button("Bob") == expected_name + assert user.mention_button() == expected_full + + def test_mention_markdown(self, user): + expected = "[{}](tg://user?id={})" + + assert user.mention_markdown() == expected.format(user.full_name, user.id) + assert user.mention_markdown("the_name*\u2022") == expected.format( + "the_name*\u2022", user.id + ) + assert user.mention_markdown(user.username) == expected.format(user.username, user.id) + + async def test_mention_markdown_v2(self, user): + user.first_name = "first{name" + user.last_name = "last_name" + + expected = "[{}](tg://user?id={})" + + assert user.mention_markdown_v2() == expected.format( + escape_markdown(user.full_name, version=2), user.id + ) + assert user.mention_markdown_v2("the{name>\u2022") == expected.format( + "the\\{name\\>\u2022", user.id + ) + assert user.mention_markdown_v2(user.username) == expected.format(user.username, user.id) + + async def test_delete_message(self, monkeypatch, user): + async def make_assertion(*_, **kwargs): + return kwargs["chat_id"] == user.id and kwargs["message_id"] == 42 + + assert check_shortcut_signature(user.delete_message, Bot.delete_message, ["chat_id"], []) + assert await check_shortcut_call(user.delete_message, user.get_bot(), "delete_message") + assert await check_defaults_handling(user.delete_message, user.get_bot()) + + monkeypatch.setattr(user.get_bot(), "delete_message", make_assertion) + assert await user.delete_message(message_id=42) + + async def test_delete_messages(self, monkeypatch, user): + async def make_assertion(*_, **kwargs): + return kwargs["chat_id"] == user.id and kwargs["message_ids"] == (42, 43) + + assert check_shortcut_signature(user.delete_messages, Bot.delete_messages, ["chat_id"], []) + assert await check_shortcut_call(user.delete_messages, user.get_bot(), "delete_messages") + assert await check_defaults_handling(user.delete_messages, user.get_bot()) + + monkeypatch.setattr(user.get_bot(), "delete_messages", make_assertion) + assert await user.delete_messages(message_ids=(42, 43)) + + async def test_instance_method_send_copies(self, monkeypatch, user): + async def make_assertion(*_, **kwargs): + from_chat_id = kwargs["from_chat_id"] == "test_copies" + message_ids = kwargs["message_ids"] == (42, 43) + user_id = kwargs["chat_id"] == user.id + return from_chat_id and message_ids and user_id + + assert check_shortcut_signature(user.send_copies, Bot.copy_messages, ["chat_id"], []) + assert await check_shortcut_call(user.send_copies, user.get_bot(), "copy_messages") + assert await check_defaults_handling(user.send_copies, user.get_bot()) + + monkeypatch.setattr(user.get_bot(), "copy_messages", make_assertion) + assert await user.send_copies(from_chat_id="test_copies", message_ids=(42, 43)) + + async def test_instance_method_copy_messages(self, monkeypatch, user): + async def make_assertion(*_, **kwargs): + from_chat_id = kwargs["from_chat_id"] == user.id + message_ids = kwargs["message_ids"] == (42, 43) + user_id = kwargs["chat_id"] == "test_copies" + return from_chat_id and message_ids and user_id + + assert check_shortcut_signature( + user.copy_messages, Bot.copy_messages, ["from_chat_id"], [] + ) + assert await check_shortcut_call(user.copy_messages, user.get_bot(), "copy_messages") + assert await check_defaults_handling(user.copy_messages, user.get_bot()) + + monkeypatch.setattr(user.get_bot(), "copy_messages", make_assertion) + assert await user.copy_messages(chat_id="test_copies", message_ids=(42, 43)) + + async def test_instance_method_forward_from(self, monkeypatch, user): + async def make_assertion(*_, **kwargs): + user_id = kwargs["chat_id"] == user.id + message_id = kwargs["message_id"] == 42 + from_chat_id = kwargs["from_chat_id"] == "test_forward" + return from_chat_id and message_id and user_id + + assert check_shortcut_signature(user.forward_from, Bot.forward_message, ["chat_id"], []) + assert await check_shortcut_call(user.forward_from, user.get_bot(), "forward_message") + assert await check_defaults_handling(user.forward_from, user.get_bot()) + + monkeypatch.setattr(user.get_bot(), "forward_message", make_assertion) + assert await user.forward_from(from_chat_id="test_forward", message_id=42) + + async def test_instance_method_forward_to(self, monkeypatch, user): + async def make_assertion(*_, **kwargs): + from_chat_id = kwargs["from_chat_id"] == user.id + message_id = kwargs["message_id"] == 42 + user_id = kwargs["chat_id"] == "test_forward" + return from_chat_id and message_id and user_id + + assert check_shortcut_signature(user.forward_to, Bot.forward_message, ["from_chat_id"], []) + assert await check_shortcut_call(user.forward_to, user.get_bot(), "forward_message") + assert await check_defaults_handling(user.forward_to, user.get_bot()) + + monkeypatch.setattr(user.get_bot(), "forward_message", make_assertion) + assert await user.forward_to(chat_id="test_forward", message_id=42) + + async def test_instance_method_forward_messages_from(self, monkeypatch, user): + async def make_assertion(*_, **kwargs): + user_id = kwargs["chat_id"] == user.id + message_ids = kwargs["message_ids"] == (42, 43) + from_chat_id = kwargs["from_chat_id"] == "test_forwards" + return from_chat_id and message_ids and user_id + + assert check_shortcut_signature( + user.forward_messages_from, Bot.forward_messages, ["chat_id"], [] + ) + assert await check_shortcut_call( + user.forward_messages_from, user.get_bot(), "forward_messages" + ) + assert await check_defaults_handling(user.forward_messages_from, user.get_bot()) + + monkeypatch.setattr(user.get_bot(), "forward_messages", make_assertion) + assert await user.forward_messages_from(from_chat_id="test_forwards", message_ids=(42, 43)) + + async def test_instance_method_forward_messages_to(self, monkeypatch, user): + async def make_assertion(*_, **kwargs): + from_chat_id = kwargs["from_chat_id"] == user.id + message_ids = kwargs["message_ids"] == (42, 43) + user_id = kwargs["chat_id"] == "test_forwards" + return from_chat_id and message_ids and user_id + + assert check_shortcut_signature( + user.forward_messages_to, Bot.forward_messages, ["from_chat_id"], [] + ) + assert await check_shortcut_call( + user.forward_messages_to, user.get_bot(), "forward_messages" + ) + assert await check_defaults_handling(user.forward_messages_to, user.get_bot()) + + monkeypatch.setattr(user.get_bot(), "forward_messages", make_assertion) + assert await user.forward_messages_to(chat_id="test_forwards", message_ids=(42, 43)) + + async def test_instance_method_refund_star_payment(self, monkeypatch, user): + async def make_assertion(*_, **kwargs): + return kwargs["user_id"] == user.id and kwargs["telegram_payment_charge_id"] == 42 + + assert check_shortcut_signature( + user.refund_star_payment, Bot.refund_star_payment, ["user_id"], [] + ) + assert await check_shortcut_call( + user.refund_star_payment, user.get_bot(), "refund_star_payment" + ) + assert await check_defaults_handling(user.refund_star_payment, user.get_bot()) + + monkeypatch.setattr(user.get_bot(), "refund_star_payment", make_assertion) + assert await user.refund_star_payment(telegram_payment_charge_id=42) diff --git a/test_userprofilephotos.py b/test_userprofilephotos.py new file mode 100644 index 0000000000000000000000000000000000000000..f0017ce6ca6a0acdcc9cea5396c3b09dabdb8a65 --- /dev/null +++ b/test_userprofilephotos.py @@ -0,0 +1,72 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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/]. +from telegram import PhotoSize, UserProfilePhotos +from tests.auxil.slots import mro_slots + + +class UserProfilePhotosTestBase: + total_count = 2 + photos = [ + [ + PhotoSize("file_id1", "file_un_id1", 512, 512), + PhotoSize("file_id2", "file_un_id1", 512, 512), + ], + [ + PhotoSize("file_id3", "file_un_id3", 512, 512), + PhotoSize("file_id4", "file_un_id4", 512, 512), + ], + ] + + +class TestUserProfilePhotosWithoutRequest(UserProfilePhotosTestBase): + def test_slot_behaviour(self): + inst = UserProfilePhotos(self.total_count, self.photos) + for attr in inst.__slots__: + assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" + + def test_de_json(self, bot): + json_dict = {"total_count": 2, "photos": [[y.to_dict() for y in x] for x in self.photos]} + user_profile_photos = UserProfilePhotos.de_json(json_dict, bot) + assert user_profile_photos.api_kwargs == {} + assert user_profile_photos.total_count == self.total_count + assert user_profile_photos.photos == tuple(tuple(p) for p in self.photos) + + def test_to_dict(self): + user_profile_photos = UserProfilePhotos(self.total_count, self.photos) + user_profile_photos_dict = user_profile_photos.to_dict() + assert user_profile_photos_dict["total_count"] == user_profile_photos.total_count + for ix, x in enumerate(user_profile_photos_dict["photos"]): + for iy, y in enumerate(x): + assert y == user_profile_photos.photos[ix][iy].to_dict() + + def test_equality(self): + a = UserProfilePhotos(2, self.photos) + b = UserProfilePhotos(2, self.photos) + c = UserProfilePhotos(1, [self.photos[0]]) + d = PhotoSize("file_id1", "unique_id", 512, 512) + + assert a == b + assert hash(a) == hash(b) + + assert a != c + assert hash(a) != hash(c) + + assert a != d + assert hash(a) != hash(d) diff --git a/test_version.py b/test_version.py new file mode 100644 index 0000000000000000000000000000000000000000..489bde0d6f91a9e4dc09a003f9c09d1bc9324f99 --- /dev/null +++ b/test_version.py @@ -0,0 +1,88 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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/]. +import pytest + +from telegram import ( + __bot_api_version__, + __bot_api_version_info__, + __version__, + __version_info__, + constants, +) +from telegram._version import Version + + +class TestVersion: + def test_bot_api_version_and_info(self): + assert __bot_api_version__ is constants.BOT_API_VERSION + assert __bot_api_version_info__ is constants.BOT_API_VERSION_INFO + + def test_version_and_info(self): + assert __version__ == str(__version_info__) + + @pytest.mark.parametrize( + ("version", "expected"), + [ + (Version(1, 2, 3, "alpha", 4), "1.2.3a4"), + (Version(2, 3, 4, "beta", 5), "2.3.4b5"), + (Version(1, 2, 3, "candidate", 4), "1.2.3rc4"), + (Version(1, 2, 0, "alpha", 4), "1.2a4"), + (Version(2, 3, 0, "beta", 5), "2.3b5"), + (Version(1, 2, 0, "candidate", 4), "1.2rc4"), + (Version(1, 2, 3, "final", 0), "1.2.3"), + (Version(1, 2, 0, "final", 0), "1.2"), + ], + ) + def test_version_str(self, version, expected): + assert str(version) == expected + + @pytest.mark.parametrize("use_tuple", [True, False]) + def test_version_info(self, use_tuple): + version = Version(1, 2, 3, "beta", 4) + assert isinstance(version, tuple) + assert version.major == version[0] + assert version.minor == version[1] + assert version.micro == version[2] + assert version.releaselevel == version[3] + assert version.serial == version[4] + + class TestClass: + def __new__(cls, *args): + if use_tuple: + return tuple(args) + return Version(*args) + + assert isinstance(TestClass(1, 2, 3, "beta", 4), tuple if use_tuple else Version) + assert version == TestClass(1, 2, 3, "beta", 4) + assert not (version < TestClass(1, 2, 3, "beta", 4)) + assert version > TestClass(1, 2, 3, "beta", 3) + assert version > TestClass(1, 2, 3, "alpha", 4) + assert version < TestClass(1, 2, 3, "candidate", 0) + assert version < TestClass(1, 2, 3, "final", 0) + assert version < TestClass(1, 2, 4, "final", 0) + assert version < TestClass(1, 3, 4, "final", 0) + + assert version < (1, 3) + assert version >= (1, 2, 3, "alpha") + assert version > (1, 1) + assert version <= (1, 2, 3, "beta", 4) + assert version < (1, 2, 3, "candidate", 4) + assert not (version > (1, 2, 3, "candidate", 4)) + assert version < (1, 2, 4) + assert version > (1, 2, 2) diff --git a/test_videochat.py b/test_videochat.py new file mode 100644 index 0000000000000000000000000000000000000000..5fbdf8ba8eb5cc8d0003bc3bca8147061cc92be0 --- /dev/null +++ b/test_videochat.py @@ -0,0 +1,210 @@ +#!/usr/bin/env python +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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/]. + +import datetime as dtm + +import pytest + +from telegram import ( + User, + VideoChatEnded, + VideoChatParticipantsInvited, + VideoChatScheduled, + VideoChatStarted, +) +from telegram._utils.datetime import UTC, to_timestamp +from tests.auxil.slots import mro_slots + + +@pytest.fixture(scope="module") +def user1(): + return User(first_name="Misses Test", id=123, is_bot=False) + + +@pytest.fixture(scope="module") +def user2(): + return User(first_name="Mister Test", id=124, is_bot=False) + + +class TestVideoChatStartedWithoutRequest: + def test_slot_behaviour(self): + action = VideoChatStarted() + for attr in action.__slots__: + assert getattr(action, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(action)) == len(set(mro_slots(action))), "duplicate slot" + + def test_de_json(self): + video_chat_started = VideoChatStarted.de_json({}, None) + assert video_chat_started.api_kwargs == {} + assert isinstance(video_chat_started, VideoChatStarted) + + def test_to_dict(self): + video_chat_started = VideoChatStarted() + video_chat_dict = video_chat_started.to_dict() + assert video_chat_dict == {} + + +class TestVideoChatEndedWithoutRequest: + duration = 100 + + def test_slot_behaviour(self): + action = VideoChatEnded(8) + for attr in action.__slots__: + assert getattr(action, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(action)) == len(set(mro_slots(action))), "duplicate slot" + + def test_de_json(self): + json_dict = {"duration": self.duration} + video_chat_ended = VideoChatEnded.de_json(json_dict, None) + assert video_chat_ended.api_kwargs == {} + + assert video_chat_ended.duration == self.duration + + def test_to_dict(self): + video_chat_ended = VideoChatEnded(self.duration) + video_chat_dict = video_chat_ended.to_dict() + + assert isinstance(video_chat_dict, dict) + assert video_chat_dict["duration"] == self.duration + + def test_equality(self): + a = VideoChatEnded(100) + b = VideoChatEnded(100) + c = VideoChatEnded(50) + d = VideoChatStarted() + + assert a == b + assert hash(a) == hash(b) + + assert a != c + assert hash(a) != hash(c) + + assert a != d + assert hash(a) != hash(d) + + +class TestVideoChatParticipantsInvitedWithoutRequest: + def test_slot_behaviour(self, user1): + action = VideoChatParticipantsInvited([user1]) + for attr in action.__slots__: + assert getattr(action, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(action)) == len(set(mro_slots(action))), "duplicate slot" + + def test_de_json(self, user1, user2, bot): + json_data = {"users": [user1.to_dict(), user2.to_dict()]} + video_chat_participants = VideoChatParticipantsInvited.de_json(json_data, bot) + assert video_chat_participants.api_kwargs == {} + + assert isinstance(video_chat_participants.users, tuple) + assert video_chat_participants.users[0] == user1 + assert video_chat_participants.users[1] == user2 + assert video_chat_participants.users[0].id == user1.id + assert video_chat_participants.users[1].id == user2.id + + @pytest.mark.parametrize("use_users", [True, False]) + def test_to_dict(self, user1, user2, use_users): + video_chat_participants = VideoChatParticipantsInvited([user1, user2] if use_users else ()) + video_chat_dict = video_chat_participants.to_dict() + + assert isinstance(video_chat_dict, dict) + if use_users: + assert video_chat_dict["users"] == [user1.to_dict(), user2.to_dict()] + assert video_chat_dict["users"][0]["id"] == user1.id + assert video_chat_dict["users"][1]["id"] == user2.id + else: + assert video_chat_dict == {} + + def test_equality(self, user1, user2): + a = VideoChatParticipantsInvited([user1]) + b = VideoChatParticipantsInvited([user1]) + c = VideoChatParticipantsInvited([user1, user2]) + d = VideoChatParticipantsInvited([]) + e = VideoChatStarted() + + assert a == b + assert hash(a) == hash(b) + + assert a != c + assert hash(a) != hash(c) + + assert a != d + assert hash(a) != hash(d) + + assert a != e + assert hash(a) != hash(e) + + +class TestVideoChatScheduledWithoutRequest: + start_date = dtm.datetime.now(dtm.timezone.utc) + + def test_slot_behaviour(self): + inst = VideoChatScheduled(self.start_date) + for attr in inst.__slots__: + assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" + + def test_expected_values(self): + assert VideoChatScheduled(self.start_date).start_date == self.start_date + + def test_de_json(self, bot): + assert VideoChatScheduled.de_json({}, bot=bot) is None + + json_dict = {"start_date": to_timestamp(self.start_date)} + video_chat_scheduled = VideoChatScheduled.de_json(json_dict, bot) + assert video_chat_scheduled.api_kwargs == {} + + assert abs(video_chat_scheduled.start_date - self.start_date) < dtm.timedelta(seconds=1) + + def test_de_json_localization(self, tz_bot, bot, raw_bot): + json_dict = {"start_date": to_timestamp(self.start_date)} + + videochat_raw = VideoChatScheduled.de_json(json_dict, raw_bot) + videochat_bot = VideoChatScheduled.de_json(json_dict, bot) + videochat_tz = VideoChatScheduled.de_json(json_dict, tz_bot) + + # comparing utcoffsets because comparing timezones is unpredicatable + videochat_offset = videochat_tz.start_date.utcoffset() + tz_bot_offset = tz_bot.defaults.tzinfo.utcoffset( + videochat_tz.start_date.replace(tzinfo=None) + ) + + assert videochat_raw.start_date.tzinfo == UTC + assert videochat_bot.start_date.tzinfo == UTC + assert videochat_offset == tz_bot_offset + + def test_to_dict(self): + video_chat_scheduled = VideoChatScheduled(self.start_date) + video_chat_scheduled_dict = video_chat_scheduled.to_dict() + + assert isinstance(video_chat_scheduled_dict, dict) + assert video_chat_scheduled_dict["start_date"] == to_timestamp(self.start_date) + + def test_equality(self): + a = VideoChatScheduled(self.start_date) + b = VideoChatScheduled(self.start_date) + c = VideoChatScheduled(dtm.datetime.utcnow() + dtm.timedelta(seconds=5)) + d = VideoChatStarted() + + assert a == b + assert hash(a) == hash(b) + + assert a != c + assert hash(a) != hash(c) + + assert a != d + assert hash(a) != hash(d) diff --git a/test_warnings.py b/test_warnings.py new file mode 100644 index 0000000000000000000000000000000000000000..3e3beb48fd440be6758a9781bcc4c5c5b7f3d585 --- /dev/null +++ b/test_warnings.py @@ -0,0 +1,87 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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/]. +from collections import defaultdict +from pathlib import Path + +import pytest + +from telegram._utils.warnings import warn +from telegram.warnings import PTBDeprecationWarning, PTBRuntimeWarning, PTBUserWarning +from tests.auxil.files import PROJECT_ROOT_PATH +from tests.auxil.slots import mro_slots + + +class TestWarnings: + @pytest.mark.parametrize( + "inst", + [ + (PTBUserWarning("test message")), + (PTBRuntimeWarning("test message")), + (PTBDeprecationWarning("20.6", "test message")), + ], + ) + def test_slots_behavior(self, inst): + for attr in inst.__slots__: + assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" + + def test_test_coverage(self): + """This test is only here to make sure that new warning classes will set __slots__ + properly. + Add the new warning class to the below covered_subclasses dict, if it's covered in the + above test_slots_behavior tests. + """ + + def make_assertion(cls): + assert set(cls.__subclasses__()) == covered_subclasses[cls] + for subcls in cls.__subclasses__(): + make_assertion(subcls) + + covered_subclasses = defaultdict(set) + covered_subclasses.update( + { + PTBUserWarning: { + PTBRuntimeWarning, + PTBDeprecationWarning, + }, + } + ) + + make_assertion(PTBUserWarning) + + def test_warn(self, recwarn): + expected_file = PROJECT_ROOT_PATH / "telegram" / "_utils" / "warnings.py" + + warn("test message") + assert len(recwarn) == 1 + assert recwarn[0].category is PTBUserWarning + assert str(recwarn[0].message) == "test message" + assert Path(recwarn[0].filename) == expected_file, "incorrect stacklevel!" + + warn("test message 2", category=PTBRuntimeWarning) + assert len(recwarn) == 2 + assert recwarn[1].category is PTBRuntimeWarning + assert str(recwarn[1].message) == "test message 2" + assert Path(recwarn[1].filename) == expected_file, "incorrect stacklevel!" + + warn(PTBDeprecationWarning("20.6", "test message 3"), stacklevel=1) + assert len(recwarn) == 3 + assert recwarn[2].category is PTBDeprecationWarning + assert str(recwarn[2].message) == "Deprecated since version 20.6: test message 3" + assert Path(recwarn[2].filename) == Path(__file__), "incorrect stacklevel!" diff --git a/test_webappdata.py b/test_webappdata.py new file mode 100644 index 0000000000000000000000000000000000000000..a13043042c2a72d56f43f6966bf7036eac9a5a4b --- /dev/null +++ b/test_webappdata.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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/]. + +import pytest + +from telegram import WebAppData +from tests.auxil.slots import mro_slots + + +@pytest.fixture(scope="module") +def web_app_data(): + return WebAppData(data=WebAppDataTestBase.data, button_text=WebAppDataTestBase.button_text) + + +class WebAppDataTestBase: + data = "data" + button_text = "button_text" + + +class TestWebAppDataWithoutRequest(WebAppDataTestBase): + def test_slot_behaviour(self, web_app_data): + for attr in web_app_data.__slots__: + assert getattr(web_app_data, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(web_app_data)) == len(set(mro_slots(web_app_data))), "duplicate slot" + + def test_to_dict(self, web_app_data): + web_app_data_dict = web_app_data.to_dict() + + assert isinstance(web_app_data_dict, dict) + assert web_app_data_dict["data"] == self.data + assert web_app_data_dict["button_text"] == self.button_text + + def test_de_json(self, bot): + json_dict = {"data": self.data, "button_text": self.button_text} + web_app_data = WebAppData.de_json(json_dict, bot) + assert web_app_data.api_kwargs == {} + + assert web_app_data.data == self.data + assert web_app_data.button_text == self.button_text + + def test_equality(self): + a = WebAppData(self.data, self.button_text) + b = WebAppData(self.data, self.button_text) + c = WebAppData("", "") + d = WebAppData("not_data", "not_button_text") + + assert a == b + assert hash(a) == hash(b) + assert a is not b + + assert a != c + assert hash(a) != hash(c) + + assert a != d + assert hash(a) != hash(d) diff --git a/test_webappinfo.py b/test_webappinfo.py new file mode 100644 index 0000000000000000000000000000000000000000..40d6587335184fc014b8a1f8905c7b46e7ffc000 --- /dev/null +++ b/test_webappinfo.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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/]. + +import pytest + +from telegram import WebAppInfo +from tests.auxil.slots import mro_slots + + +@pytest.fixture(scope="module") +def web_app_info(): + return WebAppInfo(url=WebAppInfoTestBase.url) + + +class WebAppInfoTestBase: + url = "https://www.example.com" + + +class TestWebAppInfoWithoutRequest(WebAppInfoTestBase): + def test_slot_behaviour(self, web_app_info): + for attr in web_app_info.__slots__: + assert getattr(web_app_info, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(web_app_info)) == len(set(mro_slots(web_app_info))), "duplicate slot" + + def test_to_dict(self, web_app_info): + web_app_info_dict = web_app_info.to_dict() + + assert isinstance(web_app_info_dict, dict) + assert web_app_info_dict["url"] == self.url + + def test_de_json(self, bot): + json_dict = {"url": self.url} + web_app_info = WebAppInfo.de_json(json_dict, bot) + assert web_app_info.api_kwargs == {} + + assert web_app_info.url == self.url + + def test_equality(self): + a = WebAppInfo(self.url) + b = WebAppInfo(self.url) + c = WebAppInfo("") + d = WebAppInfo("not_url") + + assert a == b + assert hash(a) == hash(b) + assert a is not b + + assert a != c + assert hash(a) != hash(c) + + assert a != d + assert hash(a) != hash(d) diff --git a/test_webhookinfo.py b/test_webhookinfo.py new file mode 100644 index 0000000000000000000000000000000000000000..48bb5ee38e64bac9678fe38db5f639a668dec164 --- /dev/null +++ b/test_webhookinfo.py @@ -0,0 +1,188 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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/]. +import time +from datetime import datetime + +import pytest + +from telegram import LoginUrl, WebhookInfo +from telegram._utils.datetime import UTC, from_timestamp +from tests.auxil.slots import mro_slots + + +@pytest.fixture(scope="module") +def webhook_info(): + return WebhookInfo( + url=WebhookInfoTestBase.url, + has_custom_certificate=WebhookInfoTestBase.has_custom_certificate, + pending_update_count=WebhookInfoTestBase.pending_update_count, + ip_address=WebhookInfoTestBase.ip_address, + last_error_date=WebhookInfoTestBase.last_error_date, + max_connections=WebhookInfoTestBase.max_connections, + allowed_updates=WebhookInfoTestBase.allowed_updates, + last_synchronization_error_date=WebhookInfoTestBase.last_synchronization_error_date, + ) + + +class WebhookInfoTestBase: + url = "http://www.google.com" + has_custom_certificate = False + pending_update_count = 5 + ip_address = "127.0.0.1" + last_error_date = time.time() + max_connections = 42 + allowed_updates = ["type1", "type2"] + last_synchronization_error_date = time.time() + + +class TestWebhookInfoWithoutRequest(WebhookInfoTestBase): + def test_slot_behaviour(self, webhook_info): + for attr in webhook_info.__slots__: + assert getattr(webhook_info, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(webhook_info)) == len(set(mro_slots(webhook_info))), "duplicate slot" + + def test_to_dict(self, webhook_info): + webhook_info_dict = webhook_info.to_dict() + + assert isinstance(webhook_info_dict, dict) + assert webhook_info_dict["url"] == self.url + assert webhook_info_dict["pending_update_count"] == self.pending_update_count + assert webhook_info_dict["last_error_date"] == self.last_error_date + assert webhook_info_dict["max_connections"] == self.max_connections + assert webhook_info_dict["allowed_updates"] == self.allowed_updates + assert webhook_info_dict["ip_address"] == self.ip_address + assert ( + webhook_info_dict["last_synchronization_error_date"] + == self.last_synchronization_error_date + ) + + def test_de_json(self, bot): + json_dict = { + "url": self.url, + "has_custom_certificate": self.has_custom_certificate, + "pending_update_count": self.pending_update_count, + "last_error_date": self.last_error_date, + "max_connections": self.max_connections, + "allowed_updates": self.allowed_updates, + "ip_address": self.ip_address, + "last_synchronization_error_date": self.last_synchronization_error_date, + } + webhook_info = WebhookInfo.de_json(json_dict, bot) + assert webhook_info.api_kwargs == {} + + assert webhook_info.url == self.url + assert webhook_info.has_custom_certificate == self.has_custom_certificate + assert webhook_info.pending_update_count == self.pending_update_count + assert isinstance(webhook_info.last_error_date, datetime) + assert webhook_info.last_error_date == from_timestamp(self.last_error_date) + assert webhook_info.max_connections == self.max_connections + assert webhook_info.allowed_updates == tuple(self.allowed_updates) + assert webhook_info.ip_address == self.ip_address + assert isinstance(webhook_info.last_synchronization_error_date, datetime) + assert webhook_info.last_synchronization_error_date == from_timestamp( + self.last_synchronization_error_date + ) + + none = WebhookInfo.de_json(None, bot) + assert none is None + + def test_de_json_localization(self, bot, raw_bot, tz_bot): + json_dict = { + "url": self.url, + "has_custom_certificate": self.has_custom_certificate, + "pending_update_count": self.pending_update_count, + "last_error_date": self.last_error_date, + "max_connections": self.max_connections, + "allowed_updates": self.allowed_updates, + "ip_address": self.ip_address, + "last_synchronization_error_date": self.last_synchronization_error_date, + } + webhook_info_bot = WebhookInfo.de_json(json_dict, bot) + webhook_info_raw = WebhookInfo.de_json(json_dict, raw_bot) + webhook_info_tz = WebhookInfo.de_json(json_dict, tz_bot) + + # comparing utcoffsets because comparing timezones is unpredicatable + last_error_date_offset = webhook_info_tz.last_error_date.utcoffset() + last_error_tz_bot_offset = tz_bot.defaults.tzinfo.utcoffset( + webhook_info_tz.last_error_date.replace(tzinfo=None) + ) + + sync_error_date_offset = webhook_info_tz.last_synchronization_error_date.utcoffset() + sync_error_date_tz_bot_offset = tz_bot.defaults.tzinfo.utcoffset( + webhook_info_tz.last_synchronization_error_date.replace(tzinfo=None) + ) + + assert webhook_info_raw.last_error_date.tzinfo == UTC + assert webhook_info_bot.last_error_date.tzinfo == UTC + assert last_error_date_offset == last_error_tz_bot_offset + + assert webhook_info_raw.last_synchronization_error_date.tzinfo == UTC + assert webhook_info_bot.last_synchronization_error_date.tzinfo == UTC + assert sync_error_date_offset == sync_error_date_tz_bot_offset + + def test_always_tuple_allowed_updates(self): + webhook_info = WebhookInfo( + self.url, self.has_custom_certificate, self.pending_update_count + ) + assert webhook_info.allowed_updates == () + + def test_equality(self): + a = WebhookInfo( + url=self.url, + has_custom_certificate=self.has_custom_certificate, + pending_update_count=self.pending_update_count, + last_error_date=self.last_error_date, + max_connections=self.max_connections, + ) + b = WebhookInfo( + url=self.url, + has_custom_certificate=self.has_custom_certificate, + pending_update_count=self.pending_update_count, + last_error_date=self.last_error_date, + max_connections=self.max_connections, + ) + c = WebhookInfo( + url="http://github.com", + has_custom_certificate=True, + pending_update_count=78, + last_error_date=0, + max_connections=1, + ) + d = WebhookInfo( + url="http://github.com", + has_custom_certificate=True, + pending_update_count=78, + last_error_date=0, + max_connections=1, + last_synchronization_error_date=123, + ) + e = LoginUrl("text.com") + + assert a == b + assert hash(a) == hash(b) + assert a is not b + + assert a != c + assert hash(a) != hash(c) + + assert a != d + assert hash(a) != hash(d) + + assert a != e + assert hash(a) != hash(e) diff --git a/test_writeaccessallowed.py b/test_writeaccessallowed.py new file mode 100644 index 0000000000000000000000000000000000000000..0f8a043156919970fab155b7295d73fdc1b4d45e --- /dev/null +++ b/test_writeaccessallowed.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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/]. +from telegram import WriteAccessAllowed +from tests.auxil.slots import mro_slots + + +class TestWriteAccessAllowed: + def test_slot_behaviour(self): + action = WriteAccessAllowed() + for attr in action.__slots__: + assert getattr(action, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(action)) == len(set(mro_slots(action))), "duplicate slot" + + def test_de_json(self): + action = WriteAccessAllowed.de_json({}, None) + assert action.api_kwargs == {} + assert isinstance(action, WriteAccessAllowed) + + def test_to_dict(self): + action = WriteAccessAllowed() + action_dict = action.to_dict() + assert action_dict == {} + + def test_equality(self): + a = WriteAccessAllowed() + b = WriteAccessAllowed() + c = WriteAccessAllowed(web_app_name="foo") + d = WriteAccessAllowed(web_app_name="foo") + e = WriteAccessAllowed(web_app_name="bar") + + assert a == b + assert hash(a) == hash(b) + + assert a != c + assert hash(a) != hash(c) + + assert c == d + assert hash(c) == hash(d) + + assert c != e + assert hash(c) != hash(e) diff --git a/warnings.py b/warnings.py new file mode 100644 index 0000000000000000000000000000000000000000..0c761b97421cdb479999ca6dd638dc17bddc4805 --- /dev/null +++ b/warnings.py @@ -0,0 +1,87 @@ +#! /usr/bin/env python +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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 used for warnings issued by this library. + +.. versionadded:: 20.0 +""" + +__all__ = ["PTBDeprecationWarning", "PTBRuntimeWarning", "PTBUserWarning"] + + +class PTBUserWarning(UserWarning): + """ + Custom user warning class used for warnings in this library. + + .. seealso:: :wiki:`Exceptions, Warnings and Logging ` + + .. versionadded:: 20.0 + """ + + __slots__ = () + + +class PTBRuntimeWarning(PTBUserWarning, RuntimeWarning): + """ + Custom runtime warning class used for warnings in this library. + + .. versionadded:: 20.0 + """ + + __slots__ = () + + +# https://www.python.org/dev/peps/pep-0565/ recommends using a custom warning class derived from +# DeprecationWarning. We also subclass from PTBUserWarning so users can easily 'switch off' +# warnings +class PTBDeprecationWarning(PTBUserWarning, DeprecationWarning): + """ + Custom warning class for deprecations in this library. + + .. versionchanged:: 20.0 + Renamed TelegramDeprecationWarning to PTBDeprecationWarning. + + Args: + version (:obj:`str`): The version in which the feature was deprecated. + + .. versionadded:: 21.2 + message (:obj:`str`): The message to display. + + .. versionadded:: 21.2 + + Attributes: + version (:obj:`str`): The version in which the feature was deprecated. + + .. versionadded:: 21.2 + message (:obj:`str`): The message to display. + + .. versionadded:: 21.2 + """ + + __slots__ = ("message", "version") + + def __init__(self, version: str, message: str) -> None: + self.version: str = version + self.message: str = message + + def __str__(self) -> str: + """Returns a string representation of the warning, using :attr:`message` and + :attr:`version`. + + .. versionadded:: 21.2 + """ + return f"Deprecated since version {self.version}: {self.message}"